c++ 我们如何使用void_t进行SFINAE?

lhcgjxsq  于 2023-06-25  发布在  其他
关注(0)|答案(3)|浏览(93)

我看了Walter Brown在Cppcon14上关于现代模板编程(Part IPart II)的演讲,他在演讲中展示了他的void_t SFINAE技术。
示例:
给定一个简单的变量模板,如果所有的模板参数都是格式良好的,则计算结果为void

template< class ... > using void_t = void;

以及以下trait,用于检查名为 * member * 的数据成员是否存在:

template< class , class = void >
struct has_member : std::false_type
{ };

// specialized as has_member< T , void > or discarded (SFINAE)
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : std::true_type
{ };

我试图理解为什么以及如何工作。举个小例子:

class A {
public:
    int member;
};

class B {
};

static_assert( has_member< A >::value , "A" );
static_assert( has_member< B >::value , "B" );
    • 1.**has_member< A >
  • has_member< A , void_t< decltype( A::member ) > >
  • A::member存在
  • decltype( A::member )良好
  • void_t<>有效,计算结果为void
  • has_member< A , void >,因此它选择专用模板
  • has_member< T , void >,计算结果为true_type
    • 2.**has_member< B >
  • has_member< B , void_t< decltype( B::member ) > >
  • B::member不存在
  • decltype( B::member )不正确,并且以静默方式失败(sfinae)
  • has_member< B , expression-sfinae >,因此丢弃此模板
  • 编译器找到默认参数为void的has_member< B , class = void >
  • has_member< B >计算为false_type

http://ideone.com/HCTlBb
问题:
1.我的理解是否正确?

  1. Walter Brown指出,默认参数必须与void_t中使用的类型完全相同,才能正常工作。为什么会这样?(我不明白为什么这些类型需要匹配,难道不是任何默认类型都能完成这项工作吗?)
2admgd59

2admgd591#

1.主类模板

当你写has_member<A>::value时,编译器会查找名字has_member并找到 primary 类模板,也就是下面的声明:

template< class , class = void >
struct has_member;

(In OP,这是作为定义写的。)
将模板参数列表<A>与此主模板的模板参数列表进行比较。由于主模板有两个参数,但您只提供了一个,因此剩余的参数默认为默认模板参数:void。就好像你写了has_member<A, void>::value

2.专用类模板

现在,模板参数列表与模板has_member的任何专门化进行比较。只有在没有匹配的专门化时,才使用主模板的定义作为后备。因此,考虑部分专业化:

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

编译器尝试将模板参数A, void与部分特化中定义的模式相匹配:Tvoid_t<..>。***首先***进行模板参数推导。上面的部分专门化仍然是一个模板,其中的模板参数需要由实参“填充”。

第一个模式T,允许编译器推导模板参数T。这是一个微不足道的推论,但考虑像T const&这样的模式,我们仍然可以推导出T。对于模式T和模板参数A,我们推断TA
在第二个模式void_t< decltype( T::member ) >中,模板参数T出现在不能从任何模板参数推导出的上下文中。

原因有二:

  • decltype中的表达式被显式排除在模板参数推导之外。我想这是因为它可以任意复杂。
  • 即使我们使用了没有decltype的模式,如void_t< T >T的推导也会发生在解析的别名模板上。也就是说,我们解析别名模板,然后尝试从结果模式中推断T类型。然而,结果模式是void,它不依赖于T,因此不允许我们找到T的特定类型。这类似于试图反转常数函数的数学问题(在这些术语的数学意义上)。

模板参数推导完成(*),*现在*****推导的 * 模板参数被替换。这将创建一个特殊化,如下所示:

template<>
struct has_member< A, void_t< decltype( A::member ) > > : true_type
{ };

现在可以计算void_t< decltype( A::member ) >类型。它在替换后是格式良好的,因此,不会发生 * 替换失败 *。我们得到:

template<>
struct has_member<A, void> : true_type
{ };

3.选择

现在,我们可以将此专门化的模板参数列表与提供给原始has_member<A>::value的模板参数进行比较。这两种类型完全匹配,因此选择了这种部分专门化。

另一方面,当我们将模板定义为:

template< class , class = int > // <-- int here instead of void
struct has_member : false_type
{ };

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

我们最终得到相同的专业化:

template<>
struct has_member<A, void> : true_type
{ };

