CppCon - Modern Template Metaprogramming 杂记
2014年底才看到github和channel9上有CppCon2014的视频和资料,顿时激动不已。最近小生也一直在研习CppCon2014中令人兴奋的内容。这篇鄙文就是小生学习了《Modern Template Metaprogramming》之后,有对Unevaluated Operands更深刻的理解,有感而写。
C++98标准中的Unevaluated Operands,只有sizeof操作符。C++11又引入了decltype,typeid和noexcept。Unevaluated Operands不会有求值的计算,即使是在编译期间。这意味着Unevaluated Operands中的表达式甚至不会生成具体的C++代码,操作符中的表达式仅需要声明而无需定义。在模板元编程中,我们有时候经常仅需要对catch a glimpse of一个表达式,而这些操作符都是很好的工具。
下面我们以std::is_copy_assignable为例来看看Unevaluated Operands的威力。
namespace cpp11 { template <typename T> struct is_copy_assignable { private: template <typename U, typename = decltype(std::declval<T&>() = std::declval<T const&>())> static std::true_type try_assign(U&&); static std::false_type try_assign(...); public: using type = decltype(try_assign(std::declval<T>())); }; }
我们以c++11的版本开始。这个std::is_copy_assignable的实现原理就是SFINAE。给定一个C++类型T,如果T存在一个有效的operator=,那么成员模板函数try_assign的第二个模板参数中decltype里面的表达式将是well formed,decltype将会推导出这个表达式的类型。而第二个非模板的普通成员函数try_assign的形参使用" ..." 是因为这种情况是最差匹配的情况,is_copy_assignable需要优先匹配成员模板函数try_assign。元函数返回type时,再次使用了decltype,这里是真正的点睛之笔。由于decltype无需求值,因此try_assign无需一个函数体。std::declval是C++11引入universal reference的一个附属品,它也是一个模板函数同样没有函数体,返回值的类型是一个T&&(universal reference),try_assign中使用std::declval可以让编译器知道我们传递给这个函数的参数类型,但没有真正的去构造一个对象,就好像是假设我传递一个实参给try_assign. 在Unevaluated Operands中的表达式如果是一个函数调用,那么std::declval是一个非常好的工具,姑且可以将它当做一个惯用法。那么如果这个T不存在一个有效的operator=,编译器将使用SFINAE选择普通的成员函数。最后元函数返回的type在T::operator= 存在的情况下是std::true_type,反之则是false_type.
在c++98的年代,Unevaluated Operands只有sizeof,那么怎么实现呢。同使用下面这个例子或者类似的实现方法。
namespace cpp98 { namespace detail { template <typename T> T declval(); } template <typename T> struct is_copy_assignable { private: typedef char One; typedef struct { char a[2]; } Two; template <int N = sizeof(detail::declval<T&>() = detail::declval<T const&>())> static One try_assign(int); static Two try_assign(...); public: typedef typename std::conditional<sizeof(try_assign(0)) == sizeof(One), std::true_type, std::false_type>::type type; }; }
sizeof只能推导出类型的大小,因此try_assign不能再返回std::true_type和st::false_type,而是返回了两个大小不同的类型。实现原理依旧是SFINAE,而元函数返回的类型是根据try_assign返回类型的大小来决定导出std::true_type还是std::false_type.由于cpp98没有右值引用,所以这里自己实现了一个declval的替代版。在c++98的版本中,元函数的类型计算都是由sizeof来驱动的。c++11引入了更多的非求值操作符后,元编程确实方便了不少。
在《Modern Template Metaprogramming》这篇演讲中还有一个非常有趣的模板工具,就是void_t. 它的实现可能如下代码所示
template <typename ... Args> struct make_void { typedef void type; }; template <typename ... Args> using void_t = typename make_void<Args...>::type;
或者,
template <typename ... Args> using void_t = void;
让我们先来看看使用void_t来实现我们的is_copy_assignable.
namespace cpp1y { template <typename ... Args> struct make_void { typedef void type; }; template <typename ... Args> using void_t = typename make_void<Args...>::type; template <typename T, typename = void> struct is_copy_assignable : std::false_type {}; template <typename T> struct is_copy_assignable<T, void_t<decltype(std::declval<T&>() = std::declval<T const&>())>> : std::true_type { }; }
利用void_t实现的版本又要比使用SFINAE的版本实现简单许多。给定一个C++类型T,传递到元函数is_copy_assignable中,由于这个版本的元函数有两个参数,那么is_copy_assignable<T>被推导为is_copy_assignable<T,void>. 接下来,编译要检查是否有more specialized的特化版本。如果T有一个有效的operator=,那么is_copy_assignable的特化版本中的第二个参数中的decltype推导的表达式是well formed,那么decltype计算类型并与void_t共同推导出void. 由于特化版本是more specialized,因此编译器选择这个特化版本,并调用元函数std::true_type。反之,如果T没有一个有效的operator=,那么decltype推导的表达式无效,因此这种情况下的is_copy_assignable没有特化版本并调用元函数std::false_type。这里要注意的是,第二个默认的模板参数必须要以void为缺省的实参类型,因为void_t实际上就是void,才能让编译器找到特化的版本。换而言之,缺省的类型要与void_t一致,如果实现一个类似的int_t,那么这个缺省的类型应该是int.
小生在MSVC12实验的void_t版本一直有问题,还有更简单版本的void_t实现在MSVC和gcc的编译器上都不对,根据这篇演讲的说法是还需要一个提案,详见CWG 1558, treatment of unused arguments in an alias template specialization. 错误都一样,无论T是否有可用的operator=,这个元函数始终选择特化版本,即都是true_type,可能是因为void_t模板没有使用任何模板参数的原因吧,还需要进一步的研究。
这是最后的实验代码和运行结果如下:
http://ideone.com/ZUCvhY
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。