【C++可变模板参数】

C++11 可变模板参数总结:搞懂参数包、包扩展和 emplace


1. 为什么 C++11 需要可变模板参数?

在 C++11 之前,如果我们想写一个"参数个数可变、参数类型也可变"的函数,基本只能靠:

  • 写很多重载
  • 或者用 ...(C 风格可变参数,类型不安全)

而可变模板参数(Variadic Templates)解决的是:

  • 参数个数可变
  • 参数类型可变
  • 编译期类型安全
  • 能和泛型、右值引用、完美转发组合

一句话:它是现代 C++ 泛型编程的基础设施。


2. 基础语法:模板参数包 & 函数参数包

2.1 模板参数包

cpp 复制代码
template<class... Args>

Args 代表"0 个或多个类型"。

2.2 函数参数包

cpp 复制代码
template<class... Args>
void Func(Args... args) {}

args 代表"0 个或多个函数参数"。

如果要支持完美转发,常见写法是:

cpp 复制代码
template<class... Args>
void Func(Args&&... args) {}

3. sizeof...:统计参数包个数

cpp 复制代码
#include <iostream>
using namespace std;

template<class... Args>
void PrintCount(Args&&... args)
{
    cout << sizeof...(args) << endl; // 参数个数
    //sizeof...(Args) 也可统计
}

int main()
{
    double x = 2.2;
    PrintCount();                         // 0
    PrintCount(1);                        // 1
    PrintCount(1, string("xxxxx"));       // 2
    PrintCount(1.1, string("xxxxx"), x);  // 3
}

4. 包扩展(最核心)

参数包不能像数组那样 args[i] 访问,能做的核心操作就是"展开(expand)"。

4.1 递归展开(C++11 经典写法)

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 递归终止
void ShowList()
{
    cout << endl;
}

// 每次取一个,剩下的继续递归
template<class T, class... Args>
void ShowList(T x, Args... args)
{
    cout << x << " ";
    ShowList(args...);
}

template<class... Args>
void Print(Args... args)
{
    ShowList(args...);
}

int main()
{
    Print();
    Print(1);
    Print(1, string("hello"));
    Print(1, string("hello"), 2.2);
}

这段代码的本质:编译器会在编译期"展开"为多组具体函数调用。

4.2 模式扩展:Func(args...) 不是唯一玩法

cpp 复制代码
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)
{
    Arguments(GetArg(args)...); // 包扩展模式:对每个 args 套一层 GetArg
}

关键理解:GetArg(args)... 等价于
GetArg(arg1), GetArg(arg2), GetArg(arg3)...


5. 可变模板参数 + 完美转发(高频考点)

5.1 为什么要 std::forward<Args>(args)...

因为:有名字的变量都是左值表达式

如果你只写 args...,右值信息会丢失,可能触发错误重载或性能下降。

cpp 复制代码
template<class... Args>
void Wrapper(Args&&... args)
{
    Target(std::forward<Args>(args)...); // 保留原始值类别
}

5.2 一句话区分

  • std::move(x):无脑转右值
  • std::forward<T>(x):按模板推导结果"有条件转发"

6. 真实价值场景:emplace 系列接口

emplace_back / emplace 本质是可变模板参数 + 完美转发。

cpp 复制代码
template<class... Args>
void emplace_back(Args&&... args);

template<class... Args>
iterator emplace(const_iterator pos, Args&&... args);

相比 push_back

  • push_back 往往是"先构造对象,再拷贝/移动进容器"
  • emplace_back 可以"直接把构造参数传到底层,在容器内存上原地构造"

示例理解:

cpp 复制代码
std::vector<std::pair<std::string, int>> v;
v.emplace_back("apple", 1); // 直接构造 pair<string,int>

这就是"参数包一路转发到节点/元素构造函数"的价值。


7. 可变模板参数常见坑

7.1 不能把参数包当数组用

错误思路:

cpp 复制代码
// args[i] // 不支持

参数包不是运行时容器,而是编译期语法结构。

7.2 忘记递归终止函数

递归展开必须有"空包终止版本",不然会无限模板实例化报错。

7.3 忘记完美转发导致右值退化

Args&&... args + 下层调用 foo(args...),会把右值当左值传下去。

7.4 const 对象即使 move 也不一定能移动

const T 通常无法真正"偷资源",很多时候仍走拷贝语义。

相关推荐
九转成圣2 小时前
Java 性能优化实战:如何将海量扁平数据高效转化为类目字典树?
java·开发语言·json
SmartRadio2 小时前
ESP32-S3 双模式切换实现:兼顾手机_路由器连接与WiFi长距离通信
开发语言·网络·智能手机·esp32·长距离wifi
laowangpython2 小时前
Rust 入门:GitHub 热门内存安全编程语言
开发语言·其他·rust·github
我叫汪枫2 小时前
在后台管理系统中,如何递归和选择保留的思路来过滤菜单
开发语言·javascript·node.js·ecmascript
_.Switch2 小时前
东方财富股票数据JS逆向:secids字段和AES加密实战
开发语言·前端·javascript·网络·爬虫·python·ecmascript
软件技术NINI2 小时前
webkit简介及工作流程
开发语言·前端·javascript·udp·ecmascript·webkit·yarn
Brendan_0012 小时前
JavaScript的Stomp.over
开发语言·javascript·ecmascript
念2342 小时前
f5 shape分析
开发语言·javascript·ecmascript
苍穹之跃2 小时前
某量JS逆向
开发语言·javascript·ecmascript
思茂信息2 小时前
CST软件如何进行参数化扫描?
运维·开发语言·javascript·windows·ecmascript·软件工程·软件需求