请添加图片描述

1.可变参数模板基础语法

1.1.核心概念定义

  • 可变参数模板:C++11中支持的可变参数模板,实际上就是支持可变数量的参数的函数模板或类模板
    形如:
//1.
template<class ...Args>
void Func(Arg... args){}
//2.
template<class ...Args>
void Func(Arg&... args) {}
//3.
template<class ...Args>
void Func(Arg&&... args) {}
  • 参数包:就是可变参数模板中可变数目的参数

其中,参数包又分为两种:
模板参数包:表示零或多个模板参数
函数参数包:表示零或多个函数参数

  • 省略号(…):在模板或函数参数中,我们用省略号来说明这个参数是一个包

在模板参数列表中,class…或typename…后面出现的参数就是参数包,表示零或多个类型列表
在函数参数列表中,类型名后面跟…指出接下来表示零或多个形参对象列表
函数参数包可以用左值引用,也可以用右值引用表示,实例化时遵循引用折叠原则

1.2.基本语法格式

  • 在函数模板中的声明方式:template <class... Args> 返回类型 函数名(Args&&... args)
  • 在类模板中的声明方式:template <class... Args> class 类名 {}
  • 参数包大小的获取:可以通过sizeof...(参数包名称)来获取参数包大小
  • 空参数包支持:实例化时参数包不传值是合法的
#include <iostream>
#include <string>

// 1. 函数模板:按指定声明方式实现,支持空参数包
template <class... Args>
void FuncTemplate(Args&&... args) {
    // 仅验证参数包特性:打印参数个数(空包时为0)
    std::cout << "函数模板 - 参数个数:" << sizeof...(args) << std::endl;
}

// 2. 类模板:按指定声明方式实现,支持空参数包
template <class... Args>
class ClassTemplate {
public:
    // 构造函数接收参数包,验证类模板的空包与多包支持
    ClassTemplate(Args&&... args) {
        std::cout << "类模板 - 构造函数参数个数:" << sizeof...(args) << std::endl;
    }
};

int main() {
    // 3. 空参数包验证:实例化/调用时不传实参(合法)
    std::cout << "=== 空参数包测试 ===" << std::endl;
    FuncTemplate();                          // 函数模板空包调用
    ClassTemplate<> classEmpty;              // 类模板空包实例化(无构造实参)

    // 补充:多参数包调用(验证非空场景与空包对比)
    std::cout << "\n=== 多参数包测试(对比用) ===" << std::endl;
    FuncTemplate(10, "hello", 3.14);         // 函数模板多参数调用
    ClassTemplate<int, std::string> classMulti(20, "world"); // 类模板多参数实例化

    return 0;
}

运行结果:
在这里插入图片描述

2.可变参数模板核心原理

2.1.实例化时的编译器自动优化

在实例化可变参数模板时,编译器会根据传入实参的类型和数目,自动生成对应版本的函数或类,不用手动写重载

#include <iostream>
using namespace std;

template <class... Args>
void PrintArgCount(Args&&... args)//万能引用
{
    cout << "参数个数:" << sizeof...(args) << endl;
}

int main()
{
    PrintArgCount(10);                // 调用1:1个int实参
    PrintArgCount(20, "hello");       // 调用2:int + const char*实参
    PrintArgCount(3.14, 'a', true);  // 调用3:double + char + bool实参
    return 0;
}

2.2.包扩展

  • 由于参数包是一个不能直接访问的列表,其中包含了若干参数,所以我们必须把它拆成一个一个的单参数来访问,我们把这个过程称为“包扩展”
  • nm其中最常用的扩展方式为递归扩展:通过递归函数每次提取参数包中的第一个参数,剩余参数重复执行递归函数,直到参数包为空。
#include <iostream>
#include <string>
using namespace std;

// 1. 递归终止条件:参数包为空时调用(无参数版本)
void PrintArgs()
{
    cout << endl;  // 所有参数打印完,换行
}

// 2. 递归版本:提取第一个参数,扩展剩余参数
template <class T, class... Args>
void PrintArgs(T first, Args&&... rest)
{
    // 打印第一个参数
    cout << first << " ";
    // 递归调用:扩展剩余参数(rest... 拆分为单个参数)
    PrintArgs(forward<Args>(rest)...);  
}

int main()
{
    PrintArgs();  // 空参数包:仅换行
    PrintArgs(10, 20, 30);  // 打印:10 20 30 
    PrintArgs("C++11", "可变参数模板", 3.14);  // 打印:C++11 可变参数模板 3.14 

    return 0;
}

