为什么这个 SFINAE 片段在 g++ 中不起作用,但在 MSVC 中起作用?
Why this SFINAE snippet is not working in g++, but working in MSVC?
在 MSVC2017 中这工作正常,static_asserts 都没有按预期触发:
template <typename T>
struct do_have_size {
template <typename = decltype(std::declval<T>().size())>
static std::true_type check(T);
static std::false_type check(...);
using type = decltype(check(std::declval<T>()));
};
int main() {
using TR = typename do_have_size<std::vector<int>>::type;
using FL = typename do_have_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}
但是,如果我在 g++7.1 或 clang 4.0 中编译,我会得到以下编译器错误:
In instantiation of 'struct do_have_size<int>':
20:39: required from here
9:24: error: request for member 'size' in 'declval<do_have_size<int>::TP>()', which is of non-class type 'int'
根据我对 SFINAE 的理解,true_type
返回函数的替换对于 int
参数应该失败,并且应该选择下一个函数,就像在 MSVC 中所做的那样。为什么 clang 和 g++ 根本不编译它?
我只用 -std=c++17
开关编译,也许还需要更多东西?
SFINAE 在这里不起作用,因为 class 已经在 do_have_size<int>::type
中用 T = int
实例化了。 SFINAE 仅适用于模板函数候选列表,在您的情况下,您会遇到一个硬错误,因为在实例化中
do_have_size<int>::type
成员函数
template <typename = decltype(std::declval<int>().size())>
static std::true_type check(T);
对于 int
肯定是病式的。
static std::false_type check(...);
永远不会考虑。所以 gcc 就在这里拒绝你的代码,MSVC2017 不应该接受代码。
相关:std::enable_if : parameter vs template parameter and SFINAE working in return type but not as template parameter
一个解决方案 是使用 void_t
的魔力(自 C++17 起,但您可以在 C++11/14 中定义自己的),它将任何类型列表映射到 void
并启用看起来非常简单的 SFINAE 技术,就像这样
#include <utility>
#include <vector>
template<typename...>
using void_t = void; // that's how void_t is defined in C++17
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
int main() {
using TR = typename has_size<std::vector<int>>::type;
using FL = typename has_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}
Here 是 Walter Brown 的 Cppcon 视频,其中非常详细地解释了 void_t
技术,我强烈推荐它!
我通过使用 std::enable_if to SFINAE away the template version of check based on either the parameter or the return type. The condition I used was std::is_fundamental 从实例化模板中排除 int、float 和其他非 class 类型来使其工作。我将 -std=c++1z
标志用于 clang 和 gcc。我希望 -std=c++14
也应该有效。
#include <type_traits>
#include <utility>
#include <vector>
template <typename T>
struct do_have_size {
static std::false_type check(...);
template <typename U = T, typename = decltype(std::declval<U>().size())>
static std::true_type check(std::enable_if_t<!std::is_fundamental<U>::value, U>);
// OR
//template <typename U = T, typename = decltype(std::declval<U>().size())>
//static auto check(U)
// -> std::enable_if_t<!std::is_fundamental<U>::value, std::true_type>;
using type = decltype(check(std::declval<T>()));
};
int main() {
using TR = typename do_have_size<std::vector<int>>::type;
using FL = typename do_have_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}
@vsoftco 回答了 "gcc is right to reject your code"。我同意。
要修复,我说这样做:
namespace details {
template<template<class...>class Z, class, class...Ts>
struct can_apply:std::false_type{};
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t = typename voider<Ts...>::type;
template<template<class...>class Z, class...Ts>
struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z,void,Ts...>;
这是一个 can_apply
库,使这种 SFINAE 变得简单。
现在编写这些特征之一非常简单:
template<class T>
using dot_size_r = decltype( std::declval<T>().size() );
template<class T>
using has_dot_size = can_apply< dot_size_r, T >;
测试代码:
int main() {
static_assert( has_dot_size<std::vector<int>>{}, "TRUE" );
static_assert( !has_dot_size<int>{}, "FALSE" );
}
实例。
在 C++17 中,您可以转移到更少的 declval 填充表达式。
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
template<class F>
constexpr auto can_invoke(F&&) {
return [](auto&&...args) {
return std::is_invocable< F(decltype(args)...) >{};
};
}
can_invoke
接受一个函数 f
和 return 一个 "invokation tester"。调用测试器接受参数,然后 returns true_type
如果这些参数有效传递给 f
,否则 false_type
。
RETURNS
可以轻松地使单语句 lambda SFINAE 变得友好。在 C++17 中,如果可能的话,lambda 的操作是 constexpr(这就是我们在这里需要 C++17 的原因)。
然后,这给了我们:
template<class T>
constexpr auto can_dot_size(T&& t) {
return can_invoke([](auto&& x) RETURNS(x.size()))(t);
}
现在,我们经常这样做是因为我们希望尽可能调用 .size()
,否则 return 0.
template<class T, class A, class...Bs>
decltype(auto) call_first_valid(T&& t, A&& a, Bs&&...bs) {
if constexpr( can_invoke(std::forward<A>(a))(std::forward<T>(t)) ) {
return std::forward<A>(a)(std::forward<T>(t));
else
return call_first_valid(std::forward<T>(t), std::forward<Bs>(bs)...);
}
现在我们可以
template<class T>
std::size_t size_at_least( T&& t ) {
return call_first_valid( std::forward<T>(t),
[](auto&& t) RETURNS(t.size()),
[](auto&&)->std::size_t { return 0; }
);
}
碰巧,@Barry 在 C++20 中提出了一项功能,用 [](auto&& f)=>f.size()
(以及更多)替换 [](auto&& f) RETURNS(f.size())
。
这与默认模板参数是否是函数模板签名的一部分完全无关。
真正的问题是 T
是一个 class 模板参数,当您实例化 class 模板的定义时,实现可以立即将其替换为您的默认模板参数, decltype(std::declval<T>().size())
在模板参数推导之外,如果 size
不存在会导致硬错误。
修复很简单;简单地让它依赖于函数模板的参数。
template <typename U, typename = decltype(std::declval<U>().size())>
static std::true_type check(U);
(您的实现还有其他问题,例如它需要一个可移动构造的非抽象 T
并且不需要 size()
是可常量调用的,但它们不是'这不是您所看到的错误的原因。)
在 MSVC2017 中这工作正常,static_asserts 都没有按预期触发:
template <typename T>
struct do_have_size {
template <typename = decltype(std::declval<T>().size())>
static std::true_type check(T);
static std::false_type check(...);
using type = decltype(check(std::declval<T>()));
};
int main() {
using TR = typename do_have_size<std::vector<int>>::type;
using FL = typename do_have_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}
但是,如果我在 g++7.1 或 clang 4.0 中编译,我会得到以下编译器错误:
In instantiation of 'struct do_have_size<int>':
20:39: required from here
9:24: error: request for member 'size' in 'declval<do_have_size<int>::TP>()', which is of non-class type 'int'
根据我对 SFINAE 的理解,true_type
返回函数的替换对于 int
参数应该失败,并且应该选择下一个函数,就像在 MSVC 中所做的那样。为什么 clang 和 g++ 根本不编译它?
我只用 -std=c++17
开关编译,也许还需要更多东西?
SFINAE 在这里不起作用,因为 class 已经在 do_have_size<int>::type
中用 T = int
实例化了。 SFINAE 仅适用于模板函数候选列表,在您的情况下,您会遇到一个硬错误,因为在实例化中
do_have_size<int>::type
成员函数
template <typename = decltype(std::declval<int>().size())>
static std::true_type check(T);
对于 int
肯定是病式的。
static std::false_type check(...);
永远不会考虑。所以 gcc 就在这里拒绝你的代码,MSVC2017 不应该接受代码。
相关:std::enable_if : parameter vs template parameter and SFINAE working in return type but not as template parameter
一个解决方案 是使用 void_t
的魔力(自 C++17 起,但您可以在 C++11/14 中定义自己的),它将任何类型列表映射到 void
并启用看起来非常简单的 SFINAE 技术,就像这样
#include <utility>
#include <vector>
template<typename...>
using void_t = void; // that's how void_t is defined in C++17
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
int main() {
using TR = typename has_size<std::vector<int>>::type;
using FL = typename has_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}
Here 是 Walter Brown 的 Cppcon 视频,其中非常详细地解释了 void_t
技术,我强烈推荐它!
我通过使用 std::enable_if to SFINAE away the template version of check based on either the parameter or the return type. The condition I used was std::is_fundamental 从实例化模板中排除 int、float 和其他非 class 类型来使其工作。我将 -std=c++1z
标志用于 clang 和 gcc。我希望 -std=c++14
也应该有效。
#include <type_traits>
#include <utility>
#include <vector>
template <typename T>
struct do_have_size {
static std::false_type check(...);
template <typename U = T, typename = decltype(std::declval<U>().size())>
static std::true_type check(std::enable_if_t<!std::is_fundamental<U>::value, U>);
// OR
//template <typename U = T, typename = decltype(std::declval<U>().size())>
//static auto check(U)
// -> std::enable_if_t<!std::is_fundamental<U>::value, std::true_type>;
using type = decltype(check(std::declval<T>()));
};
int main() {
using TR = typename do_have_size<std::vector<int>>::type;
using FL = typename do_have_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}
@vsoftco 回答了 "gcc is right to reject your code"。我同意。
要修复,我说这样做:
namespace details {
template<template<class...>class Z, class, class...Ts>
struct can_apply:std::false_type{};
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t = typename voider<Ts...>::type;
template<template<class...>class Z, class...Ts>
struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z,void,Ts...>;
这是一个 can_apply
库,使这种 SFINAE 变得简单。
现在编写这些特征之一非常简单:
template<class T>
using dot_size_r = decltype( std::declval<T>().size() );
template<class T>
using has_dot_size = can_apply< dot_size_r, T >;
测试代码:
int main() {
static_assert( has_dot_size<std::vector<int>>{}, "TRUE" );
static_assert( !has_dot_size<int>{}, "FALSE" );
}
实例。
在 C++17 中,您可以转移到更少的 declval 填充表达式。
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
template<class F>
constexpr auto can_invoke(F&&) {
return [](auto&&...args) {
return std::is_invocable< F(decltype(args)...) >{};
};
}
can_invoke
接受一个函数 f
和 return 一个 "invokation tester"。调用测试器接受参数,然后 returns true_type
如果这些参数有效传递给 f
,否则 false_type
。
RETURNS
可以轻松地使单语句 lambda SFINAE 变得友好。在 C++17 中,如果可能的话,lambda 的操作是 constexpr(这就是我们在这里需要 C++17 的原因)。
然后,这给了我们:
template<class T>
constexpr auto can_dot_size(T&& t) {
return can_invoke([](auto&& x) RETURNS(x.size()))(t);
}
现在,我们经常这样做是因为我们希望尽可能调用 .size()
,否则 return 0.
template<class T, class A, class...Bs>
decltype(auto) call_first_valid(T&& t, A&& a, Bs&&...bs) {
if constexpr( can_invoke(std::forward<A>(a))(std::forward<T>(t)) ) {
return std::forward<A>(a)(std::forward<T>(t));
else
return call_first_valid(std::forward<T>(t), std::forward<Bs>(bs)...);
}
现在我们可以
template<class T>
std::size_t size_at_least( T&& t ) {
return call_first_valid( std::forward<T>(t),
[](auto&& t) RETURNS(t.size()),
[](auto&&)->std::size_t { return 0; }
);
}
碰巧,@Barry 在 C++20 中提出了一项功能,用 [](auto&& f)=>f.size()
(以及更多)替换 [](auto&& f) RETURNS(f.size())
。
这与默认模板参数是否是函数模板签名的一部分完全无关。
真正的问题是 T
是一个 class 模板参数,当您实例化 class 模板的定义时,实现可以立即将其替换为您的默认模板参数, decltype(std::declval<T>().size())
在模板参数推导之外,如果 size
不存在会导致硬错误。
修复很简单;简单地让它依赖于函数模板的参数。
template <typename U, typename = decltype(std::declval<U>().size())>
static std::true_type check(U);
(您的实现还有其他问题,例如它需要一个可移动构造的非抽象 T
并且不需要 size()
是可常量调用的,但它们不是'这不是您所看到的错误的原因。)