`void_t`是如何工作的?

时间:2014-12-29 10:47:15

标签: c++ templates c++14 sfinae

我观看了Walter Brown在Cppcon14上关于现代模板编程(Part IPart II)的演讲,他提出了void_t SFINAE技术。

例:
给定一个简单的变量模板,如果所有模板参数都格式正确,则该模板的计算结果为void

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

以及检查是否存在名为 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 >所以此模板已弃用
  • 编译器查找has_member< B , class = void >,其中void为默认参数
  • has_member< B >评估为false_type

http://ideone.com/HCTlBb

问题:
1.我对此的理解是否正确?
2. Walter Brown声明默认参数必须与void_t中使用的默认参数完全相同才能使其工作。这是为什么? (我不明白为什么这种类型需要匹配,不是任何默认类型都可以完成这项工作吗?)

2 个答案:

答案 0 :(得分:114)

编写has_member<A>::value时,编译器会查找名称has_member并找到主要类模板,即此声明:

template< class , class = void >
struct has_member;

(在OP中,这是作为定义写的。)

将模板参数列表<A>与此主模板的模板参数列表进行比较。由于主模板有两个参数,但您只提供了一个参数,因此其余参数默认为默认模板参数:void。就像你写了has_member<A, void>::value一样。

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

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

编译器尝试将模板参数A, void与部分特化中定义的模式匹配:Tvoid_t<..>逐个匹配。首先,执行模板参数推导。上面的部分特化仍然是一个模板参数的模板,需要填充&#34;#34;通过参数。

第一个模式T允许编译器推导出模板参数T。这是一个微不足道的推论,但考虑像T const&这样的模式,我们仍然可以推导T。对于模式T和模板参数A,我们将T推导为A

在第二种模式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
{ };

现在,我们可以将此专业化的模板参数列表与提供给原始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>。参数与特化的参数不匹配,主模板被选为后退。


(*)标准,恕我直言,令人困惑,包括替换过程和模板参数推导过程中明确指定的模板参数的匹配。例如(后N4296)[temp.class.spec.match] / 2:

  

部分特化匹配给定的实际模板参数列表   如果可以推导出部分特化的模板参数   来自实际的模板参数列表。

但这不是只是意味着必须推导出部分特化的所有模板参数;它还意味着替换必须成功并且(看起来如此?)模板参数必须匹配部分特化的(替换)模板参数。请注意,我并不完全了解其中标准指定了替换参数列表与提供的参数列表之间的比较。

答案 1 :(得分:17)

// 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>的专业化(所以我们使用默认定义:inherit from {{ 1}})