📑 C++ 演进系列总览 | C++11 | C++14 | C++17 | C++20 | C++23 | C++26

C++11 是 C++ 历史上最重要的标准之一,它彻底改变了 C++ 的编程方式,被称为"现代 C++"的起点。在 C++11 之前,C++98/03 沿用了十余年,语言表达力落后于时代。C++11 一次性引入了自动类型推导、Lambda、智能指针、移动语义、并发支持等重量级特性,使 C++ 摆脱了"带类的 C"的旧面貌,转向类型安全、资源安全、表达力强的现代风格。

自动类型推导(auto)

auto 让编译器从初始化表达式推导变量类型,取代冗长难读的类型名(如 std::vector<std::string>::const_iterator)。它不是动态类型——类型在编译期就已确定,只是省去手写。主要收益是可读性(关注"做什么"而非"是什么类型")和可维护性(重构时类型自动跟随)。

注意 auto 默认会剥离引用与 const:要从容器取可修改元素需写 auto&,只读用 const auto&。范围 for 循环与 auto 配合是惯用法。

1
2
3
4
5
6
7
8
9
auto x = 42;                        // int
auto y = 3.14; // double
auto z = std::vector<int>{1, 2, 3}; // std::vector<int>

// 范围 for 循环
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& elem : vec) {
elem *= 2;
}

陷阱:auto x = {1, 2, 3} 推导出 std::initializer_list<int>,与预期可能不符;函数返回 auto 要到 C++14 才支持。auto 适合类型名长、明显或无关紧要的场合,类型对理解逻辑重要时仍应显式写出。

Lambda 表达式

Lambda 让你在用到可调用对象的地方就地定义它,无需另起一个函数或仿函数。这在传给 STL 算法(std::sortstd::find_ifstd::for_each)时尤其有价值——以前要么写函数指针、要么定义仿函数类,都很啰嗦。

语法为 [捕获](参数) -> 返回类型 { 函数体 },返回类型可省略(由 return 推导)。捕获决定了 lambda 如何"看见"外部变量:值捕获拷贝一份、引用捕获持有引用。[=]/[&] 是按值/按引用捕获所有外部变量的简写。

1
2
3
4
5
6
7
8
9
10
11
12
// 基本语法
auto lambda = [](int x, int y) { return x + y; };
int result = lambda(3, 4); // 7

// 捕获变量
int a = 10;
auto captureByValue = [a](int x) { return x + a; };
auto captureByRef = [&a](int x) { a += x; };

// 捕获所有
auto captureAll = [=](int x) { return x + a; };
auto captureAllRef = [&](int x) { a += x; };

注意:引用捕获([&])要小心 lambda 生命周期超出被引用变量——如 lambda 存入容器后异步执行,引用的对象可能已销毁,导致悬挂引用。默认优先值捕获;确需引用且能保证生命周期时再用 [&]

智能指针

智能指针用 RAII 管理动态内存,析构自动释放,从根本上治理裸 new/delete 的内存泄漏与异常安全问题。C++11 引入了三种:

  • unique_ptr:独占所有权,不可拷贝、只能移动。零开销抽象(大小与裸指针相同,无引用计数开销)。默认选择。
  • shared_ptr:共享所有权,引用计数。多个 shared_ptr 可指向同一对象,最后一个销毁时释放。有原子计数开销。
  • weak_ptr:对 shared_ptr 的弱引用,不增加计数。用于打破循环引用、观察但不影响生命周期。
1
2
3
4
5
6
7
8
9
10
11
12
#include <memory>

// unique_ptr:独占所有权
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// std::unique_ptr<int> ptr2 = ptr1; // 编译错误

// shared_ptr:共享所有权
std::shared_ptr<int> ptr3 = std::make_shared<int>(42);
std::shared_ptr<int> ptr4 = ptr3; // 引用计数 +1

// weak_ptr:弱引用,不增加引用计数
std::weak_ptr<int> ptr5 = ptr3;

