IT评测·应用市场-qidao123.com技术社区

标题: 可变参数模板(C++11) [打印本页]

作者: 耶耶耶耶耶    时间: 2024-8-22 13:32
标题: 可变参数模板(C++11)
这篇文章解说的是C++11的特性之一——可变参数模板,适合有一定根本的同学学习,如果是刚入门的同学可以看我过往的文章:C++根本入门
可变参数模板(Variadic Templates)是C++的一种高级特性,它允许你编写接受任意数量模板参数的模板。可变参数模板在函数、类和其他模板中都可以利用。
1. 可变参数模板的根本语法

  1. template<typename... Args>
  2. void func(Args... args) {
  3.     // 在这里可以使用 args...
  4. }
复制代码

2. 递归展开

作用

可变参数模板的主要作用是简化处理不确定数量参数的场景。比如,你可以利用它来创建一个函数,可以接受任意数量的参数,而无需为每种参数数量环境写不同的函数重载。
在C++11之前,可变参数也仅仅限于函数参数,比如最常见的是我们的老朋侪printf函数,而今天提到的是模板的可变参数。
示例

假设你要写一个打印多个参数的函数,可以这样做:
  1. #include <iostream>
  2. // 基本模板:递归终止条件,无参的递归终止函数
  3. void print() {
  4.     std::cout << "End of recursion\n";
  5. }
  6. // 可变参数模板
  7. template<typename T, typename... Args>
  8. void print(T first, Args... args) {
  9.     std::cout << first << std::endl;  // 打印第一个参数
  10.     print(args...);  // 递归调用自身,继续打印剩余的参数
  11. }
  12. int main() {
  13.     print(1, 2.5, "Hello", 'A');
  14.     return 0;
  15. }
复制代码
在这个例子中,print 函数接受了任意数量的参数,并依次打印它们。递归的停止条件是函数没有参数时调用的 void print() 重载。留意参数包是不支持args来获取参数的。
如果想在没有传参数的时候也是走函数模板的代码,可以改成这样:
  1. #include <iostream>
  2. // 可变参数模板:处理任意数量的参数,包括没有参数的情况
  3. template<class T, class... Args>
  4. void ShowListArg(T value, Args... args) {
  5.     std::cout << value << " ";
  6.     ShowListArg(args...);  // 递归调用,继续处理剩余的参数
  7. }
  8. // 空参数包的情况
  9. template<class... Args>
  10. void ShowListArg() {
  11.     std::cout << std::endl;  // 当没有参数时,直接打印换行符
  12. }
  13. // 包装函数
  14. template<class... Args>
  15. void ShowList(Args... args) {
  16.     ShowListArg(args...);  // 调用处理函数
  17. }
  18. int main() {
  19.     ShowList(1, 2.5, "Hello", 'A');  // 正常的参数调用
  20.     ShowList();  // 没有参数的调用
  21.     return 0;
  22. }
复制代码
带参的递归停止函数

如果你渴望递归停止条件的函数也带有一个参数,可以通过限定参数的数量,使其只处理一个参数而不再递归调用。这就是递归的“根本条件”,在这种环境下,函数只需要处理末了一个参数。以下是一个带有参数的递归停止函数的例子:
  1. #include <iostream>
  2. // 递归终止函数:处理最后一个参数
  3. template<class T>
  4. void ShowListArg(T value) {
  5.     std::cout << value << std::endl;  // 打印最后一个参数,并换行
  6. }
  7. // 可变参数模板:处理多个参数的情况
  8. template<class T, class... Args>
  9. void ShowListArg(T value, Args... args) {
  10.     std::cout << value << " ";
  11.     ShowListArg(args...);  // 递归调用,继续处理剩余的参数
  12. }
  13. // 包装函数
  14. template<class... Args>
  15. void ShowList(Args... args) {
  16.     ShowListArg(args...);  // 调用处理函数
  17. }
复制代码
也就是说这个函数至少要传一个参数,如果不传参数的话就会报错。
能不能把参数包放到数组里?

不能直接将不同类型的参数放入原生数组中,可以利用 std::initializer_list 大概 std::array 来处理参数包中的参数。
下面是一个示例,利用 std::initializer_list 将参数包中的参数放入数组并进行处理:
  1. #include <iostream>
  2. #include <initializer_list>
  3. // 包装函数,用于将参数包转为 std::initializer_list
  4. template<typename... Args>
  5. void ShowList(Args... args) {
  6.     std::initializer_list<int> list{ args... };
  7.     for (auto value : list) {
  8.         std::cout << value << " ";
  9.     }
  10.     std::cout << std::endl;
  11. }
  12. int main() {
  13.     ShowList(1, 2, 3, 4);  // 只支持同一类型的参数
  14.     return 0;
  15. }
复制代码
限定

