引子
前些时候,我在群里出了一道题目:将变参的类型连接在一起作为字符串并返回出来,要求只用函数实现,不能借助于结构体实现。用结构体来实现比较简单:
template<typename... Args> struct Connect; template< typename First, typename... Rest> struct Connect<First, Rest...> { static string GetName() { return typeid(First).name() + string(" ") + Connect<Rest...>::GetName(); } }; template<typename Last> struct Connect<Last> { static string GetName() { return typeid(Last).name(); } };
测试代码:
auto str = Connect<int, double>::GetName(); //str为int double
如果改成函数的话,试着这样写:
template<typename T> string GetNameofTypes() { return typeid(T).name(); } template<typename T, typename... Rest> string GetNameofTypes() { string str += GetNameofTypes<T>() + " " + GetNameofTypes<Rest...>(); return str; }
很遗憾,这样是编译不过的,因为编译器不知道选择哪个。有两个方法可以解决这个问题,这里主要来介绍通过逗号表达式来解决这个问题。
逗号表达式和变参的相逢
前段时间播放的舌尖上的中国第二季中有一集为相逢,当南北不同风味的普通食材放到一起时,立刻化腐朽为神奇变成难得的美味了,这正是食材相逢组合而成的天作之合。那么在c++中,不同的特性相逢在一起又是什么滋味呢?一定很奇妙。下面来看看舌尖上的c++中两个普通食材吧。
食材一:逗号表达式
我们知道逗号表达式会按顺序执行逗号前面的表达式,比如:
d = (a = b, c);
这个表达式会按顺序执行的:a先会被赋值b,接着d会被赋值c。
食材二:可变参数模板
c++11的可变参数模板增强了模板功能,在c++11之前,类模板和函数模板只能含有固定数量的模板参数,现在c++11中的新特性可变参数模板允许模板定义中包含0到任意个模板参数。可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,可变参数模板声明时需要在typename或class后面带上省略号“...”。
省略号(...)的作用有两个
- 声明一个参数包,这个参数包中可以包含0到任意个模板参数;
- 在模板定义的右边,可以将参数包展开成一个一个独立的参数。
逗号表达式和变参的相逢
来看看逗号表达式和变参的相逢会产生什么奇妙的效果。
template <class T> void printarg(T t) { cout << t << endl; } template <class ...Args> void expand(Args... args) { int arr[] = {(printarg(args), 0)...};
cout<<sizeof(arr)<<endl; }
测试代码:
expand(1,2,3,4); //将输出1 2 3 4
上面的例子将分别打印出1,2,3,4,将可变参数模板就地展开了。是的,是通过一个没有引用的数组来就地展开的,看到这种写法不要惊讶,这就是它们相逢产生的神奇效果,独有的味道。
我们来看看这种奇妙的效果是如何产生的:
我们知道逗号表达式会按顺序执行逗号前面的表达式,比如:
d = (a = b, c);
这个表达式会按顺序执行的:a先会被赋值b,接着d会被赋值c。
expand函数中的逗号表达式:(printarg(args),
0),也是按照这个执行顺序,是先执行printarg(args),再得到逗号表达式的结果0。同时还用到了c++11的另外一个特性:初始化列表,通过初始化列表来初始化一个变长数组,
{(printarg(args), 0)...}将会展开成((printarg(arg1),0),
(printarg(arg2),0),printarg(arg3),0), etc... );最终会创建一个元素值都为0的数组int
arr[sizeof...(Args)],由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
再回过头来说说本文开头提到的那个问题,通过函数将变参类型作为字符串返回出来。通过逗号表达式可以很轻松完成这个任务:
template<typename T> string GetName() { return typeid(T).name(); } template<typename... Rest> string GetNameofTypes() { string str; int arr[] = { (str +=" "+ GetName<Rest>(), 0)... };
cout<<sizeof(arr)<<endl; return str; } //测试代码: auto s = GetNameofTypes<int, double, char>(); //s将为 int double char
尝了上面的逗号表达式和可变参数模板的相逢产生奇妙的味道,一定还在回味之中,意犹未尽吧。我还没说其实还有一样食材没说呢,如果将逗号表达式和这种食材相逢在一起便又是另外一种独特的味道了。再来看看另外一种食材吧。
食材三:decltype表达式类型推断
C++11新增了decltype关键字,用来在编译时推导出一个表达式的类型。它的语法格式如下:
decltype(exp)
其中exp表示一个表达式(expression)。
它可以用来推导表达式标示符和表达式的类型,比如:
const int bar(); int i; struct A { double x; }; const A* a = new A(); decltype(bar()) x2; // 类型为const int decltype(i) x3; // 类型为int decltype(a->x) x4; // 类型为double decltype((a->x)) x5; // 类型为const double&
我们一般用decltype来推断函数的返回类型,和auto结合起来,组成一种返回值类型后置的语法,比如下面的例子:
template <typename T, typename U> auto add(T t, U u) -> decltype(t + u) { return t + u; }
为什么要返回类型后置呢?因为返回类型要依赖于模板参数,如果将decltype(t + u)放到函数的前面则无法编译通过,因为在定义函数返回值的时候,模板参数变量都还不存在呢。所以就借助auto来将返回类型占位住,等decltype推导之后再给auto初始化,从而获取了函数的返回值。
逗号表达式和decltype的相逢
来看看逗号表达式和decltype的相逢又会产生什么奇妙的效果。比如有这样一个需求,我需要在编译期判断某个类是否存在void Reserve(int i)函数。
template<typename T> struct Has_Reserve_Method { private: typedef std::true_type True; typedef std::false_type False; template<typename U> static auto Check(int) -> decltype(std::declval<U>().Reserve (0), True()); template<typename> static False Check(...); public: enum { value = std::is_same<decltype(Check<T>(0)), True>::value }; }; struct AA { void Reserve(int i) { cout << "ok" << endl; } }; 测试代码: if (Has_Reserve_Method<AA>::value) cout << "ok" << endl; //将输出OK
这里利用了SFINAE特性,它的全称是Substitution failure is not an error,匹配失败并不是错误,意思是用函数模板匹配规则来判断类型的某个属性是否存在,也就是说SFINAE可以作为一种编译期的不完整内省方法。
template<typename U> static auto Check(int) -> decltype(std::declval<U>().Reserve(0), True()); template<typename> static False Check(...);
这两行代码是关键,当Check匹配不上时,返回False;当匹配上之后,通过逗号表达式返回True,这样外面就可以根据True和False来检查是否存在该函数了。
怎么样这道菜的味道也相当好吧。其实还有很多c++特性的相逢产生的奇妙味道我们还没有发现,正等待着我们去发现呢。
后记
由于typeid可能会丢失一些类型信息,要获取准确的类型名称还需要做一些专门的处理,这里为了简单起见,忽略了typeid获取类型名可能不准确的影响。另外,有童孩说逗号表达式来展开变参的代码可读性很差,这里我也不推荐大家在实际的代码中也这样写,还是老老实实的用结构体来展开变参吧,其实通过函数来展开变参(不用逗号表达式)还有种写法:
string GetNameofMsgType() { return ""; } template <typename First> string GetNameofMsgType() { return std::string(typeid(First).name()); } template <typename First, typename Second, typename... Args> string GetNameofMsgType() { return GetNameofMsgType<First>() + " " + GetNameofMsgType<Second, Args...>(); }
如果你觉得这篇文章对你有用,可以点一下推荐,谢谢。
c++11 boost技术交流群:296561497,欢迎大家来交流技术。