注意:
1.必须定义无参版本的函数,否则参数包为空时没有函数可以匹配,编译报错
2.完美转发forward<Args>(rest)...用来保留剩余参数的左右值属性,避免调用递归函数传参时剩余参数rest…属性失效(变量表达式属性是左值)

3.可变参数模板的核心应用

3.1.STL容器中的emplace_back接口:

在C++11之前,容器中的push_back每次调用都需要通过临时对象来拷贝/移动构造,而C++11中新增的emplace_back接口,可以通过可变参数模板直接在容器中构造对象,避免了中间过程,大大提高了效率

模拟实现list中的emplace_back

#include <iostream>
#include <utility>
#include <string>
using namespace std;

// 模拟 list 的节点类
template <class T>
struct ListNode
{
    T _data;
    ListNode* _next;
    ListNode* _prev;

    // 节点构造函数:支持可变参数(直接构造 T 对象)
    template <class... Args>
    ListNode(Args&&... args)
        : _data(forward<Args>(args)...)  // 用参数包构造 T 对象
        , _next(nullptr)
        , _prev(nullptr)
    {
        cout << "ListNode 构造:直接构造 T 对象" << endl;
    }
};

// 模拟 list 类
template <class T>
class List
{
private:
    ListNode<T>* _head;  // 哨兵节点

public:
    List()
    {
        // 初始化哨兵节点
        _head = new ListNode<T>();  // 空参数包:调用 T 的默认构造
        _head->_next = _head;
        _head->_prev = _head;
    }

    // emplace_back:接收 T 构造所需的可变参数
    template <class... Args>
    void emplace_back(Args&&... args)
    {
        // 1. 创建新节点:传递参数包,直接构造 T 对象
        ListNode<T>* newNode = new ListNode<T>(forward<Args>(args)...);
        // 2. 将新节点插入链表尾部(简化逻辑)
        ListNode<T>* tail = _head->_prev;
        tail->_next = newNode;
        newNode->_prev = tail;
        newNode->_next = _head;
        _head->_prev = newNode;
    }

    ~List()
    {
        // 析构逻辑(简化)
        ListNode<T>* cur = _head->_next;
        while (cur != _head)
        {
            ListNode<T>* next = cur->_next;
            delete cur;
            cur = next;
        }
        delete _head;
    }
};

// 测试用的自定义类
class Person
{
public:
    string _name;
    int _age;

    // 默认构造函数
    Person() : _name(""), _age(0) {
        cout << "Person 默认构造" << endl;
    }

    // 有参构造函数
    Person(string name, int age)
        : _name(name)
        , _age(age)
    {
        cout << "Person 有参构造:" << _name << " " << _age << endl;
    }
};

int main()
{
    List<Person> lst;
    // emplace_back:直接传递 Person 的构造参数,无临时对象
    lst.emplace_back("张三", 20);

    return 0;
}

运行结果:
在这里插入图片描述

3.2.封装通用的“参数转发”函数

可变参数模板结合完美转发,可以实现通用的“参数转发”函数,这样能避免传参实例化时,左右值属性的丢失

#include <iostream>
#include <utility>
#include <string>
using namespace std;

// 底层目标函数:打印参数
void TargetFunc(int a, const string& b, double&& c)
{
    cout << "TargetFunc:" << a << " " << b << " " << c << endl;
}

// 通用转发函数:接收可变参数,转发给 TargetFunc
template <class... Args>
void ForwardFunc(Args&&... args)
{
    // 完美转发:保留参数的左/右值属性
    TargetFunc(forward<Args>(args)...);
}

int main()
{
    int a = 10;
    string b = "hello";
    // 调用转发函数:a是左值,b是左值,1.23是右值
    ForwardFunc(a, b, 1.23);

    return 0;
}

运行结果:
在这里插入图片描述

4.注意事项

  • 空参数包的显式实例化:类模板空参数包必须显式写为ClassName<>,不能省略<>,函数模板的空参数包可以直接调用(Func()
  • 包扩展必须有模式:不能直接写args...,否则会编译报错,必须配合具体操作,如:Print(args)..., forward<Args>(arg)...
  • 递归终止条件不能少:使用递归扩展参数包时,必须声明无参版本的终止函数,否则参数包为空时就找不到对应的类/函数了
  • 完美转发的必要性:参数包扩展时,为了保证参数左右值属性不丢失,必须使用完美转发std::forward, 否则右值参数会被转化为左值,影响移动语义等特性
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