当前位置:   article > 正文

C++变参模板

C++变参模板

        从c++11开始,模板可以接受一组数量可变的参数,这种技术称为变参模板。

变参模板

        下面一个例子,通过变参模板打印一组数量和类型都不确定的参数。

  1. #include <iostream>
  2. #include <string>
  3. void print(void)
  4. {
  5. std::cout<<"........................"<<std::endl;
  6. }
  7. template <typename T, typename ... Ts>
  8. void print(T arg1, Ts ... args)
  9. {
  10. std::cout<<arg1<<std::endl;
  11. print(args ...);
  12. }
  13. int main(int argc, char **argv)
  14. {
  15. print("hello", 7.5, 10, std::string("building"));
  16. }

        看到上面这段代码,首先会产生两个疑问:

  • void print(T arg1, Ts ... args)中arg1是什么作用?
  • void print(void)有什么作用?

        下面将通过解释这段代码的运行过程,来解答上面的问题。仔细观察上面这段代码,不难发现,print函数模板是一个递归函数模板。执行过程大体如下:

  1. main函数调用print("hello", 7.5, 10, std::string("building"));,"hello"赋值给arg1,其余参数赋值给args
  2. print函数输出"hello",再次调用自身print(7.5, 10, std::string("building"));,7.5赋值给arg1,其余参数赋值给args
  3. print函数输出7.5,再次调用自身print(10, std::string("building"));,10赋值给arg1,其余参数赋值给args
  4. print函数输出10,再次调用自身print(std::string("building"));,"building"赋值给arg1,args为空
  5. print函数输出"building",因为args为空,此时不在调用自身,而是重载函数print(void),然后结束递归。

        从整个过程来看,arg1的主要作用就是从args迭代取值,print(void)负责处理args为空的情况。那么不定义void print(void)是否可以呢?答案是否定的,不定义该函数,编译将会报错“No matching function for call to 'print'”。

        此处还应该注意一个问题,print和c/c++的printf原理不一样:printf通过va_list实现变参,而print函数模板是为每种情况都生成了一个重载函数,如下:

        上面的信息来自于xcode调试,当然,也可以通过objdump查看,也会得到相同的结果,编译器确实生成了多个print重载函数:

         当然,上面的代码还可以写成下面的样子:

  1. template <typename T>
  2. void print(T arg)
  3. {
  4. std::cout<<arg<<std::endl;
  5. }
  6. template <typename T, typename ... Ts>
  7. void print(T arg1, Ts ... args)
  8. {
  9. print(arg1);
  10. print(args ...);
  11. }

        如果代码中没有print(arg1),程序知会打印最后一个参数building,print只有迭代到最后一个参数时,才会找到合适的函数print(T arg)。

        但一定要注意,下面的实现方式是错误的,无递归结束条件,无限迭代,直到耗尽堆栈空间:

  1. void print(void)
  2. {
  3. }
  4. template <typename ... Ts>
  5. void print(Ts ... args)
  6. {
  7. print(args ...);
  8. }