make_unique/make_shared 优先于裸 new:一次分配(make_shared 把对象和控制块合并到一次分配)、异常安全(避免 f(unique_ptr(new A), unique_ptr(new B)) 求值顺序导致的泄漏)。weak_ptr 使用前需 lock() 提升为 shared_ptr 检查对象是否仍存活。shared_ptr 控制块的原子操作有开销,热路径上应优先 unique_ptr

右值引用与移动语义

移动语义让"资源转移"成为一等操作:把昂贵的拷贝(如 vector 拷贝整个堆缓冲)换成廉价的指针交换。它区分了"拷贝"(复制底层资源,源对象不变)与"移动"(转移资源所有权,源对象置空)。

核心机制是右值引用 T&&——绑定到即将销毁的临时对象(右值)。std::move 是个无操作转换,把左值强转为右值引用,表示"我不再需要这个值,你可以掏空它"。移动构造/移动赋值用 T&& 接管资源后把源对象置为有效但未指定状态(通常置空)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 移动构造函数
class MyClass {
public:
MyClass(MyClass&& other) noexcept
: data_(std::move(other.data_)) {
other.data_ = nullptr;
}

// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete data_;
data_ = std::move(other.data_);
other.data_ = nullptr;
}
return *this;
}

private:
int* data_;
};

// std::move
std::string str1 = "Hello";
std::string str2 = std::move(str1); // str1 现在为空

关键点:移动操作应标记 noexcept——vector 扩容时只有 noexcept 移动才会真正移动,否则退化为拷贝以保证强异常安全。std::move 后不要再使用源对象(除赋值或销毁外)。返回局部变量时不要 std::move——会阻止 RVO。现代代码里手写移动构造已少见,编译器生成的版本(零规则)通常足够。

std::function 和 std::bind

std::function 是可调用对象的类型擦除包装器:能统一存储函数指针、仿函数、Lambda、成员函数指针等任何可调用对象,只要签名匹配。这让回调、事件处理、策略注入等场景可以把"任意可调用物"当一等对象传递。

代价是有一点运行期开销(类型擦除、可能的堆分配、间接调用),热路径上不如模板或直接 Lambda。C++14 起 Lambda 捕获初始化、C++23 std::move_only_function 进一步削弱了它的地位,但 std::function 仍是"存储任意回调"的标准选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <functional>

int add(int a, int b) { return a + b; }

// std::function:可调用对象的包装器
std::function<int(int, int)> func = add;
int result = func(3, 4); // 7

// std::bind:绑定参数
auto add5 = std::bind(add, std::placeholders::_1, 5);
int result2 = add5(10); // 15

// 绑定成员函数
struct Calculator {
int multiply(int a, int b) { return a * b; }
};
Calculator calc;
auto multiplyBy2 = std::bind(&Calculator::multiply, &calc, std::placeholders::_1, 2);
int result3 = multiplyBy2(5); // 10

std::bind 部分应用参数(柯里化),现代代码更倾向用 Lambda 替代——auto add5 = [](int x) { return add(x, 5); }; 更清晰、更易内联、不占额外类型。std::bind 在绑定重载函数或完美转发时容易踩坑,能不用就不用。

std::unordered_map 和 std::unordered_set

基于哈希表的容器,提供平均 O(1) 的查找/插入/删除。与之相对,std::map/std::set 基于红黑树,是有序的 O(log n)。选择标准:需要按键排序遍历时用 map/set,只需要快速查找且不关心顺序时用 unordered_map/unordered_set

哈希容器的代价:最坏情况 O(n)(哈希冲突,恶意输入可能触发)、无序遍历、对自定义类型需提供 std::hash 特化与 operator==。元素指针/引用在 rehash 时可能失效(不同于 map 的稳定指针)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unordered_map>
#include <unordered_set>

// 哈希表
std::unordered_map<std::string, int> scores;
scores["Alice"] = 90;
scores["Bob"] = 85;

// 查找
if (scores.find("Alice") != scores.end()) {
std::cout << "Alice's score: " << scores["Alice"] << "\n";
}

// unordered_set
std::unordered_set<int> uniqueValues = {1, 2, 2, 3, 3, 3};
// uniqueValues = {1, 2, 3}

