【C++】—— C++11之可变参数模板

简介: 【C++】—— C++11之可变参数模板

前言:

  • 在C语言中,我们谈论了有关可变参数的相关知识。在C++11中引入了一个新特性---即可变参数模板。本期,我们将要介绍的就是有关可变参数模板的相关知识!!!



序言

C++11的新特性可变参数模板能够让我们可以接受可变参数的函数模板和类模板,相比

C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。

 


(一)可变参数模板函数

在了解模板函数和模板类之前,我们需要先知道几个概念:

一个 可变参数模板 就是一个接收可变数目参数的模板函数或者是模板类。可变数目的参数被称为 函数包。存在两种函数包:

  • 模板参数包:使用 typename/class ... Args 来指出 Args 是一个模板参数包,表示零个或多个模板参数
  • 函数参数包:使用 Args ... rest 来指出 rest 是一个函数参数包,表示零个或多个函数参数

💨下面就是一个基本可变参数的函数模板:

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

【解释说明】

  • 上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。

与往常一样,编译器从函数的实参推倒模板参数类型。对于一个可变参数模型,编译器还会推断包中参数的数目。例如,看下面一个简单例子:

// 可变参数函数模板
template<typename... Args>
void printArgs(Args... args) 
{
    cout << "Number of arguments: " << sizeof...(args) << endl;
    cout << endl;
}
int main() {
    printArgs(1, 2, 3);                  // 输出:Number of arguments: 3   Arguments: 1 2 3
    printArgs("Hello", 3.14, 'c');       // 输出:Number of arguments: 3   Arguments: Hello 3.14 c
    printArgs(10);                       // 输出:Number of arguments: 1   Arguments: 10
    printArgs();                         // 输出:Number of arguments: 0   Arguments:
    return 0;
}

编译器会为 printArgs 实例化出以下四个不同版本,我们看下上面程序的汇编代码:


1、sizeof... 运算符

大家可以发现上述代码样例中,我使用了 sizeof...  这样的字段。那么这个是什么意思呢?

其实是 sizeof...   C++11引入的一元运算符,用于获取可变模板参数包中的参数数量:

  • 因此,接下来我尝试运行一下代码,看结果:

【解释说明】

  • 在上面的示例中,我们定义了一个函数模板  printArgs ,它接受可变数量的模板参数。在函数模板中,我们使用  sizeof...   来获取参数包 args 中的参数数量

【小结】

  1. 运算符可以用于类模板和函数模板中,用于获取参数包的大小;
  2. 它在处理可变参数模板时非常有用,可以帮助我们实现更加灵活和通用的代码。

(二)扩展参数包的两种方法

因为我们无法直接获取参数包args中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

1、递归函数方式展开参数包

通过递归函数方式展开参数包是一种常见的处理可变参数模板的方法。这种方法利用函数的递归调用来依次处理参数包中的每个参数。

  • 下面是一个示例展示了如何使用递归函数方式展开参数包:
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
  cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
  cout << value << " ";
  ShowList(args...);
}
int main()
{
  ShowList(1);
  ShowList(1, 'A');
  ShowList(1, 'A', std::string("sort"));
  return 0;
}

【解释说明】

在上面的示例中,ShowList 是一个展开函数模板,用于递归展开参数包并输出每个参数的值。

  • ShowList(const T& t):是递归终止函数,用于处理只有一个参数的情况。它接受一个参数 t,并将其输出到标准输出流;
  • ShowList(T value, Args... args):是展开函数模板的递归部分。它接受一个参数 value 和更多的参数包 Args... args。在函数体内,它首先输出 value 的值,然后通过递归调用 ShowList 函数来处理剩余的参数包 args...

以下是运行上述代码的输出结果:

通过递归函数方式展开了参数包,并成功输出了每个参数的值。这是一种常见的使用递归的方法来处理可变参数模板的方式。

💨【注意】 当定义可变参数版本的 ShowList 时,非可变参数版本必须要声明在可变参数版本 (递归体) 的作用域当中,否则会导致无限递归!!!


2、逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式
实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

 

  • 下面是一个示例展示了如何利用逗号表达式来展开参数包:
template <class T>
void PrintArg(T t)
{
  cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
  int arr[] = { (PrintArg(args), 0)... };
  cout << endl;
}
int main()
{
  ShowList(1);
  ShowList(1, 'A');
  ShowList(1, 'A', std::string("sort"));
  return 0;
}

【解释说明】

