`std::tuple_size_v`在不同编译器上的不同SFINAE行为
考虑这个代码:
#include <tuple>
#include <type_traits>
#include <iostream>
template <typename T, typename = void> struct is_tuple_like : std::false_type {};
template <typename T> struct is_tuple_like<T, decltype(std::tuple_size_v<T>, void())> : std::true_type {};
int main()
{
std::cout << is_tuple_like<std::string>::value << 'n';
}
#include <tuple>
#include <type_traits>
#include <iostream>
template <typename T, typename = void> struct is_tuple_like : std::false_type {};
template <typename T> struct is_tuple_like<T, decltype(std::tuple_size_v<T>, void())> : std::true_type {};
int main()
{
std::cout << is_tuple_like<std::string>::value << 'n';
}
Run on gcc.godbolt.org
在 GCC 10.2 和 MSVC 19.28 上,它会导致硬错误,如下所示:
另一方面,在 Clang 11.0.1 上,它1
使用 libstdc++ 和 libc++编译和打印。
哪个编译器在这里是正确的?
请注意,Clang 打印1
而不是0
,这意味着它不会将std::tuple_size<std::string>::value
( 的初始化程序tuple_size_v
)视为软错误,而是选择完全忽略它!
这在某种程度上是有道理的,因为如果tuple_size_v
定义为template <typename T> inline constexpr size_t tuple_size_v = ...
,则类型decltype(tuple_size_v<...>)
不依赖于模板参数,并且始终为size_t
。
我想问题归结为是否tuple_size_v
需要在此处实例化of 的初始化程序,即使这不是绝对必要的。
我知道我可以通过替换std::tuple_size_v<...>
为来修复它std::tuple_size<...>::value
,然后它会0
在所有三个编译器上打印。
回答
我认为 Clang 有这个权利。
[temp.inst]/7 中的规则是:
除非变量模板特化是声明的特化,否则在需要变量定义存在的上下文中引用变量模板特化时,或者如果定义的存在影响程序的语义,则变量模板特化会被隐式实例化。
此程序不需要定义的std::tuple_size_v<std::string>
,只有声明。和声明:
template <typename T>
inline constexpr size_t tuple_size_v = tuple_size<T>::value;
足以评估部分特化中的表达式。decltype(std::tuple_size_v<T>, void())
根本不依赖于这里的值,对于 any size_t
,这是 type 的有效表达式void
。
我们处理的是函数模板而不是变量模板:
template <typename T>
constexpr size_t tuple_size_v() { return tuple_size<T>::value; }
也许更清楚的是,我们不需要定义,只需要声明,并且 gcc 和 msvc 都接受这种替代公式(实际上,gcc 甚至警告说它毫无意义):example。
后来,在[temp.inst]/8,我们有:
如果变量或函数需要由表达式 ([expr.const]) 进行常量求值,则认为变量或函数的定义的存在会影响程序的语义,即使不需要对表达式进行常量求值或者如果常量表达式计算不使用定义。
但这里的情况并非如此:我们不需要变量来进行常量评估。