<C++> C++11可变参数模板

C++11可变参数模板

C++中的可变参数模板(Variadic Templates)是C++11引入的一项特性,它允许定义可以接受任意数量和任意类型参数的函数模板或类模板。

可变参数模板的基本语法是在参数列表中使用省略号(…)表示可变参数的位置。

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

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

比如下面的例子使用args[i]获取可变参数出现报错:

template<class ...Args>
void ShowList(Args... args) {
    
    
	cout << sizeof...(args) << endl;   //sizeof... 计算可变参数的数量,而不是字节
	
	//error C3520: 'args': parameter pack must be expanded in this context(必须在此上下文中展开参数包) 编译器无法知道
    //当我们使用sizeof...(args)获取参数包的数量时,这是一个在编译时求值的表达式。然而,在运行时通过索引访问参数包的元素是不允许的,因为编译器无法确定具体的参数类型和数量。
	for (int i = 0; i < sizeof...(args); i++) {
    
    
		cout << args[i] << " ";
	}
	cout << endl;
}


int main() {
    
    
	ShowList();
	ShowList('x');
	ShowList('x', 'y');
	ShowList('x', 1);

	return 0;
}

正确的处理方式是递归展开参数包

递归展开获取参数包

// 递归终止函数
template<class T>
void ShowList(const T& t) {
    
    
	cout << t << " ";
}

// 添加重载版本,处理没有参数的情况
void ShowList() {
    
    
	cout << endl;
}

// 展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args) {
    
    
	cout << value << " ";
	ShowList(args...);
}

int main() {
    
    
	ShowList();
	ShowList('x');
	ShowList('x', 'y');
	ShowList('x', 1);

	return 0;
}

可变参数模板的工作原理是基于递归展开参数包的概念。在上述代码中,ShowList函数模板被定义为递归调用自身的形式,每次调用时都处理一个参数,并将剩余的参数包作为参数传递给下一次递归调用。

这种递归展开的方式允许我们在每次递归调用中处理一个参数,并逐步处理完所有的参数。在每个递归调用中,第一个参数被打印出来,然后剩余的参数包继续作为参数传递给下一次递归调用。递归的终止条件是当参数包为空时,即没有更多的参数需要处理时,递归结束。

通过这种方式,我们可以灵活地处理任意数量和类型的参数,而无需提前知道参数的具体数量或类型。递归展开参数包的过程在编译时进行,因此可以保证在运行时高效地展开和处理参数。

逗号表达式展开参数包

可以使用逗号表达式(comma expression)来展开参数包。逗号表达式可以同时执行多个表达式,并返回最后一个表达式的结果。通过在展开过程中使用逗号表达式,我们可以依次处理参数包中的每个参数。

以下是使用逗号表达式展开参数包的示例代码:

#include <iostream>

template <class... Args>
void ShowList(Args... args) {
    
    
    int dummy[] = {
    
     (std::cout << args << " ", 0)... };
    std::cout << std::endl;
}

int main() {
    
    
    ShowList(1);
    ShowList(1, 'a');
    ShowList(1, 'a', "Hello");
    ShowList(1, 'a', "Hello", 3.14);
    return 0;
}

在上述代码中,我们使用逗号表达式在展开过程中依次执行 (std::cout << args << " ", 0)。这里使用了一个技巧,将逗号表达式作为初始化列表的一部分,并将它们赋值给一个名为 dummy 的整型数组。因为逗号表达式返回最后一个表达式的结果,这里我们使用了 0 作为最后一个表达式,来确保整型数组中的元素都是 0

通过这种方式,逗号表达式会依次执行参数包中的每个表达式,并输出到标准输出流中。

输出结果为:

1

a

a Hello

1 a Hello 3.14

请注意,在展开过程中使用逗号表达式时,我们通常会将它们作为表达式的一部分,例如在赋值语句、初始化列表、函数调用等中,以便正确地展开参数包。

STL容器中的empalce相关接口函数:

cplusplus.com/reference/vector/vector/emplace_back/

cplusplus.com/reference/list/list/emplace_back/

template<class... Args>
void emplace_back(Args &&...args);

template<class... Args>
void emplace_back(Args &&...args);

