为什么void_t需要检查成员类型的存在?

m.s*_*.s. 3 c++ templates c++11

当阅读巴里的答案,以检查是否给定类型有一个内部模板重新绑定,心想:

为什么我们需要void_t呢?

为什么下面工作?

#include <iostream>

template <typename X, typename Y = X>
struct has_rebind : std::false_type {};

template <typename X>
struct has_rebind<X, typename X::template rebind<int>> : std::true_type {};

struct A { };
struct B { template <typename > struct rebind { }; };

int main() {
    std::cout << has_rebind<A>::value << std::endl;
    std::cout << has_rebind<B>::value << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

产量

0
0
Run Code Online (Sandbox Code Playgroud)

demo

Yak*_*ont 8

void_t 是一个黑客.

模板类专业化模式匹配的工作原理是主模板确定每个参数的种类(类型或值),默认值为:

template<class A, class B=A, class C=void>
struct whatever {};
Run Code Online (Sandbox Code Playgroud)

要调用whatever<?...>,必须将有效参数与主要特化项匹配whatever<?...>.

在该测试通过之后,各种专业化模式匹配.所以假设你想要一个指针专门化.

template<class T>
struct whatever<T*, T*, void> {
Run Code Online (Sandbox Code Playgroud)

template<?...>这里的参数列表不匹配:它只提供"自由变量"列表.模式匹配位于类名<?...>后面的模板参数列表中whatever.每个参数依次与发送的参数whatever匹配,并从该模式匹配"自由变量"(class T上图).

诀窍是你可以在这里放置不模式匹配的表达式,但依赖于其他模式匹配,然后生成一个新类型:

template<class T>
struct whatever<T*, typename std::add_const<T>::type*, void> {
Run Code Online (Sandbox Code Playgroud)

第二个参数是(模板的add_const)依赖类型,因此不能进行模式匹配.通常,结果some_template<T>::type可以是图灵完全非内射计算:C++标准不需要反转,幸运的是编译器编写者.

编译器不会尝试 - 而是尝试T从其他模板参数中确定,然后替换T为该参数,并检查它是否与传递给的类型匹配whatever.

这里的技巧是替换失败(在直接上下文中)不会产生错误1.如果std::add_const<T>没有调用字段::type,而不是生成错误,它会改为说"好吧,这个模式不匹配"​​.它会将这种专业化视为一组候选人中的一个可行的专业.

这个功能在C++的早期就被添加了,因为模板函数甚至可以贪婪地匹配基本类型,并且iterator::value_type如果你传递了intas ,尝试使用派生类型(例如)会失败iterator.使用此功能,int没有::value_type意味着"这不是某些模板函数的有效重载",而不是重载解析期间的语法错误.实际上,如果没有任何超载,模板函数几乎没用它,所以他们添加了SFINAE - 替换失败不是错误.

一旦它出现,人们开始滥用它. void_t试图利用它.

template<class T>
struct whatever<T*, T*, void_t<decltype(std::declval<T>().hello_world())>> {
Run Code Online (Sandbox Code Playgroud)

void_t获取它的类型参数,并丢弃它们,并void 在依赖的上下文中生成(从而阻止表达式被用于推断T).它以首先计算类型参数2的方式这样做,这意味着如果生成的类型在直接上下文中导致失败,则会出现替换失败.

一点是,一旦你喂它,所产生的类型无关紧要void_t.t.hello_world()当我们定义主要特化时,我们甚至可能不知道应该是什么类型whatever<?...>.所以我们丢弃它并将其转化为void.为了匹配特化,类型必须匹配.因此我们将类型设置为void主要特化,并在特殊化中进行测试,然后将其传递void_t给扔掉类型结果.

我们也可以使用std::enable_if_t<?>一个编译时bool表达式,void当且仅当它bool为真时才产生类型(否则在直接上下文中失败).

从某种意义上说,void_t将有效类型表达式映射void到替换失败的无效类型表达式. enable_if_t映射truevoid,并false以取代的失败.在这两种情况下,您都需要void专门化中的类型来"使用"结果并正确匹配.

两者在SFINAE代码中都非常有用,这是一个强大的功能,可让您根据简单模式匹配之外的其他内容来决定使用哪种特化.


1最后我查了一下,这个要求没有明确在标准中!SFINAE规则适用于模板函数替换失败,并且每个人(包括编译器编写者和标准编写者)只是假设它应用于模板类替换失败.当然,标准很难读,我可能只是错过了它.

2在某一点上,各种编译器都不同意template<class...>using void_t=void;应该做的事情.如果传递的表达式是替换失败,则有些会失败:其他人会注意到表达式将被丢弃,并在检查替换失败之前将其丢弃.这在缺陷报告中得到澄清.


Can*_*nos 6

在这些方面:

template <typename X, typename Y = X>
struct has_rebind : std::false_type {};

template <typename X>
struct has_rebind<X, typename X::template rebind<int>> : std::true_type {};
Run Code Online (Sandbox Code Playgroud)

默认类型(X)和您为specialization(typename X::template rebind<int>)提供的类型必须是相同的类型.由于void是一个非常好的"默认虚拟类型",它通常用作默认类型,我们void_t用来改变专业化中给出的任何内容

  • 我错过了关于"同类型"的事实,感谢你指出这一点. (2认同)