自定义键类型需特化 std::hash<Key> 并提供 operator==;若想保留插入顺序可配合存一个 vector 记录键的顺序。operator[] 在键不存在时会插入默认值——只想查找时用 findat(后者不存在时抛异常)。

std::tuple

std::tuple 是异质值的固定大小容器:把不同类型的若干值打包成一个对象。相比自定义结构体,它无需命名字段、可用模板泛化处理任意类型组合,适合函数多返回值、临时聚合、编译期元编程。

访问用 std::get<I>(t)(按索引)或 std::get<T>(t)(按类型,要求唯一)。C++17 结构化绑定让它解包更优雅。std::tuple_cat 拼接多个 tuple。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <tuple>

// 创建 tuple
std::tuple<int, double, std::string> t{42, 3.14, "hello"};

// 获取元素
std::get<0>(t); // 42
std::get<1>(t); // 3.14
std::get<2>(t); // "hello"

// 结构化绑定(C++17)
auto [id, value, name] = t;

// 连接 tuple
auto combined = std::tuple_cat(t, std::make_pair(true, 'a'));

权衡:需要语义明确字段名时还是用结构体——point.xstd::get<0>(point) 可读得多。tuple 适合类型组合在编译期才能确定、或临时聚合无需命名的场合。

std::chrono

std::chrono 是类型安全的时间库,用编译期类型系统区分时间点(time_point)、时长(duration)与时钟(clock)。它消除了 C 风格 time_t/gettimeofday 的无类型算术与可移植性问题——不同单位的时长(秒、毫秒、纳秒)是不同类型,混算会编译报错而非静默出错。

三个时钟:system_clock(挂钟时间,可转 time_t)、steady_clock(单调递增,适合计时)、high_resolution_clock(最小分辨率,通常即 steady_clock)。C++14 起的 chrono_literals100ms2s)让字面量更直观。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <chrono>

// 时间点
auto now = std::chrono::system_clock::now();

// duration
using namespace std::chrono_literals;
auto duration = 100ms; // 100 毫秒
auto seconds = 2s; // 2 秒
auto minutes = 5min; // 5 分钟

// 睡眠
std::this_thread::sleep_for(500ms);

// 计时
auto start = std::chrono::high_resolution_clock::now();
// ... 执行操作
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

测量代码耗时优先用 steady_clock(不受系统时间调整影响);跨进程/持久化时间用 system_clockduration_cast 在单位不整除时截断。

std::array

std::array 是 C 风格数组的零开销包装:大小在编译期固定、栈分配、不退化为指针、提供 STL 容器接口(size()begin()/end()at()front()/back())。它取代了既不安全(退化为指针、无边界检查)又缺少接口的裸数组 T[]

std::vector 的区别:array 大小固定、栈分配、无动态扩容开销;vector 大小可变、堆分配。已知大小且不需要增删时用 array,否则用 vector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <array>

// 固定大小数组
std::array<int, 5> arr = {1, 2, 3, 4, 5};

// 访问元素
arr[0] = 10;
arr.at(1) = 20; // 越界抛异常,operator[] 不检查

// 迭代
for (const auto& elem : arr) {
std::cout << elem << " ";
}

// 大小
std::cout << "Size: " << arr.size() << "\n"; // 5

// 前后元素
std::cout << "Front: " << arr.front() << "\n";
std::cout << "Back: " << arr.back() << "\n";

// 填充
arr.fill(0);

at() 做边界检查(越界抛 std::out_of_range),operator[] 不检查但更快。std::get<I>(arr) 提供编译期边界检查。array 是值类型——拷贝会复制全部元素,传参用 const&span

std::forward_list

std::forward_list 是单向链表,相比 std::list(双向)每个节点少存一个指针、更省内存。它提供 O(1) 的头部插入/删除,以及任意位置 O(1) 的"插入/删除后继"操作(insert_after/erase_after)。

适用场景:内存极其紧张、只需单向遍历、频繁头部增删。但要注意现代硬件上链表缓存不友好——元素巨大且确需中间 O(1) 增删时才考虑,否则 vector 往往更快。Core Guidelines 不推荐链表作为默认选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <forward_list>