emplace_back的原理是通过使用完美转发和变长参数模板来实现直接在容器中构造对象。相比于push_back,它避免了额外的构造、拷贝或移动操作,提高了性能和效率。

下面是一个使用emplace_backpush_back的示例代码,以说明它们的区别和原理:

#include <iostream>
#include <vector>

class MyClass {
    
    
public:
    MyClass(int value1) : data1(value1) {
    
    
        std::cout << "Constructor: " << data1 << std::endl;
    }

    MyClass(int value1, int value2) : data1(value1), data2(value2) {
    
    
        std::cout << "Constructor: " << data1 << ", " << data2 << std::endl;
    }

    MyClass(const MyClass &other) : data1(other.data1), data2(other.data2) {
    
    
        std::cout << "Copy Constructor: " << data1 << ", " << data2 << std::endl;
    }

    MyClass(MyClass &&other) noexcept : data1(other.data1), data2(other.data2) {
    
    
        std::cout << "Move Constructor: " << data1 << ", " << data2 << std::endl;
    }

private:
    int data1 = 0;
    int data2 = 0;
};

int main() {
    
    
    std::vector<MyClass> vec;

    MyClass obj1(1);          // 构造
    vec.push_back(obj1);      // 调用拷贝构造函数
    vec.push_back(2);         //构造+移动构造,会创建一个临时对象
    vec.push_back(MyClass(2));//构造+移动构造

    vec.emplace_back(3);  // 调用接受一个参数的构造函数,直接构造
    vec.emplace_back(4, 5);  // 调用接受两个参数的构造函数,直接构造

    return 0;
}

在上述代码中,我们定义了一个简单的 MyClass 类,它具有不同类型的构造函数和拷贝/移动构造函数。我们使用std::vector作为容器,并通过 push_backemplace_back 向容器中添加元素。

通过运行这段代码,可以观察到构造函数和拷贝/移动构造函数的输出。以下是输出结果的解释:

从输出结果可以看出,使用 push_back 时,需要先构造对象并复制或移动到容器中,因此会调用相应的构造函数和拷贝/移动构造函数。而使用 emplace_back 时,我们直接在容器内部构造对象,省去了额外的构造、拷贝或移动操作。

因此,emplace_back的原理是通过使用完美转发和变长参数模板,在容器内部直接构造对象,避免了额外的构造、拷贝或移动操作,提高了性能和效率。但是提升的并不明显,因为push_back也只是多了一次移动构造。

但是在以下场景中,提高的性能和效率就明显了

#include <iostream>
#include <vector>
#include <string>

struct Person {
    
    
    std::string name;
    int age;

    Person(const std::string& n, int a) : name(n), age(a) {
    
    
        std::cout << "Constructor: " << name << ", " << age << std::endl;
    }

    Person(const Person& other) : name(other.name), age(other.age) {
    
    
        std::cout << "Copy Constructor: " << name << ", " << age << std::endl;
    }

    Person(Person&& other) noexcept : name(std::move(other.name)), age(other.age) {
    
    
        std::cout << "Move Constructor: " << name << ", " << age << std::endl;
    }
};

int main() {
    
    
    std::vector<Person> people;

    std::string name = "Alice";
    int age = 25;

    // 使用 emplace_back
    people.emplace_back(name, age);  //直接构造

    std::cout << "--------------" << std::endl;

    //使用 push_back
    people.push_back(Person(name, age));  //构造+移动构造
    //不可以向emplace_back直接传name,age,只能用name,age先构造一个Person对象

    return 0;
}
  • 使用emplace_back时,我们直接传递参数nameageemplace_back,它在容器内部直接构造一个新的Person对象。输出只显示了构造函数的调用,没有调用复制构造函数或移动构造函数。这是因为emplace_back直接使用参数在容器内构造了新元素,避免了临时对象的创建和复制/移动操作。
  • 使用push_back时,我们创建了一个Person对象(push_back不支持可变参数模板,不可以直接传递nameage),然后将其传递给push_back。输出显示构造函数和移动构造函数的调用。这是因为在push_back中,我们创建了一个临时的Person对象,然后将其移动到容器中。

猜你喜欢

转载自blog.csdn.net/ikun66666/article/details/131333482