为什么 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++ 标准的部分:
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::string
的 derived 怎么办?考虑:
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&)
构造函数的情况,有用。
背景
关于这个的讨论是在
问题
此简单代码对 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++ 标准的部分:
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::string
的 derived 怎么办?考虑:
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&)
构造函数的情况,有用。