// 单向链表
std::forward_list<int> list = {1, 2, 3, 4, 5};

// 在前面插入
list.push_front(0);

// 在指定位置后插入
auto it = list.begin();
list.insert_after(it, 100);

// 删除元素
list.pop_front();
list.erase_after(it);

// 遍历
for (const auto& elem : list) {
std::cout << elem << " ";
}

// 检查是否为空
if (list.empty()) {
std::cout << "List is empty\n";
}

由于单向,只能前向遍历、没有 size()(为保 O(1) 接口而省略)、操作基于"前驱"而非"当前节点"(因为删除当前节点需要修改前驱的指针)。

std::random

C++11 的 <random> 是对 C 风格 rand() 的彻底升级。rand() 问题重重:分布不均、周期短、全局状态非线程安全、% 取模引入模偏差。新库把"随机源"与"分布"解耦:引擎(如 std::mt19937)产生均匀随机位,分布(如 uniform_int_distribution)把位映射成所需分布。

这套设计质量远超 rand()——Mersenne Twister 周期长达 2^19937-1,分布数学正确(无偏差),引擎可实例化避免全局状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <random>

// 随机数引擎
std::random_device rd; // 硬件随机数生成器(用于播种)
std::mt19937 gen(rd()); // Mersenne Twister 引擎

// 均匀分布
std::uniform_int_distribution<> dis(1, 100);
int randomInt = dis(gen); // 1-100 之间的随机整数

// 浮点数分布
std::uniform_real_distribution<> disReal(0.0, 1.0);
double randomDouble = disReal(gen);

// 正态分布
std::normal_distribution<> normal(5.0, 2.0);
double normalValue = normal(gen);

// 伯努利分布
std::bernoulli_distribution bernoulli(0.5);
bool coinFlip = bernoulli(gen);

引擎对象较重(mt19937 状态约 2.5KB),应在作用域内复用而非频繁构造。安全敏感场景(密码学、token 生成)不要用 <random>——它的输出可预测,应用平台密码学 API。

std::regex

C++11 把正则表达式纳入标准库,支持 ECMAScript、POSIX 等多种语法。它统一了之前各平台各库(Boost.Regex、PCRE)的碎片化,提供编译期/运行期正则、匹配/搜索/替换、迭代器接口。

需注意 <regex> 的实现普遍较慢(尤其 GCC),不适合高频热路径——能用手写字符串算法(find/substr)解决就别上正则。适合一次性解析、配置校验等低频场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <regex>

// 正则表达式匹配
std::string text = "Hello, World! 123";
std::regex pattern(R"(\d+)"); // 匹配数字(原始字符串字面量避免反斜杠转义)

std::smatch matches;
if (std::regex_search(text, matches, pattern)) {
std::cout << "Found: " << matches[0] << "\n"; // 123
}

// 正则表达式替换
std::string result = std::regex_replace(text, std::regex(R"(\d+)"), "NUM");
std::cout << result << "\n"; // Hello, World! NUM

// 正则表达式迭代
std::regex word_pattern(R"(\w+)");
std::sregex_iterator it(text.begin(), text.end(), word_pattern);
std::sregex_iterator end;

for (; it != end; ++it) {
std::cout << it->str() << "\n";
}

std::regex 构造开销大,应构造一次复用,不要在循环里反复构造。原始字符串字面量 R"(...)" 让写正则时不用双重转义反斜杠。regex_search 找首次匹配,regex_match 要求整串匹配。

范围 for 循环

范围 for 循环遍历可迭代范围(容器、初始化列表、数组),消除了手写迭代器/索引的样板与 off-by-one 错误。它等价于对 begin()/end() 迭代器的循环,但意图清晰、不易写错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::vector<int> vec = {1, 2, 3, 4, 5};

// 只读
for (const auto& elem : vec) {
std::cout << elem << " ";
}

// 修改
for (auto& elem : vec) {
elem *= 2;
}

// 数组
int arr[] = {1, 2, 3};
for (auto& x : arr) {
x *= 2;
}

