为什么 SFINAE 对于 std::basic_string 构造函数之一如此严格?

Why is SFINAE for one of the std::basic_string constructors so restrictive?

背景

关于这个的讨论是在 下开始的,问题很简单。

问题

此简单代码对 std::basic_string:

的构造函数进行了意外的重载解析
#include <string>

int main() {
    std::string s{"some string to test", 2, 3};
    return 0;
}

现在有人期望这将调用此构造函数:

std::basic_string<CharT,Traits,Allocator>::basic_string - cppreference.com

template< class T > basic_string( const T& t, size_type pos, size_type n, const Allocator& alloc = Allocator() ); (since C++17) (11)
template< class T > constexpr basic_string( const T& t, size_type pos, size_type const Allocator& alloc = Allocator() ); (since C++20) (11)

基本原理基于此 c++ 标准的部分:

[string.cons]

template<class T> constexpr basic_string(const T& t, size_type pos, size_type n, const Allocator& a = Allocator());

5 Constraints: is_­convertible_­v<const T&, basic_­string_­view<charT, traits>> is true.

6 Effects: Creates a variable, sv, as if by basic_­string_­view<charT, traits> sv = t; and then behaves the same as: basic_string(sv.substr(pos, n), a);

现在当此代码由 cppinsights 处理时,结果与预期不同:

#include <string>

int main()
{
  std::string s = std::basic_string<char>{std::basic_string<char>("some string to test", std::allocator<char>()), 2, 3};
  return 0;
}

将此代码折叠为 std::string 的相应类型定义后,它变成了:

#include <string>

int main()
{
  std::string s = std::string{std::string("some string to test"), 2, 3};
  return 0;
}

因此执行了字符串文字到 std::string 的转换,然后使用了 (3) 形式的构造函数。因此执行了不希望的额外分配。

我确定这不是工具的问题我做了其他实验来证实这一点。这里有一个godbolt example。组装显然遵循这种模式。

分析

为了找到此问题的解释,我已 introduced an error 此代码,因此错误报告会打印可能的过载解决方案。这是错误报告中最有趣的部分:

