为什么其中一个std::BASIC_STRING构造函数的SFINAE如此严格?

2022-04-05 00:00:00 language-lawyer c++ c++20 c++17 std

背景

在this answer下针对非常简单的问题开始了有关此问题的讨论。

问题

此简单代码意外地解析了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

模板<;类T&>BASIC_STRING(常量T&;t,SIZE_TYPE位置,SIZE_TYPE n,常量分配器&;Alloc=Allocator()); (从C++17开始) (11)
模板<;类T&>conexpr BASIC_STRING(const T&;t,SIZE_TYPE位置,SIZE_TYPE常量分配器&;Alloc=Allocator()); (从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约束:是否可转换为常量T&A;,基本字符串查看<;图表,特征&>为真.

6效果:创建一个变量sv,类似于BASIC_STRING_UNVIEW<;图表,特征&>sv=t;然后行为与: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拒绝。

相关标准库代码如下所示: Overload (11)STD::BASIC_STRING构造函数:

    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是一个问题,因为清楚地字符数组(字符串文字)可以转换为指向字符的指针。

问题

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

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

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

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

奖金问题

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


解决方案

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

在C++17中,您的代码应该创建一个string_view并避免临时的。GCC的实现被过度约束,因此其行为仍类似于C++14,从而创建了额外的临时。

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

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

_If_sv帮助器实现每个接受basic_string_view(除外)的basic_string函数所需的约束。它有一个稍弱的约束,允许将const CharT*指针传递给它并转换为字符串视图。因为libstdc++对该构造函数使用相同的_If_sv帮助器,所以它不能按照标准要求工作。我现在已修复(PR 103919)。

_If_sv中的三个条件:

  • 标准要求将";转换为string_view
  • 不可转换为const CharT*";的标准要求除以外的所有函数都需要一个。
  • 添加了最后一个条件,即不能转换为basic_string";one,以修复由标准指定的约束引起的regression I discovered。下面描述了这个问题,不喜欢点击链接的人可以离开。

如果我对重载解析的理解正确,这些额外条件已过时

没有。

因为当第一个参数为std::string时,重载(3)的优先级高于重载(11),因为它与非模板函数完全匹配。

是的,如果它是std::string,那么无论如何都会选择另一个构造函数。不能转换为std::string";约束在这里是多余的,因为无论如何都会选择另一个构造函数。这是无害的,但却是多余的。但是,如果参数不是std::string,而是派生自std::string该怎么办?请考虑:

class tstring : public std::string
{
public:
  tstring() : std::string() {}
  tstring(tstring&& s) : std::string(std::move(s)) {}
};
在C++14中,它按照预期使用了basic_string(basic_string&&)Move构造函数。在C++17中,它使用basic_string(const T&, const Alloc& = Alloc()),因为tstring&&参数可以转换为string_view。这意味着我们复制s,而不是按计划移动它。

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

相关文章