关键:遍历非 trivial 类型(string、自定义类)时用 const auto& 避免拷贝;要修改元素用 auto&;只在元素是廉价值类型(int 等)时用 auto(按值)。遍历过程中不要修改容器结构(增删元素会使迭代器失效)。

初始化列表

C++11 的统一初始化(花括号 {})试图统一各种初始化语法:变量、对象、容器、成员、堆分配都能用 {}。它还引入了 std::initializer_list,使容器能像 vec = {1, 2, 3} 这样初始化。

一个重要安全特性:花括号初始化禁止窄化转换(如 int x{3.14} 编译错误),而 int x = 3.14 会静默截断。这让初始化更安全。

1
2
3
4
5
6
7
8
9
10
// 统一初始化
int x{42};
std::vector<int> vec{1, 2, 3, 4, 5};
std::map<std::string, int> m{
{"apple", 1},
{"banana", 2}
};

// 防止窄化转换
// int y{3.14}; // 编译错误

注意"最烦人的解析"(most vexing parse)问题:Widget w(); 会被解析为函数声明,Widget w{} 才是默认构造。花括号在此更明确。但花括号会优先匹配 initializer_list 构造函数——vector<int> v(5, 0) 是 5 个 0,vector<int> v{5, 0} 是元素 5 和 0,行为不同需留意。

nullptr

nullptr 是类型安全的空指针常量,类型为 std::nullptr_t,可隐式转换为任意指针类型,但不转换为整型。它取代了有歧义的 NULL(在 C 里 NULL 常被定义为 0(void*)0,在 C++ 里通常是 0)。

NULL 的根本问题:func(NULL)void func(int*)void func(int) 重载时,NULL 作为 0 会匹配 func(int)——出乎意料。nullptr 明确表示指针,无歧义地匹配指针重载。

1
2
3
4
5
6
7
void func(int* ptr) {}
void func(int x) {}

func(nullptr); // 调用 func(int*)
// func(NULL); // 可能有歧义:NULL 是 0,匹配 func(int)

int* ptr = nullptr;

现代 C++ 一律用 nullptr,不用 NULL0 表示空指针。模板推导时 nullptr 的类型 nullptr_t 也比 0 更准确。

constexpr

constexpr 表示"可在编译期求值",让常量计算、表格生成、模板参数等从运行期挪到编译期。相比 C 风格 #defineconstconstexpr 既类型安全又能用于更多语境(数组大小、模板参数、static_assert)。

C++11 的 constexpr 限制较严:函数体只能有一条 return(递归实现)。C++14 起大幅放宽,C++23 进一步扩展。但即便在 C++11,constexpr 递归已能做可观的编译期计算。

1
2
3
4
5
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}

constexpr int result = factorial(5); // 编译期计算

constexpr 函数既可在编译期(用于常量语境)也可在运行期调用——是否编译期取决于调用语境。若要求强制编译期,C++20 起用 constevalconstexpr 变量是真正的编译期常量,不像 const 只读不一定是编译期。

类型别名

usingtypedef 的现代替代,语法从右到左更直观,且支持模板别名——typedef 无法做到。模板别名让"带部分参数的模板"成为一等公民,极大简化了模板元编程与复杂类型的可读性。

1
2
3
4
5
6
7
8
using String = std::string;
using IntVector = std::vector<int>;

// 模板别名(typedef 做不到)
template<typename T>
using Vec = std::vector<T>;

Vec<int> v; // std::vector<int>

usingtypedef 语义等价(都不引入新类型,只是别名),但 using 语法更清晰(using Name = Type 像赋值)且能模板化。新代码一律用 using

委托构造函数

委托构造允许一个构造函数调用同类的另一个构造函数,消除多个构造函数间重复的成员初始化代码。之前要么每个构造函数各写一遍初始化列表、要么抽个 init() 函数(后者不能用于 const 成员/引用成员初始化)。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {
public:
MyClass(int x, int y) : x_(x), y_(y) {}

// 委托给上面的构造函数
MyClass(int x) : MyClass(x, 0) {}

MyClass() : MyClass(0, 0) {}

private:
int x_, y_;
};

