C++14标准库实用工具上

1:开篇提醒

C++14 最具实用价值的部分其实是标准库的扩展 ------ 本篇讲解的四个工具(std::exchangestd::make_uniquestd::integer_sequencestd::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++ 没有规定函数参数的求值顺序,编译器可能会按以下顺序执行:

  1. new int(42) → 分配内存
  2. get_value() → 调用函数,如果这里抛出异常
  3. std::unique_ptr<int>(...) → 永远不会执行,分配的内存泄漏

而使用 make_unique 可以完全避免这个问题:

cpp 复制代码
// 安全的写法:不会发生内存泄漏
void safe_function() {
    process(std::make_unique<int>(42), get_value());
}

因为 make_unique 内部将 newunique_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::transformstd::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:常见陷阱和注意事项

  1. 输入时必须使用 std::quoted :如果输入的字符串包含引号或空格,不使用 std::quoted 会导致读取不完整
  2. 转义字符的处理std::quoted 会自动转义字符串中的引号和转义字符,输出时会添加转义符,输入时会去除转义符
  3. 自定义转义字符 :可以通过第三个参数指定转义字符,例如 std::quoted(s, '"', '/') 使用 / 作为转义符
  4. 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:对于简单的变量替换,直接写更清晰