但是我们的has_member<A>::value的模板参数列表现在是<A, int>。参数与专门化的参数不匹配,主模板被选为后备模板。
(*)令人困惑的是,该标准包括替换过程和 * 模板参数推导 * 过程中显式指定的模板参数的匹配。例如(post-N4296)[temp.class.spec.match]/2:
如果部分专门化的模板参数可以从实际模板参数列表推导出,则部分专门化匹配给定的实际模板参数列表。
但这并不意味着必须推导出部分特化的所有模板参数;这也意味着替换必须成功并且(看起来?模板变元必须匹配部分专门化的(替代的)模板参数。请注意,我并不完全清楚 where,标准指定了替代参数列表和提供的参数列表之间的比较。

gwo2fgha

gwo2fgha2#

// specialized as has_member< T , void > or discarded (sfinae)
template<class T>
struct has_member<T , void_t<decltype(T::member)>> : true_type
{ };

上述特殊化仅在格式良好时才存在,因此当decltype( T::member )有效且无二义性时。has_member<T , void>的特殊化是如此,如注解中的状态。
当您编写has_member<A>时,由于默认模板参数,它是has_member<A, void>
我们对has_member<A, void>进行了专门化(因此继承自true_type),但对has_member<B, void>没有专门化(因此我们使用默认定义:继承自false_type

omhiaaxx

omhiaaxx3#

这个线程和线程SFINAE: Understanding void_t and detect_if救了我。我想通过一些例子来演示这种行为:
工具:cppinsights
按float类型和以下类型测试实现:

struct A {
    using type = int;
};

struct B{
    using type = void;
}

检测人

auto f = has_type_member<float>::value;
auto a = has_type_member<A>::value;
auto b = has_type_member<B>::value;

标准实施

this std::void_t reference

#include <type_traits>

// primary template handles types that have no nested ::type member:
template< class, class = int >
struct has_type_member : std::false_type { };

// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };

输出量

bool f = false;
bool a = true;
bool b = true;

bool x = has_type_member<A, int>::value; //x = false;

案例1

#include <type_traits>

// primary template handles types that have no nested ::type member:
template< class, class = int >
struct has_type_member : std::false_type { };

template< class T >
struct has_type_member<T, void> : std::true_type { };

// specialization recognizes types that do have a nested ::type member:
template< class T >
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };

输出量

/home/insights/insights.cpp:14:8: error: redefinition of 'has_type_member<T, std::void_t<typename T::type>>'
struct has_type_member<T, std::void_t<typename T::type>> : std::true_type { };
       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/insights/insights.cpp:8:8: note: previous definition is here
struct has_type_member<T, void>: std::true_type {};
       ^
1 error generated.
Error while processing /home/insights/insights.cpp.

因此,has_type_member<T, std::void_t<typename T::type>>定义了has_type_member的特殊化,签名正好是has_type_member<T, void>

案例二

#include <type_traits>

template< class, class = void >
struct has_type_member : std::false_type { };

// specialize 2nd type as void
template< class T>
struct has_type_member<T, void> : std::true_type { };

输出:

bool f = true;
bool a = true;
bool b = true;

这个例子说明编译器:
1.想找一个匹配has_type_member<float>
1.发现模板需要2个参数,然后用默认参数填充第2个参数。结构体类似于has_type_member<float, void>
1.找到此签名的特殊化并从std::true_type获取值

案例三

#include <type_traits>

template< class, class = void >
struct has_type_member : std::false_type { };

template<class T>
struct has_type_member<T, typename T::type>: std::true_type {};

输出:

bool f = false;
bool a = false;
bool b = true;

case f

  1. has_type_member<float>完成为has_type_member<float, void>
    1.然后编译器尝试typename float::type失败。
    1.拾取的主模板。
case a
  1. has_type_member<A>已完成为has_type_member<A, void>
    1.然后编译器尝试has_type_member<A, typename A::type>并发现它是has_type_member<A, int>
    1.编译器决定它不是has_type_member<A, void>的规范
    1.然后拾取主模板。
案例B
  1. has_type_member<B>完成为has_type_member<B, void>
    1.然后编译器尝试has_type_member<B, typename B::type>,发现它是has_type_member<B, void>
    1.编译器决定它是has_type_member<B, void>的一个特殊化
  2. true_type已拾取。

案例4

#include <type_traits>

//int as default 2nd argument
template< class, class = int >
struct has_type_member : std::false_type { };

template<class T>
struct has_type_member<T, std::void<typename T::type>>: std::true_type {};

输出:

bool f = false;
bool a = false;
bool b = false;

has_type_member<T>对于所有3个变量都是has_type_member<T, int>类型,而true_type的签名是has_type_member<T, void>,如果它是有效的。

总结

std::void_t
1.检查T::type是否有效。
1.如果只提供一个模板参数,则提供主模板的专门化。

相关问题