折叠表达式

        从c++17开始,c++引入了一种更为简洁灵活的编程方式——折叠表达式,下面是一个简单的例子:

  1. #include <cstdio>
  2. template <typename ...T>
  3. auto sum(T ... args)
  4. {
  5. return (... + args);
  6. }
  7. int main(int argc, char **argv)
  8. {
  9. int s = sum(1, 2, 3, 4, 5);
  10. printf("%d\n", s);
  11. }

        几乎所有的二元运算符都可以用于折叠表达式,下面是一些其他运算符的例子:

  1. template <typename F, typename ...T>
  2. auto apply(F f, T ...args)
  3. {
  4. return (f(args), ...);
  5. }
  6. template <typename ...T>
  7. bool and_op(T ...args)
  8. {
  9. return (args && ...);
  10. }

        迭代表达式,仅仅是围绕一个操作符简单地展开,例如连加。因此,对于三元操作符:?,很难用迭代表达式来实现。所以,想使用迭代表达式和:?求一个集合中的极值,是无法实现的。但是可以通过其他方式实现,下面便是一种实现方式:

  1. template <typename T>
  2. struct min_op final
  3. {
  4. public:
  5. min_op(T data) : is_first(true), min_data(data) {
  6. }
  7. T operator()(T rhs) {
  8. if (is_first) {
  9. is_first = false;
  10. min_data = rhs;
  11. return min_data;
  12. }
  13. min_data = min_data < rhs ? min_data : rhs;
  14. return min_data;
  15. }
  16. private:
  17. bool is_first;
  18. T min_data;
  19. };
  20. template <typename T, typename ...Ts>
  21. auto min(T arg, Ts ... args)
  22. {
  23. min_op<T> op(arg);
  24. return (op(args), ...);
  25. }

        很明显,这种方式还不如直接使用for循环直接利索。

        与之前的递归迭代方式相比,迭代表达式最大的优点是编译器没有为其生成过多的重载函数。迭代表达式与之前的优点:不使用vector,不会生成多个函数,缺点:解决元素较少的情况。

        变参模板的优点:

  • 可以支持不同的类型
  • 可以不使用容器
  • 直接访问元素,效率比较高

        但其并不是完美无缺的,:

  • 不适用元素较多的情况
  • 使用递归迭代会生成大量的重载函数

变参类模板和变参表达式

变参表达式

        函数参数包除了转发所有参数外,还可以做其他事,例如计算他们的值。

  1. template <typename ... Ts>
  2. void print_doubled(Ts ... args)
  3. {
  4. print((args + args) ...);
  5. }
  6. ...
  7. print_doubled(1, 2, 3, 4, 5, 6);
  8. ...

变参下标

        作为另外一个例子,下面的函数通过一组变参下标来访问第一个参数中相应的元素:

  1. template<typename T, typename ...IDS>
  2. void print_elems(T a, IDS ...ids)
  3. {
  4. print(a[ids]...);
  5. }
  6. ...
  7. std::vector<int> v{1, 2, 3, 4, 5, 6};
  8. print_elems(v, 1, 3, 5);
  9. ...

变参模板类

        提到变参模板类,首先会想到std::tuple,该种技术使得不定义新类型的前提下,多值返回成为一种可能,提供了更加灵活的编程方式,例如:

  1. template <typename T>
  2. std::tuple<T, T, T, T> calc(T x, T y)
  3. {
  4. return std::make_tuple(x + y, x - y, x * y, x / y);
  5. }
  6. ...
  7. auto result = calc(10.0, 2.5);
  8. ...

 变参基类

        变参基类从不定数的基类派生出一个新的类,主要目的是代码复用,比普通写法更加方便,派生类无需引入基类头文件,但需要注意多继承陷阱。下面是一个简单的例子:

  1. #include <cstdio>
  2. struct fly_animal
  3. {
  4. void fly(void) { printf("flying !\n"); }
  5. };
  6. struct swim_animal
  7. {
  8. void swim(void) { printf("swiming !\n"); }
  9. };
  10. struct run_animal
  11. {
  12. void run(void) { printf("running !\n"); }
  13. };
  14. struct fish
  15. {
  16. //...
  17. };
  18. struct bird
  19. {
  20. //...
  21. };
  22. struct mammal
  23. {
  24. //....
  25. };
  26. template <typename ...Bases>
  27. struct overloader : Bases...
  28. {
  29. //using Bases::operator()...;
  30. };
  31. int main(int argc, const char **argv)
  32. {
  33. using flyfish = overloader<fly_animal, swim_animal>;
  34. flyfish ff;
  35. ff.fly();
  36. ff.swim();
  37. using crocodile = overloader<run_animal, swim_animal>;
  38. crocodile ccdl;
  39. ccdl.run();
  40. ccdl.swim();
  41. using cat = overloader<mammal, run_animal>;
  42. cat ct;
  43. ct.run();
  44. return 0;
  45. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/236868
推荐阅读
相关标签
  

闽ICP备14008679号