编译器对C++ 11变参模板(Variadic Template)的函数包扩展实现的差异

编译器对C++ 11变参模板(Variadic Template)的函数包扩展实现的差异

题目挺绕口的。C++ 11的好东西不算太多,但变参模板(Variadic Template)肯定是其中耀眼的一颗明星,在C++设计新思维中,你可以看到很多模版的代码为了支持不确定的参数个数,而要重载1个参数到N个模板参数的N个函数。虽然种代码一般也是用会用宏和脚步辅助生成。但我想也没有人愿意看到几千行这种单调的函数。通过这个东东,模板的威力可以爆发。

目前的最新的编译器基本都已经支持Variadic Template了,GCC 4.6和Visual studio 2013都已经支持变参模板。但今天我在写一个Lua粘结层的时候发现了一个有意思的问题。

先上代码。

 1 #include <iostream>
 2 
 3 
 4 template <typename ... T>
 5 void dummy_wrapper(T... t)
 6 {
 7 };
 8 
 9 template <class T>
10 T unpacker(const T t)
11 {
12     std::cout << '[' << t << ']';
13     return t;
14 }
15 
16 
17 template <typename... Args>
18 void write_line(const Args& ... data)
19 {
20     dummy_wrapper(unpacker(data)...);
21     std::cout << '\n';
22 }
23 
24 int main()
25 {
26     write_line(1, "--", "2.2.2", "--", 3.0);
27     return 0;
28 }

稍作解释,write_line是一个接受变参的模版函数,其内部调用dummy_wrapper,这个函数帮主辅助进行包扩展,让每个参数都调用unpacker(data)函数。那这个代码这个代码的输出预计应该是什么?

我想大部分人会认为是 [1][--][2.2.2][--][3],但其实在Visual C++ 2013和GCC 4.8上的输出都正好相反。输出的是[3][--][2.2.2][--][1]。是不是有点搞?

我觉得这个问题的原因是因为C++的参数入栈顺序是从右到左,1这个参数被计算处理后,被先扔到栈里面。反而变成了最右的参数。所以导致了这个问题。

其实坦白讲这种问题就是典型的规范没有写明的问题。其实我认为从结果和语义正确性上讲,[1][--][2.2.2][--][3]的输出肯定更加正确一些,但这个对于编译器,就要等若干结果计算出来了,再入栈。

而这个问题在IBM的编译器团队的写的《深入理解C++11》上也有一节提过,IBM的编译器XL编译器上类似的测试输出结果是顺序的,他们也测试了GCC,他们认为GCC输出是混乱的(其实是倒序的)。

如何规避这个问题呢?有个法子是把参数反着传递给write_line函数,但这种方法不具备可移植性(谁知道这个bug会哪天修复,另外,你输出东西的时候能倒着想问题?)。

其实可以考虑使用递归的包扩展方式,

 1 #include <iostream>
 2 
 3 template <typename ... Tlist>
 4 void dummy_wrapper(Tlist... tlst)
 5 {
 6 };
 7 
 8 template <typename T, typename ... Tlist>
 9 void dummy_wrapper(T t, Tlist... tlst)
10 {
11     unpacker(t);
12     dummy_wrapper(tlst...);
13 };
14 
15 template <class T>
16 T unpacker(const T t)
17 {
18     std::cout << '[' << t << ']';
19     return t;
20 }
21 
22 template <typename... Args>
23 void write_line(const Args& ... data)
24 {
25     dummy_wrapper(data...);
26     std::cout << '\n';
27 }
28 
29 int main()
30 {
31     write_line(1, "--", "2.2.2", "--", 3.0);
32     return 0;
33 }

这样改写后,输出变成[1][--][2.2.2][--][3]、

这种方式参数是一个个展开的,所以避免了上述的问题。大部分情况可以这样改写规避一部分问题。但当然这样也不能解决所有问题,

我的代码里面中间有一段是这样的,目的是为了将一个函数注册给lua,

 1 template<typename ret_type, 
 2     typename ...args_type>
 3 class g_functor_ret
 4 {
 5 public:
 6     static int invoke(lua_State *state)
 7     {
 8         void *upvalue_1 = lua_touserdata(state, lua_upvalueindex(1));
 9         ret_type(*fun_ptr)(args_type...) = (ret_type( *)(args_type...))(upvalue_1);
10         int para_idx = 0;
11         push_stack<ret_type>(state, fun_ptr(read_stack<args_type>(state, para_idx++)...));
12     }
13 };

fun_ptr(read_stack<args_type>(state, para_idx--)...) 这段代码,由于fun_ptr是一个函数指针,我不可能为所有的函数指针都去写一个递归方法,所以上面的方法无效。

怎么办呢?只能先把参数反向传入,后面等编译器有更新再慢慢改,也只有这个很挫的方法先应付。目前我能测试到的VS2013和GCC 4.8目前还都有问题。

【本文作者是雁渡寒潭,本着自由的精神,你可以在无盈利的情况完整转载此文档,转载时请附上BLOG链接:http://www.cnblogs.com/fullsail/,否则每字一元,每图一百不讲价。对Baidu文库和360doc加价一倍】

posted @ 2014-07-27 23:22  fullsail  阅读(1284)  评论(0编辑  收藏