这篇文章讲解的是C++11的特性之一——可变参数模板,适合有一定基础的同学学习,如果是刚入门的同学可以看我过往的文章:C++基础入门
可变参数模板(Variadic Templates)是C++的一种高级特性,它允许你编写接受任意数量模板参数的模板。可变参数模板在函数、类和其他模板中都可以使用。
1. 可变参数模板的基本语法
template<typename... Args>
void func(Args... args) {
// 在这里可以使用 args...
}
typename... Args
:这里的Args...
表示这是一个模板参数包,它可以包含任意数量的模板参数,类型也可以不同。args...
:这是一个函数参数包,对应于模板参数包Args...
。它可以包含任意数量的参数。
2. 递归展开
作用
可变参数模板的主要作用是简化处理不确定数量参数的场景。比如,你可以使用它来创建一个函数,可以接受任意数量的参数,而无需为每种参数数量情况写不同的函数重载。
在C++11之前,可变参数也仅仅限于函数参数,比如最常见的是我们的老朋友printf函数,而今天提到的是模板的可变参数。
示例
假设你要写一个打印多个参数的函数,可以这样做:
#include <iostream>
// 基本模板:递归终止条件,无参的递归终止函数
void print() {
std::cout << "End of recursion\n";
}
// 可变参数模板
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << std::endl; // 打印第一个参数
print(args...); // 递归调用自身,继续打印剩余的参数
}
int main() {
print(1, 2.5, "Hello", 'A');
return 0;
}
在这个例子中,print
函数接受了任意数量的参数,并依次打印它们。递归的终止条件是函数没有参数时调用的 void print()
重载。注意参数包是不支持args[i]
来获取参数的。
如果想在没有传参数的时候也是走函数模板的代码,可以改成这样:
#include <iostream>
// 可变参数模板:处理任意数量的参数,包括没有参数的情况
template<class T, class... Args>
void ShowListArg(T value, Args... args) {
std::cout << value << " ";
ShowListArg(args...); // 递归调用,继续处理剩余的参数
}
// 空参数包的情况
template<class... Args>
void ShowListArg() {
std::cout << std::endl; // 当没有参数时,直接打印换行符
}
// 包装函数
template<class... Args>
void ShowList(Args... args) {
ShowListArg(args...); // 调用处理函数
}
int main() {
ShowList(1, 2.5, "Hello", 'A'); // 正常的参数调用
ShowList(); // 没有参数的调用
return 0;
}
带参的递归终止函数
如果你希望递归终止条件的函数也带有一个参数,可以通过限制参数的数量,使其只处理一个参数而不再递归调用。这就是递归的“基础条件”,在这种情况下,函数只需要处理最后一个参数。以下是一个带有参数的递归终止函数的例子:
#include <iostream>
// 递归终止函数:处理最后一个参数
template<class T>
void ShowListArg(T value) {
std::cout << value << std::endl; // 打印最后一个参数,并换行
}
// 可变参数模板:处理多个参数的情况
template<class T, class... Args>
void ShowListArg(T value, Args... args) {
std::cout << value << " ";
ShowListArg(args...); // 递归调用,继续处理剩余的参数
}
// 包装函数
template<class... Args>
void ShowList(Args... args) {
ShowListArg(args...); // 调用处理函数
}
也就是说这个函数至少要传一个参数,如果不传参数的话就会报错。
能不能把参数包放到数组里?
不能直接将不同类型的参数放入原生数组中,可以使用 std::initializer_list
或者 std::array
来处理参数包中的参数。
下面是一个示例,使用 std::initializer_list
将参数包中的参数放入数组并进行处理:
#include <iostream>
#include <initializer_list>
// 包装函数,用于将参数包转为 std::initializer_list
template<typename... Args>
void ShowList(Args... args) {
std::initializer_list<int> list{ args... };
for (auto value : list) {
std::cout << value << " ";
}
std::cout << std::endl;
}
int main() {
ShowList(1, 2, 3, 4); // 只支持同一类型的参数
return 0;
}
限制
使用 std::initializer_list
时,所有参数必须是同一类型(如上例中的 int
)。如果你希望处理不同类型的参数,则需要使用其他方法,如 std::tuple
或者变体类(例如 std::variant
)。以下是一个使用 std::tuple
的示例:(这段代码有点难,但不是这篇文章的重点,可以暂时忽略,感兴趣可以借助ai来学习)
#include <iostream>
#include <tuple>
// 辅助函数,用于递归地打印 std::tuple 中的元素
template<std::size_t Index = 0, typename... Args>
void printTuple(const std::tuple<Args...>& t) {
if constexpr (Index < sizeof...(Args)) {
std::cout << std::get<Index>(t) << " ";
printTuple<Index + 1>(t);
}
else {
std::cout << std::endl;
}
}
// 包装函数,将参数包放入 std::tuple
template<typename... Args>
void ShowList(Args... args) {
auto t = std::make_tuple(args...); // 创建 std::tuple
printTuple(t); // 打印 tuple 中的所有元素
}
int main() {
ShowList(1, 2.5, "Hello", 'A'); // 支持不同类型的参数
return 0;
}
-
std::tuple
:std::tuple
是一个可以包含多个不同类型元素的容器。我们将参数包args...
放入std::tuple
中,以便处理不同类型的参数。 -
printTuple
函数:- 这个函数使用递归方式来打印
std::tuple
中的每个元素。 if constexpr
是一种在编译时进行条件判断的方式,当递归到达tuple
的末尾时,停止递归并打印换行符。
- 这个函数使用递归方式来打印
这个示例支持将不同类型的参数放入数组并进行处理。你可以根据你的需求选择适合的方式。
2. 折叠表达式展开
使用折叠表达式来展开参数包是一种高级技巧,它可以在处理可变参数模板时简化代码。
下面是一个示例:
#include <iostream>
// 使用逗号表达式展开参数包
template<typename... Args>
void ShowList(Args... args) {
(std::cout << ... << args) << std::endl;
}
int main() {
ShowList(1, 2.5, "Hello", 'A'); // 调用示例
return 0;
}
(std::cout << ... << args)
:这是一个使用折叠表达式(fold expression)的语法,它可以对参数包进行操作。...
:表示参数包的展开位置。std::cout << args
:表示将参数包中的每个元素依次输出到std::cout
。
逗号表达式与折叠表达式的结合
在更传统的情况下,逗号表达式通常与初始化列表一起使用来展开参数包:
#include <iostream>
// 使用逗号表达式和初始化列表展开参数包
template<typename... Args>
void ShowList(Args... args) {
(void)std::initializer_list<int>{(std::cout << args << " ", 0)...};
std::cout << std::endl;
}
int main() {
ShowList(1, 2.5, "Hello", 'A'); // 调用示例
return 0;
}
解释
- std::initializer_list{(std::cout << args << " ", 0)…}:
{(std::cout << args << " ", 0)...}
:这是逗号表达式在初始化列表中的应用。这里的std::cout << args << " "
负责输出每个参数,逗号后的0
是为了满足std::initializer_list
的类型要求。...
负责展开参数包args...
,将每个args
依次传递给表达式(std::cout << args << " ", 0)
,然后将结果(即0
)放入std::initializer_list<int>
中。- 使用
(void)
是为了忽略std::initializer_list
的结果,因为我们只关心输出操作。
总结
- 使用折叠表达式
(std::cout << ... << args)
是现代 C++(C++17 及以后)的简洁做法,它直接对参数包进行展开,并将结果输出。 - 使用传统的逗号表达式和初始化列表是一种更通用的方法,适用于更早版本的 C++,但是不是很推荐。
这两种方法都可以有效地展开参数包,并执行所需的操作。根据你的编译器支持情况,你可以选择其中一种方式来使用。
3. emplace_back
在C++11中,STL的容器加入了emplace系列的接口,支持模板的可变参数
注意:此处的"&&"表示的是万能引用,详细可见上一篇文章(链接)
在 C++ 中,std::list
中的 push_back
和 emplace_back
是用于向列表的末尾添加元素的两个函数,但它们在使用方式和效率上有一些重要的区别。
1. push_back
-
用法:
push_back
接受一个已存在的对象或对象的副本作为参数,并将其添加到列表的末尾。 -
过程:
- 传入的对象首先会被复制(或者移动,如果支持移动语义)。
- 然后,
std::list
会调用该对象的拷贝构造函数(或移动构造函数),将对象放入列表中。
-
示例:
std::list<std::string> myList; std::string str = "Hello"; myList.push_back(str); // 传入的是对象的副本
-
性能影响: 由于需要复制(或移动)对象,
push_back
在某些情况下可能会有额外的性能开销,特别是在处理大型对象时。
2. emplace_back
-
用法:
emplace_back
直接在列表末尾构造一个对象。它接受构造函数的参数,然后在列表末尾调用构造函数来创建对象。 -
过程:
emplace_back
不需要先创建对象并再进行复制或移动,而是直接在目标位置调用构造函数进行对象构造。- 它可以避免不必要的复制或移动,从而提高性能。
-
示例:
std::list<std::string> myList; myList.emplace_back("Hello"); // 直接在列表中构造对象
-
性能优势:
emplace_back
直接构造对象,避免了对象的拷贝或移动,这在处理复杂对象或大对象时尤其高效。
当你需要向列表中添加对象,并且该对象的构造过程较为复杂或你希望避免不必要的拷贝时,emplace_back
是更好的选择。如果你已经有一个现成的对象,并且只是需要将它添加到列表中,那么使用 push_back
也是完全可以的。
传参数包
当你使用参数包传递给 push_back
和 emplace_back
时,两者的行为仍然有所不同,特别是当你处理参数包中的多个参数时。
1. push_back
和参数包
push_back
不能直接接受参数包,因为 push_back
只接受一个已经构造好的对象。如果你传递参数包给 push_back
,首先你需要将参数包展开并用于构造对象,然后将这个构造好的对象传递给 push_back
。
示例:
#include <list>
#include <string>
template <typename T, typename... Args>
void addToList(std::list<T>& lst, Args&&... args) {
lst.push_back(T(std::forward<Args>(args)...));
}
int main() {
std::list<std::string> myList;
addToList(myList, "Hello", 5, 'a'); // 传递给std::string的构造函数
return 0;
}
2. emplace_back
和参数包
emplace_back
可以直接接受参数包并将其转发给对象的构造函数。因此,当你传递参数包给 emplace_back
时,它会将这些参数直接用于在容器中构造对象,而不会进行多余的拷贝或移动操作。
示例:
#include <list>
#include <string>
template <typename T, typename... Args>
void emplaceToList(std::list<T>& lst, Args&&... args) {
lst.emplace_back(std::forward<Args>(args)...);
}
int main() {
std::list<std::string> myList;
emplaceToList(myList, "Hello", 5, 'a'); // 在容器中直接构造std::string
return 0;
}
3. 区别总结
-
push_back
和参数包:- 你需要手动展开参数包,构造对象,然后将该对象传递给
push_back
。 - 这种方式可能会导致额外的对象拷贝或移动操作。
- 你需要手动展开参数包,构造对象,然后将该对象传递给
-
emplace_back
和参数包:- 可以直接将参数包传递给
emplace_back
,由它在容器中直接构造对象。 - 更加高效,避免了不必要的拷贝和移动操作。
- 可以直接将参数包传递给
4. 实际应用
- 当你需要在容器中直接构造对象,并且传递了多个参数时,
emplace_back
是更好的选择,因为它更高效且代码更简洁。 - 如果你需要传递已经构造好的对象,或者你不能直接使用构造函数参数,则只能使用
push_back
。
这样做的好处在于使用 emplace_back
时,你可以省去中间对象的创建,直接在容器中进行对象的构造,尤其是在处理参数包时,emplace_back
能够让代码更具表现力和效率。
可变参数模板非常强大,但也可能让代码变得复杂,所以在使用时需要小心。如果学会了就会对代码的理解有进了一步,恭喜你~ 如果文章对你有帮助的话不妨点个赞。