为什么其中一个std::BASIC_STRING构造函数的SFINAE如此严格?
背景
在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&)
构造函数但不应该选择的情况,有用。
相关文章