最近看到一段代码,感觉非常trick,但也非常有意思,写出来记录一下。
背景是这样的,有一个模板函数 copy_assign ,其作用非常简单,就是将第二参数“拷贝”给第一个参数,但是为了对能够进行深拷贝的类型进行深拷贝,希望的行为是这样的:
如果T有成员函数int assign(const T &),则调用dest.assign(src),并以assign函数的返回值作为返回值;
如果T没有成员函数int assign(const T &),则调用dest=src,并返回0。
函数的原型如下:
1 2 |
template <typename T> inline int copy_assign(T &dest, const T &src); |
并且为了降低运行时开销,我们希望这一切是在编译期确定的,所以我们需要在编译期就能够确定类型T是否有assign成员函数,并且根据结果指定对应的行为。
如何判断一个类有没有特定成员函数?
首先要解决的第一个问题是:在模板类的代码部分,我们并不知道类型T是否有我们想要的成员函数,据我所知C++也没有提供这样的机制来判断,那该怎么解决这个问题呢?
我们必须要在编译期利用C++的一些机制让编译器在不报错退出的情况下完成我们的目的,下面定义的模板类就是 __has_assign__ 用来做这件事情的:
1 2 3 4 5 6 7 8 9 10 11 12 |
template <typename T> struct __has_assign__ { typedef int (T::*Sign)(const T &); typedef char yes[1]; typedef char no[2]; template <typename U, U> struct type_check; template <typename _1> static yes &chk(type_check<Sign, &_1::assign> *); template <typename> static no &chk(...); static bool const value = sizeof(chk<T>(0)) == sizeof(yes); }; |
代码着实有些trick,需要细细品味。这个模板类针对类型参数T的实例化的静态变量value的值,就代表了类型T中是否有我们想要的assign函数。
代码的关键步骤在函数的最后三行,声明了两个模板函数chk,而第二个函数是总是可以匹配上T的,这就利用了C++模板匹配中的一点规则:当有多个可行的匹配时,编译器总会选择更“紧”的匹配(更特例化)。那么什么情况下第一个函数声明会是一个匹配呢?答案在于type_check这个模板类,它接受两个相同的类型参数,而传入的第一个是我们想要的assign函数的类型,第二个参数是模板类型参数的成员函数assign(如果有的话),也就是说,如果传入的类型T是一个具有成员函数 int assign(const T &) 的类型,则第一个chk会成为一个更“紧”的匹配被编译器选中,这样静态变量value就会被确定为true,目标达成。
如何判断变量是不是类的实例?
上面这个函数只能对自定义类型使用,那C++的基本类型怎么办呢?我们当然也需要支持基本类型的拷贝。很简单:
1 2 3 4 5 |
template <typename T> struct __has_assign__ { static bool const value = false; }; |
直接把value设为false就可以了。
如何用一个函数同时适用于类和基本类型?
但上面说的这两个模板类明显是冲突的,不能同时使用,那怎么办呢?
我们可以借助C++模板的偏特例化机制,把这两个模板类通过一个bool类型的参数区分开,形成同一个模板类的两种特例化形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
template<bool, typename T> struct __has_assign__; template <typename T> struct __has_assign__<true, T> { typedef int (T::*Sign)(const T &); typedef char yes[1]; typedef char no[2]; template <typename U, U> struct type_check; template <typename _1> static yes &chk(type_check<Sign, &_1::assign> *); template <typename> static no &chk(...); static bool const value = sizeof(chk<T>(0)) == sizeof(yes); }; template <typename T> struct __has_assign__<false, T> { static bool const value = false; }; |
这个模板类怎么用呢?对于一个类型 someClass 来说(可以是基础类型),我们可以通过 __has_assign__<__is_class(someClass), someClass>::value 来判断它有没有我们想要的assign函数。 __is_class 是gcc提供的编译期类型判断原语中的一个,见这里。
实现copy_assign
有了这个神奇的类,我们终于可以实现文章开头提到的 copy_assign 函数了。可是有一个问题,上面代码最后得到了一个表示有没有assign成员函数的bool类型,但是编译期是没有办法使用bool类型的变量值来改变行为的啊?
既然变量不行,那就用类型。
再声明一个模板类,用来把bool类型的变量转变为类型:
1 2 3 4 5 6 7 |
template <bool c> struct BoolType { static const bool value = c; }; typedef BoolType<false> FalseType; typedef BoolType<true> TrueType; |
该模板类只有一个bool类型的非类型模板参数c,用这个模板类,我们可以把一个bool值转化成对应的类型了!有了不同的类型,再结合C++的重载机制,我们就可以在编译期完成这样的工作了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
template <typename T> inline int copy_assign_wrap(T &dest, const T &src, TrueType c) { return dest.assign(src); } template <typename T> inline int copy_assign_wrap(T &dest, const T &src, FalseType c) { dest = src; return 0; } // 此函数用于拷贝赋值 // - 如果T有成员函数int assign(const T &),则调用dest.assign(src), // 并以assign函数的返回值作为返回值; // - 如果T没有成员函数int assign(const T &),则调用dest=src, // 并返回0。 template <typename T> inline int copy_assign(T &dest, const T &src) { return copy_assign_wrap(dest, src, BoolType<__has_assign__<__is_class(T), T>::value>()); } |
针对最后一个参数的类型不同,编译器会在两个重载的模板函数中选择合适的函数实例化,最后达到了我们的目的。
感慨一下
我觉得能写出这样C++代码的人,一定是对C++有着非常深入的了解,真的可以说是精通C++了。而我等小菜,连读起来都费劲,只能望其项背啊。
据说C++的元编程是完备的,真的是太神奇了!但另一方面,C++也真的是太难学了!