目录
[一、C++11 类型分类:重新认识左值与右值](#一、C++11 类型分类:重新认识左值与右值)
[1.1 为什么需要重新分类?](#1.1 为什么需要重新分类?)
[1.2 C++11 的 value categories(值类别)](#1.2 C++11 的 value categories(值类别))
[1.2.1 纯右值(prvalue, Pure Rvalue)](#1.2.1 纯右值(prvalue, Pure Rvalue))
[1.2.2 将亡值(xvalue, Expiring Value)](#1.2.2 将亡值(xvalue, Expiring Value))
[1.2.3 左值(lvalue, Locator Value)](#1.2.3 左值(lvalue, Locator Value))
[1.2.4 总结:值类别的核心区别](#1.2.4 总结:值类别的核心区别)
[二、引用折叠:C++11 的 "引用魔术"](#二、引用折叠:C++11 的 "引用魔术")
[2.1 为什么需要引用折叠?](#2.1 为什么需要引用折叠?)
[2.2 引用折叠的核心规则](#2.2 引用折叠的核心规则)
[2.3 引用折叠的应用场景](#2.3 引用折叠的应用场景)
[2.3.1 模板中的引用折叠](#2.3.1 模板中的引用折叠)
[2.3.2 typedef/using 中的引用折叠](#2.3.2 typedef/using 中的引用折叠)
[2.3.3 引用折叠的注意事项](#2.3.3 引用折叠的注意事项)
[3.1 为什么需要完美转发?](#3.1 为什么需要完美转发?)
[3.2 完美转发的实现:std::forward](#3.2 完美转发的实现:std::forward)
[3.3 完美转发的代码示例](#3.3 完美转发的代码示例)
[3.4 完美转发的应用场景](#3.4 完美转发的应用场景)
[3.4.1 容器的 emplace 系列接口](#3.4.1 容器的 emplace 系列接口)
[3.4.2 工厂函数](#3.4.2 工厂函数)
[3.4.3 回调函数封装](#3.4.3 回调函数封装)
[3.5 完美转发的注意事项](#3.5 完美转发的注意事项)
[四、可变参数模板:C++11 的 "参数魔法"](#四、可变参数模板:C++11 的 "参数魔法")
[4.1 可变参数模板的基本语法及原理](#4.1 可变参数模板的基本语法及原理)
[4.1.1 基本语法](#4.1.1 基本语法)
[4.1.2 核心原理](#4.1.2 核心原理)
[4.1.3 参数包的基本操作](#4.1.3 参数包的基本操作)
[4.2 包扩展的高级用法](#4.2 包扩展的高级用法)
[4.2.1 表达式中的包扩展](#4.2.1 表达式中的包扩展)
[4.2.2 函数调用中的包扩展](#4.2.2 函数调用中的包扩展)
[4.2.3 类型中的包扩展](#4.2.3 类型中的包扩展)
[4.3 emplace 系列接口:可变参数模板的经典应用](#4.3 emplace 系列接口:可变参数模板的经典应用)
[4.3.1 emplace 系列接口的语法](#4.3.1 emplace 系列接口的语法)
[4.3.2 emplace 与 push_back 的区别](#4.3.2 emplace 与 push_back 的区别)
[4.3.3 emplace 系列接口的优势场景](#4.3.3 emplace 系列接口的优势场景)
[4.3.4 emplace 系列接口的实现原理(模拟)](#4.3.4 emplace 系列接口的实现原理(模拟))
前言
在 C++ 的发展历程中,C++11 无疑是一座里程碑。它不仅修复了 C++98/03 中的诸多痛点,更引入了一系列革命性的特性,彻底改变了 C++ 的编程范式。其中,类型分类、引用折叠、完美转发和可变参数模板这几个特性,堪称 C++11 泛型编程的 "四大基石"------ 它们相互关联、层层递进,为后续的高效代码编写、容器优化、函数封装提供了强大的语法支撑。
本文将从实际应用出发,结合底层原理和代码示例,手把手带大家吃透这些核心特性。无论你是 C++ 新手想要系统入门,还是有经验的开发者希望查漏补缺,相信都能从中有所收获。下面就让我们正式开始吧!
一、C++11 类型分类:重新认识左值与右值
上一篇C++11的博客中,我为大家详细介绍了左值和右值的概念,下面就让我们先来复习一下。
在 C++98 中,我们对左值和右值的认知非常简单:能放在赋值符号左边的是左值,只能放在右边的是右值。但这种朴素的理解,在 C++11 引入移动语义后已经不够用了。C++11 对值类型进行了更精细的划分,这是后续所有特性的基础。
1.1 为什么需要重新分类?
C++98 的左值右值划分,只能满足简单的赋值场景。但当我们面临 "临时对象的资源如何高效复用" 这个问题时,旧的分类就显得力不从心了。比如函数返回的临时字符串对象,在 C++98 中只能通过拷贝构造传递给接收者,造成了不必要的性能开销。
C++11 的类型分类,核心目的是区分 "可复用资源的对象" 和 "不可复用的纯数据",从而为移动语义铺路 ------ 让编译器知道哪些对象的资源可以 "窃取",哪些只能老老实实拷贝。
1.2 C++11 的 value categories(值类别)
C++11 将所有表达式的结果分为三大类:泛左值(glvalue)、纯右值(prvalue)和将亡值(xvalue),它们的关系可以用一句话概括:泛左值 = 左值 + 将亡值;右值 = 纯右值 + 将亡值。

1.2.1 纯右值(prvalue, Pure Rvalue)
纯右值是最 "纯粹" 的值,它们要么是字面量常量,要么是求值后产生的不具名临时对象,核心特征是不可寻址、无持久状态。
常见的纯右值包括:
- 字面量常量:
42、3.14、true、nullptr;- 表达式求值结果:
a + b、x * y、fmin(1.5, 2.5);- 传值返回的函数调用:
string("hello")、add(1,2)(假设 add 返回 int);- 非类型模板参数:
template<int N> class A {};中的N。
代码示例:
cpp
#include <iostream>
#include <string>
using namespace std;
int add(int a, int b) {
return a + b; // 返回的是纯右值
}
int main() {
int a = 10, b = 20;
// 以下都是纯右值,无法取地址
42; // 字面量纯右值
a + b; // 表达式求值纯右值
add(3, 4); // 传值返回纯右值
string("临时字符串"); // 临时对象纯右值
// 以下代码都会编译报错:无法取纯右值的地址
// cout << &42 << endl;
// cout << &(a + b) << endl;
// cout << &string("临时字符串") << endl;
return 0;
}
纯右值的生命周期很短,通常在当前表达式结束后就会被销毁,这也是为什么它们无法被取地址 ------ 编译器不需要为它们分配持久的内存空间(可能存储在寄存器中)。
1.2.2 将亡值(xvalue, Expiring Value)
将亡值是 C++11 新增的类型,也是最关键的类型。它代表那些即将被销毁、资源可以被安全窃取的对象,核心特征是 "可寻址但生命周期即将结束"。
常见的将亡值包括:
- 返回右值引用的函数调用:
std::move(a)的结果;- 转换为右值引用的表达式:
static_cast<string&&>(s);- 成员函数返回的右值引用:
string&& getTemp() { return string("xvalue"); }。
代码示例:
cpp
#include <iostream>
#include <string>
using namespace std;
string&& getXvalue() {
// 返回右值引用,结果是将亡值
return string("将亡值示例");
}
int main() {
string s = "原始字符串";
// std::move(s)将s转换为将亡值,s的资源即将被"窃取"
string s1 = move(s);
cout << "s1: " << s1 << endl; // 输出"将亡值示例"
cout << "s: " << s << endl; // s的资源已被转移,输出为空
// getXvalue()返回右值引用,结果是将亡值
string s2 = getXvalue();
cout << "s2: " << s2 << endl; // 输出"将亡值示例"
return 0;
}
将亡值的本质是**"被标记为可移动"** 的左值 ------ 它本身是有地址的(因为是对象),但编译器知道它即将被销毁,所以允许移动构造函数 "窃取" 它的资源(如内存、文件句柄等),从而避免拷贝开销。
1.2.3 左值(lvalue, Locator Value)
C++11 中的左值,更准确的定义是 "有明确存储地址、可长期存在的对象",核心特征是可寻址、有持久状态。
常见的左值包括:
- 变量名:
int a、string s、const double pi = 3.14;- 解引用指针:
*p;- 数组元素:
arr[0];- 字符串字面量(C 风格):
"hello"(注意:"hello"是 const char [] 类型,是左值);- 函数名:
add(函数指针是左值)。
代码示例:
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
int a = 10;
const int b = 20;
int* p = &a;
string s = "左值字符串";
// 以下都是左值,可以取地址
cout << &a << endl; // 变量a的地址
cout << &b << endl; // const左值也可寻址
cout << &*p << endl; // 解引用指针是左值
cout << &s[0] << endl; // 数组元素是左值
cout << &"hello" << endl; // C风格字符串字面量是左值
// 左值可以出现在赋值符号左边(非const)
a = 30;
// b = 40; // const左值不能赋值,但仍是左值
return 0;
}
需要特别注意的是:右值引用变量本身是左值 。比如int&& rr = 42;中,rr是右值引用变量,但它是可寻址的,所以是左值。这一点在后续的引用折叠和完美转发中非常重要。
1.2.4 总结:值类别的核心区别
| 值类别 | 核心特征 | 能否寻址 | 资源可复用 |
|---|---|---|---|
| 左值 | 持久状态 | 是 | 否(除非用 move 转换) |
| 将亡值 | 即将销毁 | 是 | 是 |
| 纯右值 | 无持久状态 | 否 | 否 |
简单记:能取地址的是泛左值,不能取地址的是纯右值;泛左值中即将销毁的是将亡值。
二、引用折叠:C++11 的 "引用魔术"
C++ 中不允许直接定义 "引用的引用"(如int& &r = a;会编译报错),但在模板或 typedef 中,通过类型推导可能会间接产生 "引用的引用"。C++11 引入引用折叠规则,就是为了解决这个问题,同时为完美转发和可变参数模板提供语法支持。
2.1 为什么需要引用折叠?
在模板编程中,我们经常需要编写能同时接收左值和右值的函数。比如:
cpp
template <typename T>
void func(T&& param) {
// 处理param
}
如果没有引用折叠规则,当我们传入左值int a = 10; func(a);时,模板参数T会被推导为int&,此时param的类型就变成了int& &&------ 这是 "引用的引用",直接编译报错。
引用折叠规则的出现,就是为了让这种 "引用的引用" 能够合法地转换为单一引用类型,从而让模板函数能够同时兼容左值和右值参数。
2.2 引用折叠的核心规则
引用折叠的规则非常简单,只有两条,记住就能灵活运用:
- 右值引用的右值引用折叠为右值引用(T&& && → T&&);
- 所有其他组合(左值引用 + 左值引用、左值引用 + 右值引用、右值引用 + 左值引用)都折叠为左值引用(T& & → T&、T& && → T&、T&& & → T&)。
可以用一句话概括:只有 "&& + &&" 才会折叠为 &&,其余全是 &。
2.3 引用折叠的应用场景
引用折叠主要应用在模板推导和 typedef/using 类型定义中,下面通过具体例子详细说明。
2.3.1 模板中的引用折叠
这是引用折叠最常见的场景,也是实现 "万能引用" 的基础。**万能引用(Universal Reference)指的是T&&**形式的模板参数,它能同时接收左值和右值,本质就是通过引用折叠实现的。
代码示例:
cpp
#include <iostream>
#include <type_traits>
using namespace std;
// 万能引用模板
template <typename T>
void func(T&& param) {
cout << "param类型:";
if (is_lvalue_reference_v<decltype(param)>) {
cout << "左值引用" << endl;
} else if (is_rvalue_reference_v<decltype(param)>) {
cout << "右值引用" << endl;
} else {
cout << "非引用" << endl;
}
}
int main() {
int a = 10;
const int b = 20;
// 场景1:传入左值a,T推导为int&
// param类型为int& && → 折叠为int&(左值引用)
func(a); // 输出:param类型:左值引用
// 场景2:传入const左值b,T推导为const int&
// param类型为const int& && → 折叠为const int&(左值引用)
func(b); // 输出:param类型:左值引用
// 场景3:传入右值10,T推导为int
// param类型为int&& → 无折叠(右值引用)
func(10); // 输出:param类型:右值引用
// 场景4:传入move(a)(将亡值,右值),T推导为int
// param类型为int&& → 无折叠(右值引用)
func(move(a)); // 输出:param类型:右值引用
return 0;
}
从例子中可以看到,万能引用T&&通过引用折叠,实现了对左值、const 左值、右值的全面兼容,这是 C++11 模板编程的一大突破。
2.3.2 typedef/using 中的引用折叠
在 typedef 或 using定义类型时,也可能出现引用折叠的情况。比如:
cpp
#include <iostream>
#include <type_traits>
using namespace std;
int main() {
typedef int& LRef;
typedef int&& RRef;
// 以下都是引用折叠的情况
LRef& r1 = a; // LRef& → int& & → 折叠为int&(左值引用)
LRef&& r2 = a; // LRef&& → int& && → 折叠为int&(左值引用)
RRef& r3 = a; // RRef& → int&& & → 折叠为int&(左值引用)
RRef&& r4 = 10; // RRef&& → int&& && → 折叠为int&&(右值引用)
// 验证类型
cout << boolalpha;
cout << is_same_v<decltype(r1), int&> << endl; // true
cout << is_same_v<decltype(r2), int&> << endl; // true
cout << is_same_v<decltype(r3), int&> << endl; // true
cout << is_same_v<decltype(r4), int&&> << endl; // true
return 0;
}
这种场景虽然不如模板中常用,但在复杂类型定义中可能会遇到,理解引用折叠规则能避免类型推导错误。
2.3.3 引用折叠的注意事项
- 引用折叠只发生在模板推导或typedef/using 类型定义中,直接定义 "引用的引用" 会编译报错。
- 右值引用变量本身是左值,所以int&& rr = 10; func(rr);中,
rr是左值,T会推导为int&,param类型折叠为int&。 - 引用折叠是编译期行为,不影响运行时性能。
三、完美转发:传递参数的 "无损转发"
完美转发(Perfect Forwarding)是 C++11 引入的另一个核心特性,它的目的是在函数模板中,将参数原封不动地转发给另一个函数------ 这里的 "原封不动" 包括参数的类型(左值 / 右值)、const 属性等。
完美转发通常和万能引用、引用折叠配合使用,是实现高效模板函数的关键。
3.1 为什么需要完美转发?
在没有完美转发之前,模板函数中传递参数时,会丢失参数的原始类型信息。比如:
cpp
#include <iostream>
using namespace std;
void process(int& x) {
cout << "处理左值:" << x << endl;
}
void process(int&& x) {
cout << "处理右值:" << x << endl;
}
template <typename T>
void func(T&& param) {
// 此处param是左值(无论传入的是左值还是右值)
process(param);
}
int main() {
int a = 10;
func(a); // 传入左值,期望调用process(int&) → 实际调用process(int&)(正确)
func(20); // 传入右值,期望调用process(int&&) → 实际调用process(int&)(错误)
return 0;
}
为什么会出现这种错误?因为右值引用变量本身是左值 。当我们传入右值20时,param的类型是int&&(右值引用),但param是一个变量,有地址,所以是左值。因此**process(param)**会匹配左值引用版本的process,而不是我们期望的右值引用版本。
这就是完美转发要解决的问题:如何在转发参数时,保留参数的原始类型(左值 / 右值)。
3.2 完美转发的实现:std::forward
C++11 提供了std::forward函数模板,定义在**<utility>**头文件中,它的作用是根据参数的原始类型,将参数转发为左值或右值。
std::forward的核心原理是结合引用折叠和模板推导,其简化实现如下:
cpp
template <typename T>
T&& forward(typename remove_reference_t<T>& arg) noexcept {
// 将左值转发为左值或右值
return static_cast<T&&>(arg);
}
template <typename T>
T&& forward(typename remove_reference_t<T>&& arg) noexcept {
// 确保只有右值才能被转发为右值
static_assert(!is_lvalue_reference_v<T>, "forwarding a rvalue as lvalue");
return static_cast<T&&>(arg);
}
std::forward的使用规则是很简单的:对于万能引用参数T&& param,使用std::forward<T>(param)进行转发。
3.3 完美转发的代码示例
下面修改之前的代码,使用std::forward实现完美转发:
cpp
#include <iostream>
#include <utility>
using namespace std;
void process(int& x) {
cout << "处理左值:" << x << endl;
}
void process(int&& x) {
cout << "处理右值:" << x << endl;
}
template <typename T>
void func(T&& param) {
// 完美转发:保留param的原始类型
process(forward<T>(param));
}
int main() {
int a = 10;
func(a); // 传入左值,T推导为int&,forward<int&>(param) → 左值引用
// 调用process(int&) → 输出:处理左值:10(正确)
func(20); // 传入右值,T推导为int,forward<int>(param) → 右值引用
// 调用process(int&&) → 输出:处理右值:20(正确)
const int b = 30;
func(b); // 传入const左值,T推导为const int&,forward<const int&>(param) → const左值引用
// 若没有process(const int&)重载,会编译报错(符合预期)
return 0;
}
运行结果完全符合预期!std::forward成功保留了参数的原始类型,实现了完美转发。
3.4 完美转发的应用场景
完美转发在模板编程中应用广泛,尤其是在容器的 emplace 系列接口、工厂函数、回调函数封装等场景中。
3.4.1 容器的 emplace 系列接口
C++11 容器新增的emplace_back、emplace等接口,就是通过完美转发实现的。它们能直接在容器中构造对象,避免临时对象的拷贝开销。
3.4.2 工厂函数
工厂函数是创建对象的通用接口,通过完美转发,工厂函数可以接收任意类型的构造参数,并转发给对象的构造函数。
代码示例:
cpp
#include <iostream>
#include <utility>
#include <string>
using namespace std;
class Person {
public:
Person(string name, int age) {
cout << "构造函数:name=" << name << ", age=" << age << endl;
}
Person(const Person& other) {
cout << "拷贝构造函数" << endl;
}
Person(Person&& other) {
cout << "移动构造函数" << endl;
}
};
// 工厂函数:通过完美转发创建Person对象
template <typename... Args>
Person createPerson(Args&&... args) {
// 完美转发参数给Person的构造函数
return Person(forward<Args>(args)...);
}
int main() {
// 传入右值,调用构造函数(无拷贝)
Person p1 = createPerson("张三", 20);
// 输出:构造函数:name=张三, age=20
string name = "李四";
// 传入左值,调用构造函数(无拷贝)
Person p2 = createPerson(name, 25);
// 输出:构造函数:name=李四, age=25
// 传入将亡值,调用移动构造函数
Person p3 = createPerson(move(p1));
// 输出:移动构造函数
return 0;
}
3.4.3 回调函数封装
在封装回调函数时,完美转发可以保留回调函数参数的原始类型,提高代码的灵活性和效率。
代码示例:
cpp
#include <iostream>
#include <utility>
#include <functional>
using namespace std;
void callback(int& x) {
x += 10;
cout << "回调函数处理左值:x=" << x << endl;
}
void callback(int&& x) {
cout << "回调函数处理右值:x=" << x << endl;
}
// 封装回调函数调用
template <typename Func, typename... Args>
void invokeCallback(Func&& func, Args&&... args) {
// 完美转发参数给回调函数
func(forward<Args>(args)...);
}
int main() {
int a = 10;
invokeCallback(callback, a); // 传入左值,调用callback(int&)
// 输出:回调函数处理左值:x=20
cout << "a=" << a << endl; // a被修改为20
invokeCallback(callback, 20); // 传入右值,调用callback(int&&)
// 输出:回调函数处理右值:x=20
return 0;
}
3.5 完美转发的注意事项
- 完美转发仅适用于万能引用(T&&)参数,普通引用参数无法实现完美转发。
- std::forward的模板参数必须是模板推导的
T,不能手动指定其他类型,否则会导致转发错误。- 完美转发只能转发可移动的参数,对于不可移动的对象(如
const对象),会自动转为拷贝。
四、可变参数模板:C++11 的 "参数魔法"
在 C++11 之前,模板只能接收固定数量的参数。如果需要实现支持不同参数个数的函数(如printf),只能通过函数重载或宏定义,代码冗余且维护困难。C++11 引入的可变参数模板(Variadic Templates),允许模板接收任意数量、任意类型的参数,彻底解决了这个问题。
4.1 可变参数模板的基本语法及原理
4.1.1 基本语法
可变参数模板的语法非常简洁,核心是**...**(省略号)的使用,主要包括三个部分:
- 模板参数包(Template Parameter Pack):template <typename... Args>,表示零个或多个模板参数。
- 函数参数包(Function Parameter Pack):void func(Args&&... args),表示零个或多个函数参数。
- 包扩展(Pack Expansion):args...,用于将参数包展开为独立的参数。
语法示例:
cpp
// 可变参数模板函数
template <typename... Args> // 模板参数包:Args
void func(Args&&... args) { // 函数参数包:args
// 包扩展:将args展开为独立参数
process(forward<Args>(args)...);
}
4.1.2 核心原理
可变参数模板的本质是编译期代码生成------ 编译器会根据传入的参数个数和类型,自动实例化出对应的函数版本。
比如调用**func(1, "hello", 3.14)**时,编译器会实例化出:
cpp
void func(int&& arg1, string&& arg2, double&& arg3) {
process(forward<int>(arg1), forward<string>(arg2), forward<double>(arg3));
}
这种编译期实例化的方式,既保证了类型安全,又不会带来额外的运行时开销。
4.1.3 参数包的基本操作
对参数包的操作主要有两个:获取参数个数和包扩展。
(1)获取参数个数
使用**sizeof...(args)**可以获取函数参数包的参数个数,**sizeof...(Args)**可以获取模板参数包的参数个数(两者结果相同)。
代码示例:
cpp
#include <iostream>
using namespace std;
template <typename... Args>
void printArgsCount(Args&&... args) {
cout << "参数个数:" << sizeof...(args) << endl;
cout << "模板参数个数:" << sizeof...(Args) << endl;
}
int main() {
printArgsCount(); // 参数个数:0,模板参数个数:0
printArgsCount(1); // 参数个数:1,模板参数个数:1
printArgsCount(1, "hello"); // 参数个数:2,模板参数个数:2
printArgsCount(1, "hello", 3.14, true); // 参数个数:4,模板参数个数:4
return 0;
}
(2)包扩展
包扩展是可变参数模板的核心,通过**args...**将参数包展开为独立的参数。包扩展可以用于函数调用、表达式、类型定义等场景。
代码示例:
cpp
#include <iostream>
using namespace std;
// 递归终止函数:处理0个参数
void print() {
cout << endl;
}
// 可变参数模板函数:处理1个或多个参数
template <typename T, typename... Args>
void print(T&& first, Args&&... rest) {
// 打印第一个参数
cout << first << " ";
// 递归调用,处理剩余参数(包扩展)
print(forward<Args>(rest)...);
}
int main() {
print(1); // 输出:1
print(1, "hello"); // 输出:1 hello
print(1, "hello", 3.14, true); // 输出:1 hello 3.14 1
return 0;
}
这个例子通过递归实现了参数包的展开:
- 第一次调用
print(1, "hello", 3.14, true),first=1,rest={"hello", 3.14, true},打印1后递归调用print("hello", 3.14, true)。- 第二次调用
print("hello", 3.14, true),first="hello",rest={3.14, true},打印hello后递归调用print(3.14, true)。- 第三次调用
print(3.14, true),first=3.14,rest={true},打印3.14后递归调用print(true)。- 第四次调用
print(true),first=true,rest=,打印true后递归调用print()。- 调用
print()(递归终止函数),打印换行。
4.2 包扩展的高级用法
包扩展不仅可以用于递归展开,还可以结合表达式、函数调用、类型定义等场景,实现更复杂的功能。
4.2.1 表达式中的包扩展
可以在表达式中对参数包的每个元素进行操作,再展开为多个表达式。
代码示例:
cpp
#include <iostream>
using namespace std;
// 递归终止函数
int sum() {
return 0;
}
// 计算所有参数的和
template <typename T, typename... Args>
int sum(T&& first, Args&&... rest) {
// 表达式扩展:first + sum(rest...)
return first + sum(forward<Args>(rest)...);
}
int main() {
cout << sum(1, 2, 3) << endl; // 1+2+3=6
cout << sum(10, 20, 30, 40) << endl; // 10+20+30+40=100
cout << sum() << endl; // 0
return 0;
}
4.2.2 函数调用中的包扩展
可以将参数包展开后作为其他函数的参数,结合完美转发实现高效调用。
代码示例:
cpp
#include <iostream>
#include <utility>
using namespace std;
void printSingle(int x) {
cout << "int: " << x << " ";
}
void printSingle(const string& s) {
cout << "string: " << s << " ";
}
void printSingle(double d) {
cout << "double: " << d << " ";
}
// 可变参数模板函数:转发所有参数到printSingle
template <typename... Args>
void printAll(Args&&... args) {
// 包扩展:调用printSingle(forward<Args>(args)) for each args
(printSingle(forward<Args>(args)), ...);
cout << endl;
}
int main() {
printAll(1, "hello", 3.14);
// 输出:int: 1 string: hello double: 3.14
return 0;
}
这里的(printSingle(forward<Args>(args)), ...)是 C++17 引入的折叠表达式(Fold Expression),用于将参数包展开为逗号表达式序列。如果使用 C++11/14,需要通过递归实现类似功能。
4.2.3 类型中的包扩展
可以将模板参数包展开为多个类型,用于定义数组、元组等。
代码示例:
cpp
#include <iostream>
#include <tuple>
using namespace std;
// 可变参数模板:定义元组类型
template <typename... Args>
using Tuple = tuple<Args...>;
int main() {
Tuple<int, string, double> t1(1, "hello", 3.14);
cout << get<0>(t1) << endl; // 1
cout << get<1>(t1) << endl; // hello
cout << get<2>(t1) << endl; // 3.14
Tuple<> t2; // 空元组
cout << tuple_size_v<decltype(t2)> << endl; // 0
return 0;
}
4.3 emplace 系列接口:可变参数模板的经典应用
C++11 为 STL 容器新增了emplace_back、emplace等接口,它们是可变参数模板的经典应用。与push_back、insert相比,emplace系列接口能直接在容器中构造对象,避免临时对象的拷贝或移动开销,效率更高。
4.3.1 emplace 系列接口的语法
以vector为例,emplace_back的语法如下:
cpp
template <typename... Args>
void emplace_back(Args&&... args);
- Args&&... args:可变参数模板,接收对象构造所需的任意参数。
- 函数内部会通过**forward<Args>(args)...**将参数完美转发给对象的构造函数,直接在容器的内存空间中构造对象。
4.3.2 emplace 与 push_back 的区别
push_back接收的是对象本身(左值或右值),而emplace_back接收的是对象构造的参数。两者的核心区别在于:
- push_back:如果传入的是右值,会调用移动构造;如果传入的是左值,会调用拷贝构造。
- emplace_back:直接在容器中构造对象,无需创建临时对象,也无需调用拷贝或移动构造(除非参数是已存在的对象)。
代码示例:
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Person {
public:
Person(string name, int age) {
cout << "构造函数:name=" << name << ", age=" << age << endl;
}
Person(const Person& other) {
cout << "拷贝构造函数" << endl;
}
Person(Person&& other) {
cout << "移动构造函数" << endl;
}
};
int main() {
vector<Person> vec;
cout << "=== 使用push_back ===" << endl;
// push_back:先创建临时对象,再移动构造到容器中
vec.push_back(Person("张三", 20));
// 输出:构造函数 → 移动构造函数
cout << "=== 使用emplace_back ===" << endl;
// emplace_back:直接在容器中构造对象,无临时对象
vec.emplace_back("李四", 25);
// 输出:构造函数
return 0;
}
运行结果对比:
- push_back:创建临时对象(调用构造函数)→ 移动构造到容器(调用移动构造函数)→ 销毁临时对象。
- emplace_back:直接在容器的内存空间中调用构造函数创建对象,无临时对象,无移动 / 拷贝开销。
4.3.3 emplace 系列接口的优势场景
- 构造函数参数较多时:
emplace可以直接传递多个构造参数,无需手动创建临时对象。- 对象拷贝 / 移动开销较大时:如
string、vector等容器,emplace能避免拷贝 / 移动,显著提升性能。- 构造临时对象不方便时:如
pair、tuple等聚合类型,emplace可以直接传递成员的构造参数。
代码示例(pair 的 emplace):
cpp
#include <iostream>
#include <list>
#include <string>
using namespace std;
int main() {
list<pair<string, int>> lst;
cout << "=== 使用push_back ===" << endl;
// push_back:需要先创建pair临时对象
lst.push_back(pair<string, int>("苹果", 10));
// 输出:构造pair临时对象(隐式)
cout << "=== 使用emplace_back ===" << endl;
// emplace_back:直接传递pair的构造参数,在容器中构造pair
lst.emplace_back("香蕉", 20);
// 输出:直接构造pair,无临时对象
return 0;
}
4.3.4 emplace 系列接口的实现原理(模拟)
为了更好地理解emplace的工作原理,我们可以模拟实现一个简单的list容器,并添加emplace_back接口:
cpp
#include <iostream>
#include <utility>
#include <string>
using namespace std;
// 模拟实现list节点
template <typename T>
struct ListNode {
T _data;
ListNode* _prev;
ListNode* _next;
// 普通构造函数
ListNode(const T& data) : _data(data), _prev(nullptr), _next(nullptr) {
cout << "ListNode拷贝构造" << endl;
}
ListNode(T&& data) : _data(move(data)), _prev(nullptr), _next(nullptr) {
cout << "ListNode移动构造" << endl;
}
// 可变参数构造函数:用于emplace
template <typename... Args>
ListNode(Args&&... args) : _data(forward<Args>(args)...), _prev(nullptr), _next(nullptr) {
cout << "ListNode可变参数构造" << endl;
}
};
// 模拟实现list容器
template <typename T>
class List {
private:
ListNode<T>* _head;
ListNode<T>* _tail;
public:
List() {
_head = new ListNode<T>(T()); // 哨兵节点
_tail = _head;
}
// push_back:接收左值
void push_back(const T& data) {
ListNode<T>* newNode = new ListNode<T>(data);
// 插入到尾部(简化实现)
_tail->_next = newNode;
newNode->_prev = _tail;
_tail = newNode;
}
// push_back:接收右值
void push_back(T&& data) {
ListNode<T>* newNode = new ListNode<T>(move(data));
// 插入到尾部(简化实现)
_tail->_next = newNode;
newNode->_prev = _tail;
_tail = newNode;
}
// emplace_back:可变参数模板
template <typename... Args>
void emplace_back(Args&&... args) {
// 直接构造节点,参数完美转发给T的构造函数
ListNode<T>* newNode = new ListNode<T>(forward<Args>(args)...);
// 插入到尾部(简化实现)
_tail->_next = newNode;
newNode->_prev = _tail;
_tail = newNode;
}
};
// 测试类
class Person {
public:
Person(string name, int age) {
cout << "Person构造函数:" << name << "," << age << endl;
}
};
int main() {
List<Person> lst;
cout << "=== push_back ===" << endl;
lst.push_back(Person("张三", 20));
// 输出:Person构造函数 → ListNode移动构造
cout << "=== emplace_back ===" << endl;
lst.emplace_back("李四", 25);
// 输出:Person构造函数 → ListNode可变参数构造
return 0;
}
从模拟实现可以看出,emplace_back的核心是通过可变参数模板接收构造参数,再通过完美转发传递给节点的构造函数,最终直接在节点中构造T类型对象,避免了临时对象的创建。
总结
C++11 的类型分类、引用折叠、完美转发和可变参数模板,是相互关联的核心特性。它们共同构成了 C++ 泛型编程的基础,让开发者能够编写更高效、更灵活、更通用的代码。
正是这些特性,彻底改变了 C++ 的编程方式。只有深入理解它们的底层原理和应用场景,才能真正写出高效、优雅的 C++ 代码。希望本文能为大家打开 C++11 泛型编程的大门,祝大家在 C++ 的学习之路上越走越远!