在上面的示例中,PrintArg 是一个简单的辅助函数模板,用于打印参数的值。

  1. ShowList 是一个展开函数模板,它接受可变数量的参数,并使用逗号表达式来展开参数包。在展开过程中,每个参数都会被传递给 PrintArg 函数进行处理,并且逗号表达式的结果被忽略。
  2. main 函数中,我们调用了 ShowList 函数,并传递了不同数量和类型的参数。通过逗号表达式展开参数包,每个参数都会被依次处理,并调用 PrintArg 函数将其值输出到标准输出流。

以下是运行上述代码的输出结果:

上述代码我们还可以像下述这样去进行实现操作:

【说明】

  1. expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0;
  2. 同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)];
  3. 由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分 printarg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

3、两种方法的优缺点

逗号表达式扩展方式和递归包扩展方式都可以用于展开可变参数模板,但它们具有不同的优缺点

具体如下:

逗号表达式扩展方式的优点:

  1. 简洁性:使用逗号表达式可以在一行代码中展开参数包,代码量较少且结构清晰。
  2. 高效性:逗号表达式会在编译时展开参数包,可以生成高效的代码。
  3. 可读性:逗号表达式展开参数包的语法较为直观,易于阅读和理解。

逗号表达式扩展方式的缺点:

  1. 顺序限制:逗号表达式展开参数包的顺序是从左到右,无法灵活地改变参数的处理顺序。
  2. 局限性:逗号表达式虽然简洁,但在某些复杂的情况下可能比较难以处理。

递归包扩展方式的优点:

  1. 灵活性:利用递归函数方式展开参数包可以在每一步递归中对参数进行处理和逻辑操作,具有更高的灵活性。
  2. 可控性:递归包展开方式可以通过递归函数中的控制语句和条件语句对参数包的展开进行控制。

递归包扩展方式的缺点:

  1. 代码冗余:递归函数方式展开参数包可能需要更多的代码量来实现,相对于逗号表达式方式来说,代码可能会更冗长。
  2. 可读性:递归函数方式展开参数包的代码结构可能比较复杂,不够直观。

根据具体的情况和需求,可以根据代码的复杂度和可读性的要求选择使用逗号表达式扩展方式或递归包扩展方式。逗号表达式方式适用于简单的参数展开,而递归包方式则适用于复杂的参数展开,可以更灵活地进行处理。


总结

以上便是关于c++11 可变参数模板函数的全部知识讲解!!感谢大家的观看与支持!!!

相关文章
|
6天前
|
存储 编译器 C++
【C++】详解C++的模板
【C++】详解C++的模板
|
4天前
|
C++ 开发者
C++一分钟之-编译时计算:constexpr与模板元编程
【7月更文挑战第2天】C++的`constexpr`和模板元编程(TMP)实现了编译时计算,增强代码效率。`constexpr`用于声明编译时常量表达式,适用于数组大小等。模板元编程则利用模板进行复杂计算。常见问题包括编译时间过长、可读性差。避免方法包括限制TMP使用,保持代码清晰。结合两者可以解决复杂问题,但需明确各自适用场景。正确使用能提升代码性能,但需平衡复杂性和编译成本。
14 3
|
4天前
|
编译器 C语言 C++
【C++】模板初阶(下)
C++的函数模板实例化分为隐式和显式。隐式实例化由编译器根据实参推断类型,如`Add(a1, a2)`,但`Add(a1, d1)`因类型不一致而失败。显式实例化如`Add&lt;double&gt;(a1, d1)`则直接指定类型。模板函数不支持自动类型转换,优先调用非模板函数。类模板类似,用于创建处理多种数据类型的类,如`Vector&lt;T&gt;`。实例化类模板如`Vector&lt;int&gt;`和`Vector&lt;double&gt;`创建具体类型对象。模板使用时,函数模板定义可分头文件和实现文件,但类模板通常全部放头文件以避免链接错误。
|
4天前
|
机器学习/深度学习 算法 编译器
【C++】模板初阶(上)
**C++模板简介** 探索C++泛型编程,通过模板提升代码复用。模板作为泛型编程基础,允许编写类型无关的通用代码。以`Swap`函数为例,传统方式需为每种类型编写单独函数,如`Swap(int&)`、`Swap(double&)`等,造成代码冗余。函数模板解决此问题,如`template&lt;typename T&gt; void Swap(T&, T&)`,编译器根据实参类型推导生成特定函数,减少重复代码,增强可维护性。模板分函数模板和类模板,提供处理不同数据类型但逻辑相似的功能。
|
4天前
|
算法 编译器 程序员
|
4天前
|
存储 编译器 程序员
|
7天前
|
安全 C++
详细解读c++异常模板复习
详细解读c++异常模板复习
|
4天前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
4天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `&lt;`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
4天前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。