利用 std::initializer_list 时,全部参数必须是同一类型(如上例中的 int)。如果你渴望处理不同类型的参数,则需要利用其他方法,如 std::tuple 大概变体类(例如 std::variant)。以下是一个利用 std::tuple 的示例:(这段代码有点难,但不是这篇文章的重点,可以暂时忽略,感爱好可以借助ai来学习
  1. #include <iostream>
  2. #include <tuple>
  3. // 辅助函数,用于递归地打印 std::tuple 中的元素
  4. template<std::size_t Index = 0, typename... Args>
  5. void printTuple(const std::tuple<Args...>& t) {
  6.     if constexpr (Index < sizeof...(Args)) {
  7.         std::cout << std::get<Index>(t) << " ";
  8.         printTuple<Index + 1>(t);
  9.     }
  10.     else {
  11.         std::cout << std::endl;
  12.     }
  13. }
  14. // 包装函数,将参数包放入 std::tuple
  15. template<typename... Args>
  16. void ShowList(Args... args) {
  17.     auto t = std::make_tuple(args...);  // 创建 std::tuple
  18.     printTuple(t);  // 打印 tuple 中的所有元素
  19. }
  20. int main() {
  21.     ShowList(1, 2.5, "Hello", 'A');  // 支持不同类型的参数
  22.     return 0;
  23. }
复制代码

这个示例支持将不同类型的参数放入数组并进行处理。你可以根据你的需求选择适合的方式。

2. 折叠表达式展开

利用折叠表达式来展开参数包是一种高级技巧,它可以在处理可变参数模板时简化代码。
下面是一个示例:
  1. #include <iostream>
  2. // 使用逗号表达式展开参数包
  3. template<typename... Args>
  4. void ShowList(Args... args) {
  5.     (std::cout << ... << args) << std::endl;
  6. }
  7. int main() {
  8.     ShowList(1, 2.5, "Hello", 'A');  // 调用示例
  9.     return 0;
  10. }
复制代码

逗号表达式与折叠表达式的结合

在更传统的环境下,逗号表达式通常与初始化列表一起利用来展开参数包:
  1. #include <iostream>
  2. // 使用逗号表达式和初始化列表展开参数包
  3. template<typename... Args>
  4. void ShowList(Args... args) {
  5.     (void)std::initializer_list<int>{(std::cout << args << " ", 0)...};
  6.     std::cout << std::endl;
  7. }
  8. int main() {
  9.     ShowList(1, 2.5, "Hello", 'A');  // 调用示例
  10.     return 0;
  11. }
复制代码
表明


总结


这两种方法都可以有效地展开参数包,并实行所需的操作。根据你的编译器支持环境,你可以选择此中一种方式来利用。

3. emplace_back


在C++11中,STL的容器加入了emplace系列的接口,支持模板的可变参数

留意:此处的"&&"表示的是万能引用,详细可见上一篇文章(链接)


在 C++ 中,std::list 中的 push_back 和 emplace_back 是用于向列表的末端添加元素的两个函数,但它们在利用方式和效率上有一些紧张的区别。
1. push_back


2. emplace_back


当你需要向列表中添加对象,而且该对象的构造过程较为复杂或你渴望避免不必要的拷贝时,emplace_back 是更好的选择。如果你已经有一个现成的对象,而且只是需要将它添加到列表中,那么利用 push_back 也是完全可以的。
传参数包

当你利用参数包通报给 push_back 和 emplace_back 时,两者的活动仍然有所不同,特殊是当你处理参数包中的多个参数时。
1. push_back 和参数包

push_back 不能直接接受参数包,因为 push_back 只接受一个已经构造好的对象。如果你通报参数包给 push_back,首先你需要将参数包展开并用于构造对象,然后将这个构造好的对象通报给 push_back。
示例:
  1. #include <list>
  2. #include <string>
  3. template <typename T, typename... Args>
  4. void addToList(std::list<T>& lst, Args&&... args) {
  5.     lst.push_back(T(std::forward<Args>(args)...));
  6. }
  7. int main() {
  8.     std::list<std::string> myList;
  9.     addToList(myList, "Hello", 5, 'a'); // 传递给std::string的构造函数
  10.     return 0;
  11. }
复制代码
2. emplace_back 和参数包

emplace_back 可以直接接受参数包并将其转发给对象的构造函数。因此,当你通报参数包给 emplace_back 时,它会将这些参数直接用于在容器中构造对象,而不会进行多余的拷贝或移动操作。
示例:
  1. #include <list>
  2. #include <string>
  3. template <typename T, typename... Args>
  4. void emplaceToList(std::list<T>& lst, Args&&... args) {
  5.     lst.emplace_back(std::forward<Args>(args)...);
  6. }
  7. int main() {
  8.     std::list<std::string> myList;
  9.     emplaceToList(myList, "Hello", 5, 'a'); // 在容器中直接构造std::string
  10.     return 0;
  11. }
复制代码
3. 区别总结


4. 实际应用


这样做的好处在于利用 emplace_back 时,你可以省去中央对象的创建,直接在容器中进行对象的构造,尤其是在处理参数包时,emplace_back 能够让代码更具表现力和效率。
可变参数模板非常强盛,但也可能让代码变得复杂,以是在利用时需要小心。如果学会了就会对代码的理解有进了一步,恭喜你~ 如果文章对你有帮助的话不妨点个赞。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4