/opt/compiler-explorer/gcc-trunk-20220104/include/c++/12.0.0/bits/basic_string.h:771:9: note: candidate: 'template<class _Tp, class> std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _Tp&, size_type, size_type, const _Alloc&) [with <template-parameter-2-2> = _Tp; _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
  771 |         basic_string(const _Tp& __t, size_type __pos, size_type __n,
      |         ^~~~~~~~~~~~
/opt/compiler-explorer/gcc-trunk-20220104/include/c++/12.0.0/bits/basic_string.h:771:9: note:   template argument deduction/substitution failed:
In file included from /opt/compiler-explorer/gcc-trunk-20220104/include/c++/12.0.0/bits/char_traits.h:42,
                 from /opt/compiler-explorer/gcc-trunk-20220104/include/c++/12.0.0/string:40,
                 from <source>:1:
/opt/compiler-explorer/gcc-trunk-20220104/include/c++/12.0.0/type_traits: In substitution of 'template<bool _Cond, class _Tp> using enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = void]':
/opt/compiler-explorer/gcc-trunk-20220104/include/c++/12.0.0/bits/basic_string.h:156:8:   required by substitution of 'template<class _CharT, class _Traits, class _Alloc> template<class _Tp, class _Res> using _If_sv = std::enable_if_t<std::__and_<std::is_convertible<const _Tp&, std::basic_string_view<_CharT, _Traits> >, std::__not_<std::is_convertible<const _Tp*, const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>*> >, std::__not_<std::is_convertible<const _Tp&, const _CharT*> > >::value, _Res> [with _Tp = char [20]; _Res = void; _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]'
/opt/compiler-explorer/gcc-trunk-20220104/include/c++/12.0.0/bits/basic_string.h:769:30:   required from here
/opt/compiler-explorer/gcc-trunk-20220104/include/c++/12.0.0/type_traits:2614:11: error: no type named 'type' in 'struct std::enable_if<false, void>'
 2614 |     using enable_if_t = typename enable_if<_Cond, _Tp>::type;
      |           ^~~~~~~~~~~

因此重载 (11) 已被 SFINAE 拒绝。

相关的标准库代码如下: std::basic_string 构造函数的 Overload (11)

    template<typename _Tp, typename = _If_sv<_Tp, void>>
    basic_string(const _Tp& __t, size_type __pos, size_type __n,
             const _Alloc& __a = _Alloc())
    : basic_string(_S_to_string_view(__t).substr(__pos, __n), __a) { }

这里是 _If_sv 的实现:

    template<typename _Tp, typename _Res>
    using _If_sv = enable_if_t<
      __and_<is_convertible<const _Tp&, __sv_type>,
         __not_<is_convertible<const _Tp*, const basic_string*>>,
         __not_<is_convertible<const _Tp&, const _CharT*>>>::value,
      _Res>;

所以这个条件的最后一部分:__not_<is_convertible<const _Tp&, const _CharT*>>>::value 是一个问题,因为显然 char 数组(字符串文字)可以转换为指向字符的指针。

问题

为什么 _If_sv 的实现看起来像这样?有这个标准中没有提到的额外条件的理由是什么?

如果我正确理解重载决议,这个额外条件已经过时,因为当第一个参数是 std::string 时,重载 (3) 优先于重载 (11) 作为非模板函数的精确匹配.另一方面,如果一个参数不是 std::string 并且它可以转换为 std::string_view 那么模板重载 (11) 应该获胜并且避免额外分配。

这是标准库实现中的错误还是我遗漏了什么?

为什么需要这些额外的 SFINAE 条件?

奖金问题

有没有好的方法来编写检测此问题的测试? 在不修改测试代码的情况下验证是否选择了正确的重载的一些方法(在本例中 std::basic_string)?

也许我错了,但好像是最后一部分:

__not_<is_convertible<const _Tp&, const _CharT*>>>::value

添加到 _If_sv 以防止这两个构造函数之间的歧义:

constexpr basic_string( const CharT* s,
                        const Allocator& alloc = Allocator() ); //(5)

template< class T >
explicit basic_string( const T& t,
                       const Allocator& alloc = Allocator() ); //(10)
如果 T 可转换为 string_view,则应选择

(10)。但是(10)是explicit所以这道题应该选不带{}。我认为 _If_sv 中不需要此检查。他们可能对您提到的重载 (10) 和 (11) 使用了相同的 _If_sv,因此现在两者都无法正常工作。

在 C++14 中,您的代码总是创建一个新的 std::string 临时对象,没有采用 const char*std::string_view 的构造函数可以使用。

在 C++17 中,您的代码 应该 创建一个 string_view 并避免临时。 GCC 的实现受到过度约束,因此仍然表现得像 C++14,创建了额外的临时文件。

Why implementation of _If_sv looks like this? What is rationale to have this extra conditions not mentioned in standard?

它有三个条件,其中两个在标准中是。第三个也是有原因的。

_If_sv 助手实现每个 basic_string 函数所需的约束,该函数采用 basic_string_view 除了 您要问的构造函数关于。那个约束稍微弱一些,允许将 const CharT* 指针传递给它并转换为字符串视图。因为 libstdc++ 对该构造函数使用相同的 _If_sv 帮助程序,所以它不能按标准要求工作。我现在已经修复了 (PR 103919)。

_If_sv的三个条件中:

  • 标准要求“可转换为 string_view
  • 所有函数的标准都要求“不可转换为 const CharT*除了您所询问的构造函数。
  • 添加了最后一个条件,即“不可转换为 basic_string”以修复 regression I discovered,这是由标准指定的约束引起的。这个问题在下面描述,对于那些不喜欢点击链接离开 SO 的人。

If I understood overload resolution correctly this extra conditions are obsolete,

没有。

since when first argument is std::string overloads (3) has priority over overload (11) as exact match of non template function.

是的,如果它是 std::string 那么无论如何都会选择另一个构造函数。 “不可转换为 std::string”约束在这里是多余的,因为无论如何都会选择另一个构造函数。它无害,但多余。但是,如果参数不是 std::string,而是来自 std::stringderived 怎么办?考虑:

class tstring : public std::string
{
public:
  tstring() : std::string() {}
  tstring(tstring&& s) : std::string(std::move(s)) {}
};

在 C++14 中,这按预期使用了 basic_string(basic_string&&) 移动构造函数。在 C++17 中,它使用 basic_string(const T&, const Alloc& = Alloc()),因为 tstring&& 参数可转换为 string_view。这意味着我们复制 s 而不是按预期移动它。

T 不可转换为 std::string 的附加约束在此处删除了新的 string_view 重载,因此再次使用移动构造函数。因此,对于无论如何都不会选择构造函数的情况,额外的约束是无害的,但对于将选择但不应该选择新的 basic_string(const T&, const Alloc&) 构造函数的情况,有用