1:开篇提醒
C++14 最具实用价值的部分其实是标准库的扩展 ------ 本篇讲解的四个工具(std::exchange、std::make_unique、std::integer_sequence、std::quoted)没有复杂的语法,却能从安全性、简洁性、性能三个维度彻底改变你的代码风格。
它们的共同特点是:解决了 C++11 标准库的明显短板,让 "正确的代码" 同时成为 "简洁的代码"。
2:std::exchange
1:基础语法
std::exchange 定义在 <utility> 头文件中,作用是用新值替换对象的旧值,并返回旧值。这是一个极其通用的工具函数,几乎可以在任何需要 "替换并返回旧值" 的场景中使用。
cpp
#include <iostream>
#include <utility>
#include <vector>
// 函数签名
template<class T, class U = T>
T exchange(T& obj, U&& new_value);
int main() {
// 1. 基础用法:替换基本类型
int x = 10;
int old_x = std::exchange(x, 20);
std::cout << "old_x: " << old_x << ", new_x: " << x << "\n"; // 10, 20
// 2. 替换容器
std::vector<int> v1 = {1, 2, 3};
std::vector<int> old_v = std::exchange(v1, {4, 5, 6});
std::cout << "old_v size: " << old_v.size() << ", new_v size: " << v1.size() << "\n"; // 3, 3
// 3. 简洁实现斐波那契数列
std::cout << "斐波那契数列: ";
for (int a{0}, b{1}; a < 100; a = std::exchange(b, a + b)) {
std::cout << a << ", ";
}
std::cout << "...\n";
// 4. 替换类成员变量
class Stream {
int flags_ = 0;
public:
int flags() const { return flags_; }
int flags(int newf) { return std::exchange(flags_, newf); }
};
Stream s;
std::cout << "初始flags: " << s.flags() << "\n"; // 0
std::cout << "旧flags: " << s.flags(12) << "\n"; // 0
std::cout << "新flags: " << s.flags() << "\n"; // 12
return 0;
}
2:底层原理和核心应用
1:底层原理实现
std::exchange 的实现极其简单,却蕴含了现代 C++ 的移动语义精髓:
cpp
// C++14 标准实现(简化版)
template<class T, class U = T>
T exchange(T& obj, U&& new_value) {
T old_value = std::move(obj); // 移动构造旧值
obj = std::forward<U>(new_value); // 完美转发新值赋值给obj
return old_value; // 返回旧值(会被返回值优化RVO)
}
- 所有操作都是移动语义,没有多余的复制,性能极高
- 支持任意可移动构造和可赋值的类型
- C++20 已将其升级为
constexpr,可在编译期使用
2:经典应用:实现移动赋值运算符
这是 std::exchange 最重要的使用场景,它能让移动赋值运算符的实现变得异常简洁且安全:
cpp
// 传统手动实现移动赋值运算符(容易出错)
class MyClass {
int* data;
public:
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete data; // 释放当前对象的资源
data = other.data; // 接管资源
other.data = nullptr; // 置空源对象
}
return *this;
}
};
// 使用std::exchange实现(简洁且安全)
class MyClass {
int* data;
public:
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete std::exchange(data, other.data); // 释放旧资源,同时接管新资源
other.data = nullptr;
}
return *this;
}
};
std::exchange(data, other.data) 会先返回 data 的旧值,然后将 other.data 赋值给 data,完美解决了 "先释放再接管" 的顺序问题。
3:std::exchange和std::swap
| 函数 | 作用 | 返回值 | 适用场景 |
|---|---|---|---|
std::exchange(a, b) |
用 b 的值替换 a 的值 | a 的旧值 | 单向替换,需要获取旧值 |
std::swap(a, b) |
交换 a 和 b 的值 | 无返回值 | 双向交换,不需要旧值 |
4:C++20的改进
- 支持
constexpr:可在编译期常量表达式中使用 - 支持数组:可以直接替换整个数组的内容
- 支持
std::initializer_list作为新值(C++14 已经支持,C++20 进一步优化)
3:常见陷阱和注意事项
- 新值的类型转换问题 :如果
new_value的类型与obj不同,会发生隐式类型转换,可能导致精度丢失 - 临时对象的生命周期 :如果
new_value是临时对象,会在赋值完成后立即销毁,不会影响结果 - 自赋值安全 :
std::exchange本身不处理自赋值,在移动赋值运算符中需要手动检查this != &other - 不要用于基本类型的简单替换 :对于
int x = 10; int old = x; x = 20;这种简单情况,直接写比用exchange更清晰
3:std::make_unique
1:基础语法
std::make_unique 定义在 <memory> 头文件中,是 C++14 最重要的标准库补充之一,用于安全地创建 std::unique_ptr 对象。
cpp
#include <iostream>
#include <memory>
struct Vec3 {
int x, y, z;
Vec3(int x = 0, int y = 0, int z = 0) noexcept : x(x), y(y), z(z) {}
friend std::ostream& operator<<(std::ostream& os, const Vec3& v) {
return os << "{" << v.x << ", " << v.y << ", " << v.z << "}";
}
};
int main() {
// 1. 创建单个对象(调用默认构造函数)
std::unique_ptr<Vec3> v1 = std::make_unique<Vec3>();
std::cout << "v1: " << *v1 << "\n"; // {0, 0, 0}
// 2. 创建单个对象(调用带参数的构造函数)
std::unique_ptr<Vec3> v2 = std::make_unique<Vec3>(1, 2, 3);
std::cout << "v2: " << *v2 << "\n"; // {1, 2, 3}
// 3. 创建数组(元素会被值初始化)
std::unique_ptr<Vec3[]> arr = std::make_unique<Vec3[]>(5);
std::cout << "arr[0]: " << arr[0] << "\n"; // {0, 0, 0}
// 4. 推荐写法:使用auto自动推导类型
auto ptr = std::make_unique<int>(42);
std::cout << "*ptr: " << *ptr << "\n"; // 42
return 0;
}
2:为什么必须使用std::make_unique
1:C++11为什么没有make_unique
这是 C++ 标准史上最著名的 "疏忽" 之一:C++11 引入了 std::unique_ptr,却漏掉了对应的 std::make_unique,直到 C++14 才补上。C++11 时期,大家只能自己实现一个简单版本:
cpp
// C++11 手动实现make_unique(简化版)
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
2:核心优势:异常安全
这是 make_unique 比直接使用 new 最大的优势。考虑以下代码:
cpp
// 不安全的写法:存在内存泄漏风险
void unsafe_function() {
// 假设process函数接受两个参数:一个unique_ptr和一个int
process(std::unique_ptr<int>(new int(42)), get_value());
}
C++ 没有规定函数参数的求值顺序,编译器可能会按以下顺序执行:
new int(42)→ 分配内存get_value()→ 调用函数,如果这里抛出异常std::unique_ptr<int>(...)→ 永远不会执行,分配的内存泄漏
而使用 make_unique 可以完全避免这个问题:
cpp
// 安全的写法:不会发生内存泄漏
void safe_function() {
process(std::make_unique<int>(42), get_value());
}
因为 make_unique 内部将 new 和 unique_ptr 的构造封装在了同一个函数调用中,要么全部成功,要么全部失败,不会出现中间状态。
3:std::make_unique和std::make_shared
| 特性 | std::make_unique<T> |
std::make_shared<T> |
|---|---|---|
| 内存分配 | 两次分配:一次分配对象,一次分配控制块(C++23 前) | 一次分配:对象和控制块在同一块内存中 |
| 内存释放 | 对象销毁时立即释放内存 | 最后一个 shared_ptr 和 weak_ptr 都销毁时才释放内存 |
| 数组支持 | 原生支持 make_unique<T[]>(n) |
C++20 才支持 make_shared<T[]>(n) |
| 自定义删除器 | 不支持(必须直接构造 unique_ptr) | 不支持(必须直接构造 shared_ptr) |
4:C++20扩展std::make_unique_for_overwrite
C++20 新增了 make_unique_for_overwrite,它不会初始化对象的内存,适用于性能敏感的场景:
cpp
// 普通make_unique:会将5个int初始化为0
auto arr1 = std::make_unique<int[]>(5);
// make_unique_for_overwrite:不初始化内存,直接使用
auto arr2 = std::make_unique_for_overwrite<int[]>(5);
// 立即覆盖内存,不会有未初始化值的问题
for (int i = 0; i < 5; ++i) {
arr2[i] = i;
}
对于大型数组,跳过初始化可以显著提升性能。
3:常见陷阱和注意事项
-
不能创建需要自定义删除器的
unique_ptr:如果需要自定义删除器,必须直接使用unique_ptr的构造函数:cpp// 错误:make_unique不支持自定义删除器 // auto ptr = std::make_unique<FILE>(fopen("file.txt", "r"), fclose); // 正确:直接构造 std::unique_ptr<FILE, decltype(&fclose)> ptr(fopen("file.txt", "r"), fclose); -
数组版本不能使用初始化列表 :
make_unique<int[]>({1,2,3})是编译错误,只能先创建数组再逐个赋值 -
不要用
make_unique创建std::array:直接创建栈上的std::array性能更好 -
优先使用
auto推导类型:避免重复写类型名,减少代码冗余
4:std::integer_sequence
1:基本语法
std::integer_sequence 定义在 <utility> 头文件中,是 C++14 引入的模板元编程基础工具,用于在编译期表示一个整数序列。
cpp
#include <iostream>
#include <utility>
#include <tuple>
#include <array>
// 1. 基础类型定义
template<class T, T... Ints>
class integer_sequence;
// 2. 辅助模板:快速生成序列
template<std::size_t N>
using make_index_sequence = integer_sequence<std::size_t, 0, 1, ..., N-1>;
template<typename... Ts>
using index_sequence_for = make_index_sequence<sizeof...(Ts)>;
// 打印整数序列
template<typename T, T... Ints>
void print_sequence(std::integer_sequence<T, Ints...>) {
((std::cout << Ints << " "), ...); // C++17折叠表达式
std::cout << "\n";
}
int main() {
// 手动创建序列
print_sequence(std::integer_sequence<int, 9, 2, 5, 1>{}); // 9 2 5 1
// 生成0到11的序列
print_sequence(std::make_integer_sequence<int, 12>{}); // 0 1 2 ... 11
// 生成0到9的size_t序列(最常用)
print_sequence(std::make_index_sequence<10>{}); // 0 1 2 ... 9
// 生成与参数包长度相同的序列
print_sequence(std::index_sequence_for<int, double, std::string>{}); // 0 1 2
return 0;
}
2:底层原理和核心应用
1:底层原理
std::integer_sequence 本身是一个空类,它的唯一作用是携带编译期整数序列信息。它的实现依赖于模板递归展开:
cpp
// 简化版实现
template<class T, T... Ints>
struct integer_sequence {
static constexpr std::size_t size() noexcept { return sizeof...(Ints); }
};
// 递归生成序列
template<class T, T N, T... Ints>
struct make_integer_sequence_impl
: make_integer_sequence_impl<T, N-1, N-1, Ints...> {};
// 递归终止条件
template<class T, T... Ints>
struct make_integer_sequence_impl<T, 0, Ints...> {
using type = integer_sequence<T, Ints...>;
};
template<class T, T N>
using make_integer_sequence = typename make_integer_sequence_impl<T, N>::type;
编译器会在编译期展开递归,生成从 0 到 N-1 的整数序列。
2:核心应用
元组解包与遍历
这是 std::integer_sequence 最常用的场景,解决了 C++ 中无法直接遍历元组的问题:
cpp
#include <tuple>
// 辅助函数:使用index_sequence遍历元组
template<typename Tuple, std::size_t... Indices>
void print_tuple_impl(const Tuple& t, std::index_sequence<Indices...>) {
// C++17折叠表达式:依次打印每个元素
((std::cout << std::get<Indices>(t) << " "), ...);
std::cout << "\n";
}
// 对外接口
template<typename... Args>
void print_tuple(const std::tuple<Args...>& t) {
// 生成与元组长度相同的index_sequence
print_tuple_impl(t, std::index_sequence_for<Args...>{});
}
int main() {
auto t = std::make_tuple(10, 3.14, "Hello", 'A');
print_tuple(t); // 输出:10 3.14 Hello A
return 0;
}
编译期数组初始化
std::integer_sequence 可以用于在编译期初始化数组,避免运行时开销:
cpp
#include <array>
// 编译期生成0到N-1的数组
template<std::size_t N>
constexpr std::array<int, N> make_iota_array() {
return []<std::size_t... Indices>(std::index_sequence<Indices...>) {
return std::array<int, N>{Indices...};
}(std::make_index_sequence<N>{});
}
int main() {
// 编译期生成数组{0,1,2,3,4}
constexpr auto arr = make_iota_array<5>();
static_assert(arr[0] == 0 && arr[4] == 4);
return 0;
}
函数参数包展开
std::integer_sequence 可以将数组或元组的元素展开为函数参数:
cpp
// 一个接受多个参数的函数
int sum(int a, int b, int c) {
return a + b + c;
}
// 将数组的元素展开为函数参数
template<typename T, std::size_t N, std::size_t... Indices>
T apply_array(const std::array<T, N>& arr, std::index_sequence<Indices...>) {
return sum(arr[Indices]...);
}
int main() {
std::array<int, 3> arr = {1, 2, 3};
int result = apply_array(arr, std::make_index_sequence<3>{});
std::cout << "sum: " << result << "\n"; // 6
return 0;
}
3:常见陷阱和注意事项
- 编译期序列长度限制:不同编译器对模板递归深度有不同的限制(通常是 1024),过长的序列会导致编译错误
- 不要在运行时使用 :
std::integer_sequence是纯编译期工具,运行时没有任何意义 - C++20 改进 :C++20 引入了
std::integer_sequence的算法支持(如std::transform、std::filter),但实际使用较少 - 优先使用
std::index_sequence:绝大多数场景下,使用std::size_t类型的index_sequence就足够了
5:std::quoted
1:基础语法
std::quoted 定义在 <iomanip> 头文件中,是一个 I/O 操纵器,用于简化带引号字符串的输入输出操作。
cpp
#include <iostream>
#include <sstream>
#include <iomanip>
#include <string>
int main() {
// 1. 输出:自动添加双引号
std::string text = "Hello, World!";
std::cout << "Without quoted: " << text << "\n"; // Hello, World!
std::cout << "With quoted: " << std::quoted(text) << "\n"; // "Hello, World!"
// 2. 输入:自动去除双引号
std::istringstream input("\"Hello, World!\"");
std::string extracted;
input >> std::quoted(extracted);
std::cout << "Extracted: " << extracted << "\n"; // Hello, World!
// 3. 自定义分隔符:使用单引号
std::string s = "It's a test";
std::cout << "Single quotes: " << std::quoted(s, '\'') << "\n"; // 'It\'s a test'
return 0;
}
2:底层原理和核心应用
1:底层原理
std::quoted 本身不是一个函数,而是一个返回代理对象的函数模板 。这个代理对象重载了 << 和 >> 运算符,实现了带引号的输入输出逻辑:
cpp
// 简化版实现
template<typename CharT>
struct quoted_proxy {
const CharT* str;
CharT quote;
CharT escape;
};
template<typename CharT>
quoted_proxy<CharT> quoted(const CharT* str, CharT quote = '"', CharT escape = '\\') {
return {str, quote, escape};
}
template<typename OStream, typename CharT>
OStream& operator<<(OStream& os, const quoted_proxy<CharT>& proxy) {
os << proxy.quote;
for (const CharT* p = proxy.str; *p; ++p) {
if (*p == proxy.quote || *p == proxy.escape) {
os << proxy.escape;
}
os << *p;
}
os << proxy.quote;
return os;
}
2:核心应用
CSV文件读写
CSV 文件中的字段如果包含逗号或空格,必须用引号括起来。std::quoted 可以完美处理这种情况:
cpp
#include <fstream>
#include <vector>
struct User {
std::string name;
int age;
std::string email;
};
// 写入CSV文件
void write_csv(const std::string& filename, const std::vector<User>& users) {
std::ofstream file(filename);
file << "Name,Age,Email\n";
for (const auto& user : users) {
file << std::quoted(user.name) << ","
<< user.age << ","
<< std::quoted(user.email) << "\n";
}
}
// 读取CSV文件
std::vector<User> read_csv(const std::string& filename) {
std::vector<User> users;
std::ifstream file(filename);
std::string line;
std::getline(file, line); // 跳过表头
while (std::getline(file, line)) {
std::istringstream ss(line);
User user;
char comma;
ss >> std::quoted(user.name) >> comma
>> user.age >> comma
>> std::quoted(user.email);
users.push_back(user);
}
return users;
}
配置文件序列号和反序列化
cpp
struct Config {
std::string username;
std::string password;
std::string server;
int port;
};
std::string serialize_config(const Config& config) {
std::ostringstream oss;
oss << "username=" << std::quoted(config.username) << "\n"
<< "password=" << std::quoted(config.password) << "\n"
<< "server=" << std::quoted(config.server) << "\n"
<< "port=" << config.port << "\n";
return oss.str();
}
Config deserialize_config(const std::string& str) {
Config config;
std::istringstream iss(str);
std::string key, value;
while (std::getline(iss, key, '=')) {
std::getline(iss, value);
std::istringstream value_ss(value);
if (key == "username") value_ss >> std::quoted(config.username);
else if (key == "password") value_ss >> std::quoted(config.password);
else if (key == "server") value_ss >> std::quoted(config.server);
else if (key == "port") value_ss >> config.port;
}
return config;
}
日志输出
在日志中使用 std::quoted 可以避免字符串中的空格、换行符等特殊字符导致日志解析错误:
cpp
void log(const std::string& message) {
std::cout << "[INFO] " << std::quoted(message) << "\n";
}
int main() {
log("User logged in"); // [INFO] "User logged in"
log("Message with\nnewline"); // [INFO] "Message with\nnewline"
return 0;
}
3:常见陷阱和注意事项
- 输入时必须使用
std::quoted:如果输入的字符串包含引号或空格,不使用std::quoted会导致读取不完整 - 转义字符的处理 :
std::quoted会自动转义字符串中的引号和转义字符,输出时会添加转义符,输入时会去除转义符 - 自定义转义字符 :可以通过第三个参数指定转义字符,例如
std::quoted(s, '"', '/')使用/作为转义符 - 与
std::stringstream配合使用 :std::quoted最适合与字符串流配合使用,处理内存中的字符串序列化
6:总结
| 工具 | 解决的问题 | 核心优势 | 适用场景 |
|---|---|---|---|
std::exchange |
替换值并返回旧值的重复代码 | 简洁、安全、高效 | 移动赋值运算符、状态切换、循环变量更新 |
std::make_unique |
直接使用 new 的内存泄漏风险 | 异常安全、代码简洁 | 绝大多数需要创建 unique_ptr 的场景 |
std::integer_sequence |
编译期整数序列的生成 | 模板元编程基础 | 元组遍历、编译期数组初始化、参数包展开 |
std::quoted |
带引号字符串的输入输出 | 自动处理引号和转义 | CSV 读写、配置文件、日志输出 |
- 永远优先使用
std::make_unique而不是直接 new:这是现代 C++ 的基本准则之一 - 实现移动赋值运算符时必须使用
std::exchange:避免手动实现的错误 - 元组遍历优先使用
std::integer_sequence+ 折叠表达式:不要使用递归或第三方库 - 所有需要处理带空格或特殊字符的字符串 I/O 都使用
std::quoted:避免解析错误 - 不要过度使用
std::exchange:对于简单的变量替换,直接写更清晰