std::tuple不支持输入输出,就想实现一下。最初想用模板函数实现,无奈总是不行,就改用模板类了,下面是实现代码。
1 #include <iostream> 2 #include <tuple> 3 using namespace std; 4 5 template <class Tuple, size_t N> 6 struct print_imp 7 { 8 static void print(ostream& out, const Tuple& t) 9 { 10 print_imp<Tuple, N-1>::print(out, t); 11 out << ", " << get<N-1>(t); 12 } 13 }; 14 template <class Tuple> 15 struct print_imp<Tuple, 1> 16 { 17 static void print(ostream& out, const Tuple& t) 18 { 19 out << get<0>(t); 20 } 21 }; 22 template <class... Args> 23 ostream& operator<<(ostream& out, const tuple<Args...>& t) 24 { 25 out << '('; 26 print_imp<decltype(t), sizeof...(Args)>::print(out, t); 27 return out << ')'; 28 } 29 int main() 30 { 31 auto t = make_tuple(12,4.5,3,6.7,"efa"s); 32 cout << t << endl; 33 }
用模板函数失败的原因是,std::get<INDEX>()里的INDEX必须是编译期常量,而我没有办法通过函数的参数传递一个编译期常量,只好在模板类的模板参数里传递。后来受樱子妹妹启发,不能传常量,就传类型好了。下面的代码使用了一个空类把常量封装成类型:
1 template <size_t> struct uint{}; 2 3 template <class Tuple, size_t N> 4 void print(ostream& out, const T &t, uint<N>) 5 { 6 print(out,t, uint<N-1>()); 7 out << ", " << get<N-1>(t) ; 8 } 9 10 template <class Tuple> 11 void print(ostream& out, const T &t, uint<1>) 12 { 13 out << get<0>(t); 14 } 15 16 template <class... Args> 17 ostream& operator<<(ostream& out, const tuple<Args...> &t) 18 { 19 out << '('; 20 print(out,t, uint<sizeof...(Args)>()); 21 return out << ')'; 22 }
不得不说,这份实现清爽了好多,再次感谢樱子妹妹。
顺便多说一句,关于把数值封装成类,C++标准里也有这样的功能,就是模板类integral_constant,其一个可能实现如下:
template<class T, T v> struct integral_constant { static constexpr T value = v; typedef T value_type; typedef integral_constant type; constexpr operator value_type() const { return value; } };
其模板参数有两个,第一个是类型,第二个是值,也就是它可以封装各种整型类型的常量。接口为:一个静态成员数据,两个type,一个类型转换符。
如果使用integral_constant代替手写类的话,就可以这样用:
template <size_t N> using uint = integral_constant<size_t, N>;
最初我也想用ratio来封装常量,但是失败了,因为ratio的模板参数为signed类型,与get<>()不符。
如果就这样结束了,未免显得图森破。可能有同学在想,能不能采用非递归的方式实现呢?普通的方式是不行的。我反反复复强调了,get<>()使用的是编译期常量,无论是基于变量的for循环还是基于迭代器的for循环都是不能用的。但是还真有大牛实现了,方式是传递一堆常量,再用特殊方式遍历。以下实现出自http://stackoverflow.com/a/6245777/273767。
1 template<class Ch, class Tr, class Tuple, std::size_t... Is> 2 void print_tuple_impl(std::basic_ostream<Ch,Tr>& os, 3 const Tuple & t, 4 std::index_sequence<Is...>) 5 { 6 using swallow = int[]; // guaranties left to right order 7 (void)swallow{0, (void(os << (Is == 0? "" : ", ") << std::get<Is>(t)), 0)...}; 8 } 9 10 template<class Ch, class Tr, class... Args> 11 decltype(auto) operator<<(std::basic_ostream<Ch, Tr>& os, 12 const std::tuple<Args...>& t) 13 { 14 os << "("; 15 print_tuple_impl(os, t, std::index_sequence_for<Args...>{}); 16 return os << ")"; 17 }
我看到这份代码第一反应是:这他喵是啥?!(╯°Д°)╯︵ ┻━┻ 。一步步分解吧。
先看operator<<。std::ostream就是std::basic_ostream<char>,这份代码更加泛化而已。函数前面的decltype(auto)表示自动推导返回类型,这是C++14的新特性。这样operator<<里就剩下一个对print_tuple_impl 的调用了,里面使用了一个工厂元函数 index_sequence_for<>,这又是C++14的新特性,属于integer_sequence知识点,下面细细道来:
template< class T, T... Ints > class integer_sequence {}; template<std::size_t... Ints> using index_sequence = std::integer_sequence<std::size_t, Ints...>; template<class T, T N> using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >; template<std::size_t N> using make_index_sequence = std::make_integer_sequence<std::size_t, N>; template<class... T> using index_sequence_for = std::make_index_sequence<sizeof...(T)>;
integer_sequence是对integral_constant的扩展,一次封装一个序列的整数常量。index_sequence就是类型限定为size_t的integer_sequence。std提供了三个工厂元函数,make_integer_sequence是创建序列为0, 1, 2, ..., N-1的工厂元函数,make_index_sequence同理。index_sequence_for是根据参数包的数量来调用make_ndex_sequence。那么,代码第15行的std::index_sequence_for<Args...>就是,以传入tuple的size创建一个index_sequence,从0到sizeof...(Args)-1,以供get<>()使用。对了,元函数返回的是一个类型,所以后面要加{}来创建实例。{}是初始化列表,功能同()。
接下来再来看看print_tuple_impl。第一行创建了一个类型swallow,就是int[]的别名。第二行就让人发狂了,但也不是完全看不懂。首先,这个(void)还是能看懂的。如果你定义了一个变量而未使用,编译器会给出一个warning,这时候把变量void一下就不会警告了,像这样:int a; (void)a;
所以(void)swallow{0, 什么鬼}; 就等价于 int arr[] = {0,什么鬼}; (void)arr;
所以“什么鬼”实际是整数,也就是(void(表达式), 0)... 是整数。可是(XX,**)这种格式从来没见过啊。做个实验,把这段代码cout << typeid("ewf",1.2) << endl;编译一下,gcc给出的诊断是: warning: left operand of comma operator has no effect [-Wunused-value]。就是说,逗号操作符左边的运算数没有效果,是个unused value(所以“表达式”需要void一下)。这时候我才想想来逗号确实也是一个运算符,意义是按顺序执行,先左后右。所以(void(表达式), 0)其实就是0。卧槽槽槽槽槽槽……这意味着什么?所有的C++语句都可以转化为一个0!全都是0!我感觉我要控制不住体内的洪荒之力了(╯°Д°)╯︵ ┻━┻
下面是最后一个知识点了,参数包展开。(void(表达式), 0)是一个0,(void(表达式), 0)...是一堆0,“...”就是将参数包展开。先看一下参数包展开在C++11标准里的定义:
14.5.3.4
A pack expansion is a sequence of tokens that names one or more parameter packs, followed by an ellipsis.
The sequence of tokens is called the pattern of the expansion; its syntax depends on the context in which
the expansion occurs. Pack expansions can occur in the following contexts:
— In an initializer-list (8.5); the pattern is an initializer-clause.
— In a base-specifier-list (Clause 10); the pattern is a base-specifier.
— In a mem-initializer-list (12.6.2); the pattern is a mem-initializer.
— In a template-argument-list (14.3); the pattern is a template-argument.
— In a dynamic-exception-specification (15.4); the pattern is a type-id.
— In an attribute-list (7.6.1); the pattern is an attribute.
— In an alignment-specifier (7.6.2); the pattern is the alignment-specifier without the ellipsis.
— In a capture-list (5.1.2); the pattern is a capture.
大概翻译一下:参数包展开式是一个包含参数包的标记序列后面跟着省略号。标记序列就是展开式的范式,其语法根据展开式出现的环境而定。参数包展开式可以出现在以下环境中:……
这里用到的就是第一个环境,初始化列表。代码展开了一个复杂的数值0,这个0里面包含参数包 Is。
晕了吗?现在来总结一下:print_tuple_impl 其实就建了一个数组,这个数组里每一个元素都是一条语句,所以本质上相当于执行了一个循环。稍微改写一下可能更容易理解:
1 #define FOREACH_IN_PACK(expr) swallow{(expr ,0)...} 2 // 省略了(void),Siriuslzx亲测不会warning 3 template<class Tuple, std::size_t... Is> 4 auto print_tuple_impl(std::ostream& os, const Tuple & t, 5 std::index_sequence<Is...>) 6 { 7 using swallow = int[]; 8 FOREACH_IN_PACK(os << (Is == 0? "" : ", ") << std::get<Is>(t) ); 9 } 10 11 template<class... Args> 12 decltype(auto) operator<<(std::ostream& os, const std::tuple<Args...>& t) 13 { 14 os << "("; 15 print_tuple_impl(os, t, std::index_sequence_for<Args...>{}); 16 return os << ")"; 17 }