欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++高级编程(第4版) 个人笔记 22.5 - Variadic Templates 可变参数模板

程序员文章站 2024-03-14 11:28:16
...

参考书籍:
《C++高级编程》(第4版)》
C++高级编程(第4版) 个人笔记 22.5 - Variadic Templates 可变参数模板


1.看代码粗学.

代码:

#include <iostream>
#include <bitset>

void print(){/*不执行任何操作。*/}

template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args)
{
    std::cout << "args_size: " << sizeof...(args) << std::endl;
    std::cout << firstArg << std::endl;
    std::cout << "---------------" << std::endl;
    print(args...);
}

int main() {
    print(10.04, "hello", std::bitset<16>(1124), 629);
    return 0;
}

运行结果:

C++高级编程(第4版) 个人笔记 22.5 - Variadic Templates 可变参数模板

就是一个所谓的pack(包)
用于template parameters,就是template parameters pack(模板参数)
用于function parameter types,就是function parameter types pack(函数参数类型)
用于function parameters,就是function parameters pack(函数参数类型)

sizeof… 运算符
查询形参包中的元素数量。
语法;:sizeof…( 形参包 ) (C++11 起)
解释:返回形参包中的元素数量。

2.细节:

普通模板只可采取固定数量的模板参数。可变参数模板(variadic template)可接收可变数目的模板参数。例如,下面的代码定义了一个模板,它可以接收任何数目的模板参数,使用称为Types的参数(parameter pack):

template<typename... Types>
class MyVariadicTemplate {};

注意:
typename之后的三个点并非错误。这是为可变参数模板定义参数包的语法。参数包可以接收可变数目的参数。在三个点的前后允许添加空格。

可用任何数量的类型实例化MyVariadicTemplalte,例如:

MyVariadicTemplate<int> instance1;
MyVariadicTemplate<std::string, double, std::list<int>> instance2;

甚至可用零个模板参数实例化MyVariadicTemplalte

MyVariadicTemplate<> instance3;

为避免用零个模板参数实例化可变参数模板,可以像下面这样编写模板:

template<typename T1,typename... Types>
class MyVariadicTemplate {};

有了这个定义后,试图通过零个模板参数实例化MyVariadicTemplate会导致编译错误。例如,JetBrains CLion会给出如下错误:

//Error: wrong number of template arguments (0, should be at least 1) 

不能直接遍历传给可变参数模板的不同参数。唯一方法是借助模板递归的帮助。下面通过两个例子来说明如何使用可变参数模板。

2.1类型安全的变长参数列表.

可变参数模板允许创建类型安全的变长参数列表。下面的例子定义了一个可变参数processValues(),它允许以类型安全的方式接收不同类型的可变数目的参数。函数processValues() 会处理变长参数列表中的每个值,对每个参数执行 handleValue() 函数。这意味着必须对每种要处理的类型编写handleValue() 函数,例如下例中的intdoublestring

#include <iostream>

using namespace std;

void handleValue(int value) {
    cout << "Integer: " << value << endl;
}

void handleValue(double value) {
    cout << "Double: " << value << endl;
}

void handleValue(string_view value) {
    cout << "String: " << value << endl;
}

void processValues(){/*不执行任何操作。*/}

template<typename T1, typename... Tn>
void processValues(T1 arg1, Tn... args) {
    handleValue(arg1);
    processValues(args...);
}

int main() {
    processValues(1, 2, 3.1415926, "test_string", 1.1f);
}

在前面的例子中,三点运算符“”用了两次。这个运算符出现在3个地方,有两个不同的含义。首先用在模板参数列表中typename的后面以及函数参数列表中类型Tn的后面。在这两种情况下,它都表示参数包。参数包可接收可变数目的参数。

运算符的第二种用法是在函数体中参数名args的后面。这种情况下,它表示参数包扩展。 这个运算符会解包展开参数包,得到各个参数。它基本上提取出运算符左边的内容,为包中的每个模板参数重复该内容,并用逗号隔开。从前面的例子中取出以下行:

processValues(args...) ;

这一行将args参数包解包(或扩展)为不同的参数,通过逗号分隔参数,然后用这些展开的参数调用processValues() 函数。模板总是需要至少一个模板参数: T1。通过arg… 递归调用 processValues() 的结果是:每次调用都会少一个模板参数。

由于processValues() 函数的实现是递归的,因此需要采用一种方法来停止递归。为此,实现一个processValues() 函数,要求它接收零个参数。

测试结果:
C++高级编程(第4版) 个人笔记 22.5 - Variadic Templates 可变参数模板
这个例子生成的递归调用是:

    processValues(1, 2, 3.1415926, "test_string", 1.1f);
        handleValue(1);
        processValues(2, 3.1415926, "test_string", 1.1f);
            handleValue(2);
        processValues(3.1415926, "test_string", 1.1f);
            handleValue(3.1415926);
            processValues( "test_string", 1.1f);
                handleValue("test_string");
                processValues( 1.1f);
                    handleValue(1.1f);
                    processValues();

重要的是要记住,这种变长参数列表是完全类型安全的。processValues() 函数会根据实际类型自动调用正确的handleValue() 重载版本。C++中 也会像通常那样自动执行类型转换。例如,前面例子中1.1f的类型为floatprocessValues() 函数会调用handleValue(double value), 因为从floatdouble的转换没有任何损失。然而,如果调用 processValues() 时带有某种类型的参数,而这种类型没有对应的handleValue() 函数,编译器会产生错误。

前面的实现存在一个小问题。由于这是一个递归的实现,因此每次递归调用processValues() 时都会复制参数。根据参数的类型,这种做法的代价可能会很高。你可能会认为,向processValues() 函数传递引用而不使用按值传递方法,就可以避免这种复制问题。遗憾的是,这样也无法通过字面量调用processValues() 了,因为不允许使用字面量引用,除非使用const引用。

为了在使用非const引用的同时也能使用字面量值,可使用转发引用(forwarding references)。以下实现使用了转发引用T&&,还使用st::forward() 完美转发所有”意味着,如果把 rvalue传递给processValues(),就将它作为rvalue引用转发;如果把lvalueIvalue引用传递给processValues(),就将它作为lvalue引用转发。

void processValues(){/*不执行任何操作。*/}

template<typename T1, typename... Tn>
void processValues(T1&& arg1, Tn&&... args) {
    handleValue(std::forward<T1>(arg1));
    processValues(std::forward<Tn>(args)...);
}

std::forward通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。

有一行代码需要做进一步解释:

processValues(std::forward<Tn>(args)...);

”运算用于解开参数包,它在参数包中的每个参数上使用std::forward用逗号把它们隔开。例如,假设args是一个参数包,有三个参数(a1a2a3),分别对应三种类型(A1A2A3)。扩展后的调用如下:

processValues(std::forward<Al>(a1),
        std::forward<A2> (a2),
        std:: forward<A3> (a3)) ;

在使用了参数包的函数体中,可通过以下方法获得参数包中参数的个数:

int numOfArgs = sizeof... (args) ;

一个使用变长参数模板的实际例子是编写一个类似于 printf() 版本的安全且类型安全的函数模板。这是实践变长参数模板的一次不错练习。


欢迎关注公众号:c_302888524
发送:“C++高级编程(第3版)” 获取电子书
C++高级编程(第4版) 个人笔记 22.5 - Variadic Templates 可变参数模板