注意:委托构造后,初始化列表不能再初始化其他成员(委托即把初始化完全交给目标构造函数);构造函数体可在委托调用之后执行。避免循环委托(A 委托 B、B 委托 A)。

override 和 final

override 显式声明"此虚函数重写基类的虚函数",让编译器检查签名是否真的匹配——若基类没有匹配的虚函数(拼错名字、签名不一致、基类非虚),编译报错。这抓住了 C++ 最常见的继承 bug 之一:以为重写了其实没有(静默创建了一个新函数)。

final 禁止进一步重写(用于虚函数)或禁止进一步继承(用于类),表达"此处就是终态"的设计意图,有时还能助优化(去虚化)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
public:
virtual void func() {}
virtual void finalFunc() final {}
};

class Derived : public Base {
public:
void func() override {} // 明确表示重写

// void finalFunc() override {} // 编译错误,final 函数不能重写
};

class FinalClass final : public Base {
// 不能被继承
};

现代 C++ 规约:凡重写虚函数必加 override——零成本地获得编译期检查。final 谨慎使用,过度使用会妨碍扩展;但在叶子类或安全敏感的虚函数上用它防误重写很合理。

enum class

enum class(强类型枚举)解决了裸 enum 的两大问题:枚举值会隐式转为 int(类型不安全)、枚举值泄漏到外层作用域(命名冲突)。

enum class 的枚举值不会隐式转整型(必须 static_cast)、不会泄漏作用域(必须 Color::Red 限定)、可以指定底层类型(enum class Color : uint8_t)。这让枚举真正安全。

1
2
3
4
5
6
7
8
9
enum class Color { Red, Green, Blue };
enum class Animal { Dog, Cat };

Color c = Color::Red;
// Color c2 = Red; // 编译错误,需要作用域
// if (c == Animal::Dog) {} // 编译错误,不同枚举类型不能比较

// 显式转换
int value = static_cast<int>(Color::Red); // 0

C++23 起取底层值用 std::to_underlying(c) 取代啰嗦的 static_cast。枚举转名字的 switch 末尾可用 std::unreachable() 标记不可达分支。新代码一律用 enum class,不用裸 enum

静态断言

static_assert 在编译期断言,条件为假时编译报错并输出消息。它让"对类型/常量的假设"在编译期就暴露,而非运行期崩溃。常用于模板约束、平台假设、布局检查。

1
2
3
4
5
6
static_assert(sizeof(int) == 4, "int must be 4 bytes");

template<typename T>
void checkSize() {
static_assert(sizeof(T) >= 4, "Type must be at least 4 bytes");
}

C++17 起 static_assert 的消息可省略(static_assert(cond);)。与运行期 assert 区别:static_assert 在编译期、NDEBUG 无关;assert 在运行期、NDEBUG 下被移除。模板中 static_assert 是早期做概念约束的常见手段(C++20 Concepts 取代了大部分这种用法)。

变参模板

变参模板接受任意数量、任意类型的模板参数(参数包 Args...),让"类型安全的可变参数"成为可能。它取代了 C 风格 printf 的可变参数(无类型检查)和 Java 风格 Object...(类型擦除),是实现 make_sharedtuplefunction 等的基础设施。

C++11 时代展开参数包靠递归模板(定义一个基础 case + 一个递归 case)。C++17 折叠表达式大幅简化了常见模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 打印任意数量的参数
template<typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...); // 折叠表达式(C++17)
std::cout << "\n";
}

// 递归展开
template<typename T>
T sum(T first) {
return first;
}

template<typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}

print(1, 2, 3, "hello"); // 1 2 3 hello
int total = sum(1, 2, 3, 4, 5); // 15

变参模板的递归展开较繁琐,C++17 折叠表达式能省掉大部分递归样板。转发参数包用 std::forward<Args>(args)...(完美转发)保持值类别。sizeof...(Args) 取参数个数。

常用特性总结

最常用的 C++11 特性:

  • auto 类型推导
  • Lambda 表达式
  • 智能指针(unique_ptr, shared_ptr
  • 范围 for 循环
  • nullptr
  • override 关键字
  • enum class