一、可变参数模板(Variadic Templates)

1.1 基本语法及原理

1.1.1 什么是可变参数模板?

C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板可变数目的参数被称为参数包(parameter pack)。

存在两种参数包:

参数包类型 表示 声明方式
模板参数包 零或多个模板参数 template <class... Args>
函数参数包 零或多个函数参数 void Func(Args... args)

1.1.2 基本语法

// 模板参数包 + 函数参数包(万能引用版本)
template <class... Args>
void Func(Args... args) {}          // 值传递

template <class... Args>
void Func(Args&... args) {}         // 左值引用

template <class... Args>
void Func(Args&&... args) {}        // 万能引用(推荐)

语法要点

  • class...typename... 表示接下来的参数是零或多个类型列表

  • 类型名后面跟 ... 表示接下来的参数是零或多个形参对象列表

  • 函数参数包可以用左值引用或右值引用表示,实例化时遵循引用折叠规则

1.1.3 计算参数包个数

使用 sizeof... 运算符:

1.1.4 编译原理

可变参数模板的本质跟普通模板一样,编译时实例化对应类型和个数的多个函数:

// 编译器实际生成的函数
void Print();                                       // Print()
void Print(int&& arg1);                             // Print(1)
void Print(int&& arg1, string&& arg2);              // Print(1, string("xxxxx"))
void Print(double&& arg1, string&& arg2, double& arg3); // Print(1.1, string("xxxxx"), x)


对比:没有可变参数模板时的写法

void Print();

template <class T1>
void Print(T1&& arg1);

template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);

template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);

// ... 需要无穷多个重载!

1.2 包扩展(Pack Expansion)

1.2.1 什么是包扩展?

对于一个参数包,除了计算参数个数,唯一能做的事情就是扩展它

扩展一个包 = 将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。

通过在模式的右边放一个省略号 ... 来触发扩展操作。

1.2.2 编译时递归推导

// 终止条件:参数包为 0 个时
void ShowList() {
    cout << endl;
}

// 递归展开:每次取第一个参数,剩余继续递归
template <class T, class... Args>
void ShowList(T x, Args... args) {
    cout << x << " ";
    ShowList(args...);  // 包扩展:args... 展开为剩余参数
}

// 入口函数
template <class... Args>
void Print(Args... args) {
    ShowList(args...);
}

int main() {
    Print();                           // 输出空行
    Print(1);                          // 输出 "1 "
    Print(1, string("xxxxx"));         // 输出 "1 xxxxx "
    Print(1, string("xxxxx"), 2.2);    // 输出 "1 xxxxx 2.2 "
    return 0;
}

1.2.3 编译器推导过程

Print(1, string("xxxxx"), 2.2) 为例,编译器递归推导:

1.2.4 更复杂的包扩展模式

直接将参数包依次展开作为实参给函数处理:

template <class T>
const T& GetArg(const T& x) {
    cout << x << " ";
    return x;
}

template <class... Args>
void Arguments(Args... args) {}

template <class... Args>
void Print(Args... args) {
    // GetArg 必须返回对象,才能组成参数包给 Arguments
    Arguments(GetArg(args)...);
}

int main() {
    Print(1, string("xxxxx"), 2.2);
    return 0;
}

编译器展开后的等效代码

1.3 emplace 系列接口

1.3.1 接口定义

// emplace_back:在尾部直接构造元素
template <class... Args>
void emplace_back(Args&&... args);

// emplace:在指定位置直接构造元素
template <class... Args>
iterator emplace(const_iterator position, Args&&... args);

1.3.2 emplace vs push_back

特性 push_back emplace_back
功能 插入已有对象 直接构造对象
参数 对象或右值 构造对象的参数包
效率 可能多一次拷贝/移动 直接在容器内存上构造
推荐度 兼容旧代码 推荐使用

1.3.3 使用示例

#include <list>
using namespace std;

int main() {
    list<bit::string> lt;

    // 场景1:传左值 —— 跟 push_back 一样,走拷贝构造
    bit::string s1("111111111111");
    lt.emplace_back(s1);
    cout << "*********************************" << endl;

    // 场景2:传右值 —— 跟 push_back 一样,走移动构造
    lt.emplace_back(move(s1));
    cout << "*********************************" << endl;

    // 场景3:直接传构造参数 —— push_back 做不到!
    // 直接用 string 的构造参数构造 string,不创建临时对象
    lt.emplace_back("111111111111");
    cout << "*********************************" << endl;

    // pair 场景
    list<pair<bit::string, int>> lt1;

    // push_back 风格
    pair<bit::string, int> kv("苹果", 1);
    lt1.emplace_back(kv);           // 拷贝构造
    lt1.emplace_back(move(kv));      // 移动构造
    cout << "*********************************" << endl;

    // emplace 独有:直接传 pair 的构造参数
    lt1.emplace_back("苹果", 1);      // 直接在节点内存上构造 pair
    cout << "*********************************" << endl;

    return 0;
}

1.3.4 模拟实现 list 的 emplace

ListNode 支持可变参数构造

namespace bit {
template<class T>
struct ListNode {
    ListNode<T>* _next;
    ListNode<T>* _prev;
    T _data;

    // 移动构造版本
    ListNode(T&& data)
        : _next(nullptr), _prev(nullptr), _data(move(data)) {}

    // 可变参数构造版本 —— 核心!
    template <class... Args>
    ListNode(Args&&... args)
        : _next(nullptr), _prev(nullptr), 
          _data(std::forward<Args>(args)...) {}  // 完美转发参数包
};
}

list 的 emplace_back 实现

template<class T>
class list {
    // ...

    // emplace_back:将参数包完美转发给 insert
    template <class... Args>
    void emplace_back(Args&&... args) {
        insert(end(), std::forward<Args>(args)...);
    }

    // insert 的万能引用版本
    template <class... Args>
    iterator insert(iterator pos, Args&&... args) {
        Node* cur = pos._node;
        // 关键:用参数包直接构造节点,不创建临时对象
        Node* newnode = new Node(std::forward<Args>(args)...);
        Node* prev = cur->_prev;

        // 链接节点
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;

        return iterator(newnode);
    }
};

编译器生成的等效代码(以 emplace_back("苹果", 1) 为例):

// 编译器根据调用生成对应函数
void emplace_back(const char* s, int n) {
    insert(end(), std::forward<const char*>(s), std::forward<int>(n));
}

// insert 展开
iterator insert(iterator pos, const char* s, int n) {
    Node* newnode = new Node(std::forward<const char*>(s), std::forward<int>(n));
    // ... 链接逻辑
}

// Node 构造展开
ListNode(const char* s, int n)
    : _next(nullptr), _prev(nullptr), 
      _data(std::forward<const char*>(s), std::forward<int>(n)) {}

最终效果pair<string, int> 直接在 list 节点内存上构造,零拷贝、零移动

1.3.5 完美转发参数包的必要性

// 错误写法:没有完美转发
template <class... Args>
void emplace_back(Args&&... args) {
    insert(end(), args...);  // 错误!args 是左值,右值引用变量表达式是左值
}

// 正确写法:完美转发
template <class... Args>
void emplace_back(Args&&... args) {
    insert(end(), std::forward<Args>(args)...);  // 保持原始值类别
}

原因Args&&... 是万能引用,实参是右值时,参数包中的变量表达式属性是左值。必须用 std::forward 保持原始属性,否则右值会变成左值,导致调用拷贝构造而非移动构造。

更多推荐