【C++】C++11可变参数模板(函数模板、类模板)

浅浅的花香味﹌ 2023-07-20 12:16 162阅读 0赞

在C++11之前,类模板和函数模板只能含有固定数量的模板参数。C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板。可变参数模板的加入使得C++11的功能变得更加强大,而由此也带来了许多神奇的用法。

本文实例源码github地址:https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2020Q2/20200401。

可变参数模板

可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typenameclass后面带上省略号...

  1. template<typename... Types>

其中,...可接纳的模板参数个数是0个及以上的任意数量,需要注意包括0个

若不希望产生模板参数个数为0的变长参数模板,则可以采用以下的定义:

  1. template<typename Head, typename... Tail>

本质上,...可接纳的模板参数个数仍然是0个及以上的任意数量,但由于多了一个Head类型,由此该模板可以接纳1个及其以上的模板参数

函数模板的使用

在函数模板中,可变参数模板最常见的使用场景是以递归的方法取出可用参数

  1. void print() { }
  2. template<typename T, typename... Types>
  3. void print(const T& firstArg, const Types&... args) {
  4. std::cout << firstArg << " " << sizeof...(args) << std::endl;
  5. print(args...);
  6. }

通过设置...,可以向print函数传递任意个数的参数,并且各个参数的类型也是任意。也就是说,可以允许模板参数接受任意多个不同类型的不同参数。这就是不定参数的模板,格外需要关注的是,...三次出现的位置

如果如下调用print函数:

  1. print(2, "hello", 1);

如此调用会递归将3个参数全部打印。细心的话会发现定义了一个空的print函数,这是因为当使用可变参数的模板,需要定义一个处理最后情况的函数,如果不写,会编译错误。这种递归的方式,是不是觉得很惊艳!

在不定参数的模板函数中,还可以通过如下方式获得args的参数个数:

  1. sizeof...(args)

假设,在上面代码的基础上再加上一个模板函数如下,那么运行的结果是什么呢?

  1. #include <iostream>
  2. void print() { }
  3. template<typename T, typename... Types>
  4. void print(const T& firstArg, const Types&... args) {
  5. std::cout << firstArg << " " << sizeof...(args) << std::endl;
  6. print(args...);
  7. }
  8. template <typename... Types>
  9. void print(const Types&... args) {
  10. std::cout << "print(...)" << std::endl;
  11. }
  12. int main(int argc, char *argv[]) {
  13. print(2, "hello", 1);
  14. return 0;
  15. }

现在有一个模板函数接纳一个参数加上可变参数,还有一个模板函数直接接纳可变参数,如果调用print(2, “hello”, 1),会发现这两个模板函数的参数格式都符合。是否会出现冲突、不冲突的话会执行哪一个呢?

运行代码后的结果为:

  1. yngzmiao@yngzmiao-virtual-machine:~/test/$ ./main
  2. 2 2
  3. hello 1
  4. 1 0

从结果上可以看出,程序最终选择了一个参数加上不定参数的模板函数。也就是说,当较泛化和较特化的模板函数同时存在的时候,最终程序会执行较特化的那一个

再比如一个例子,std::max函数只可以返回两个数的较大者,如果多个数,就可以通过不定参数的模板来实现:

  1. #include <iostream>
  2. template <typename T>
  3. T my_max(T value) {
  4. return value;
  5. }
  6. template <typename T, typename... Types>
  7. T my_max(T value, Types... args) {
  8. return std::max(value, my_max(args...));
  9. }
  10. int main(int argc, char *argv[]) {
  11. std::cout << my_max(1, 5, 8, 4, 6) << std::endl;
  12. return 0;
  13. }

类模板的使用

除了函数模板的使用外,类模板也可以使用不定参数的模板参数,最典型的就是tuple类了。其大致代码如下:

  1. #include <iostream>
  2. template<typename... Values> class tuple;
  3. template<> class tuple<> { };
  4. template<typename Head, typename... Tail>
  5. class tuple<Head, Tail...>
  6. : private tuple<Tail...>
  7. {
  8. typedef tuple<Tail...> inherited;
  9. public:
  10. tuple() { }
  11. tuple(Head v, Tail... vtail) : m_head(v), inherited(vtail...) { }
  12. Head& head() { return m_head;}
  13. inherited& tail() { return *this;}
  14. protected:
  15. Head m_head;
  16. };
  17. int main(int argc, char *argv[]) {
  18. tuple<int, float, std::string> t(1, 2.3, "hello");
  19. std::cout << t.head() << " " << t.tail().head() << " " << t.tail().tail().head() << std::endl;
  20. return 0;
  21. }

根据代码可以知道,tuple类继承除首之外的其他参数的子tuple类,以此类推,最终继承空参数的tuple类。继承关系可以表述为:

  1. tuple<>
  2. tuple<std::string>
  3. string "hello"
  4. tuple<float, std::string>
  5. float 2.3
  6. tuple<int, float, std::string>
  7. int 1

接下来考虑在内存中的分布,内存中先存储父类的变量成员,再保存子类的变量成员,也就是说,对象t按照内存分布来说;

  1. ┌─────────┐<---- 对象指针
  2. | hello |
  3. |─────────|
  4. | 2.3 |
  5. |─────────|
  6. | 1 |
  7. └─────────┘

这时候就可以知道下一句代码的含义了:

  1. inherited& tail() { return *this;}

tail()函数返回的是父类对象,父类对象和子类对象的内存起始地址其实是一样的,因此返回*this,再强行转化为inherited类型。

当然,上面采用的是递归继承的方式,除此之外,还可以采用递归复合的方式:

  1. template<typename... Values> class tup;
  2. template<> class tup<> {};
  3. template<typename Head, typename... Tail>
  4. class tup<Head, Tail...>
  5. {
  6. typedef tup<Tail...> composited;
  7. public:
  8. tup() {}
  9. tup(Head v, Tail... vtail) : m_head(v), m_tail(vtail...) {}
  10. Head& head() {return m_head;}
  11. composited& tail() {return m_tail;}
  12. protected:
  13. Head m_head;
  14. composited m_tail;
  15. };

两种方式在使用的过程中没有什么区别,但C++11中采用的是第一种方式(递归继承)。

在上面的例子中,取出tuple中的元素是一个比较复杂的操作,需要不断地取tail,最终取head才能获得。标准库的std::tuple,对此进行简化,还提供了一些其他的函数来进行对tuple的访问。例如:

  1. #include <iostream>
  2. #include <tuple>
  3. int main(int argc, char *argv[]) {
  4. std::tuple<int, float, std::string> t2(1, 2.3, "hello");
  5. std::get<0>(t2) = 4; // 修改tuple内的元素
  6. std::cout << std::get<0>(t2) << " " << std::get<1>(t2) << " " << std::get<2>(t2) << std::endl; // 获取tuple内的元素
  7. auto t3 = std::make_tuple(2, 3.4, "World"); // make方法生成tuple对象
  8. std::cout << std::tuple_size<decltype(t3)>::value << std::endl; // 获取tuple对象元素的个数
  9. std::tuple_element<1, decltype(t3)>::type f = 1.2; // 获取tuple对象某元素的类型
  10. return 0;
  11. }

相关阅读

  • C++标准库元组(tuple)源码浅析

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly95bmd6bWlhby5ibG9nLmNzZG4ubmV0_size_16_color_FFFFFF_t_70

发表评论

表情:
评论列表 (有 0 条评论,162人围观)

还没有评论,来说两句吧...

相关阅读