18.1 复习前面介绍过的C++11功能

C++11 标准为 C++ 语言带来了许多重要的改进和新特性,旨在提高语言的效率、安全性和易用性。在前面的章节中,我们已经接触并使用了其中的一些功能。本节将对这些已介绍的 C++11 特性进行一个简要的回顾。

18.1.1 新类型

C++11 引入了几种新的基本类型:

  • long longunsigned long long: (第 3 章)提供了至少 64 位的整数类型,用于表示比 long 更大范围的整数。
    1
    2
    long long large_num = 9876543210123LL;
    unsigned long long very_large_positive = 18446744073709551615ULL;
  • char16_tchar32_t: (第 3 章, 第 16 章)用于显式支持 UTF-16 和 UTF-32 编码的字符。分别使用 uU 前缀表示对应的字符和字符串字面量。
    1
    2
    3
    4
    char16_t utf16_char = u'Ω';
    char32_t utf32_char = U'🌍';
    const char16_t* utf16_str = u"你好";
    const char32_t* utf32_str = U"世界";
  • nullptr: (第 4 章)引入了新的空指针常量 nullptr,用于替代之前使用的 0NULLnullptr 具有明确的指针类型 (std::nullptr_t),可以避免一些与 0 (整数) 相关的歧义。
    1
    2
    3
    4
    int* ptr1 = nullptr;
    void (*func_ptr)(int) = nullptr;
    // if (ptr1 == 0) { /* ... */ } // 旧式比较
    if (ptr1 == nullptr) { /* ... */ } // C++11 推荐

18.1.2 统一的初始化

C++11 推广了使用花括号 {} 进行初始化的方式,称为统一初始化 (Uniform Initialization) 或列表初始化 (List Initialization)。这种方式可以用于多种初始化场景,并有助于防止窄化转换 (Narrowing Conversion)。

  • 基本类型:
    1
    2
    3
    int x{5};
    double pi{3.14};
    // int narrow_error{3.14}; // 编译错误!防止 double 到 int 的窄化
  • 数组:
    1
    int arr[]{1, 2, 3, 4, 5}; // 大小自动推断
  • 结构和类:
    1
    2
    3
    4
    struct Point { int x, y; };
    Point p{10, 20};

    std::string s{"Hello"};
  • STL 容器 (通过 initializer_list): (第 16 章)
    1
    2
    std::vector<int> v = {1, 2, 3};
    std::map<std::string, int> m {{"one", 1}, {"two", 2}};

18.1.3 声明

C++11 引入了新的声明方式来简化类型书写和推断:

  • auto: (第 3 章)让编译器根据变量的初始化表达式自动推断其类型。
    1
    2
    3
    4
    auto i = 42;        // i 推断为 int
    auto d = 3.14; // d 推断为 double
    auto s = std::string("text"); // s 推断为 std::string
    auto it = v.begin(); // it 推断为 std::vector<int>::iterator
  • decltype: 根据表达式推断类型,但不计算该表达式。常用于泛型编程或需要根据已有变量或函数返回值确定类型的场景。
    1
    2
    3
    4
    5
    int x = 5;
    decltype(x) y = 10; // y 的类型与 x 相同,为 int

    double func();
    decltype(func()) z; // z 的类型是 func 的返回类型 double (不实际调用 func)

18.1.4 智能指针

(第 16 章)C++11 在 <memory> 头文件中引入了现代智能指针,用于自动管理动态分配的内存,取代了容易出错的 auto_ptr

  • std::unique_ptr<T>: 独占所有权的智能指针,轻量级,不可复制,可移动。
  • std::shared_ptr<T>: 共享所有权的智能指针,通过引用计数管理对象生命周期。
  • std::weak_ptr<T>: 非拥有型指针,用于观察 shared_ptr 管理的对象,解决循环引用问题。

18.1.5 异常规范方面的修改

(第 15 章)C++11 引入了 noexcept 说明符和运算符,用于指示函数是否可能抛出异常。

  • noexcept 说明符: 放在函数声明或定义后,表示该函数保证不抛出任何异常。
    1
    void process_data() noexcept; // 声明保证不抛异常
    这有助于编译器进行优化,并用于异常安全保证。析构函数默认是 noexcept 的。
  • noexcept 运算符: noexcept(expression),在编译时判断表达式 expression 是否可能抛出异常,返回一个 bool 常量。

旧的 throw() 异常规范在 C++11 中被废弃。

18.1.6 作用域内枚举

(第 10 章)C++11 引入了强类型枚举 (Strongly-typed enums) 或**作用域内枚举 (Scoped enums)**,使用 enum class (或 enum struct) 关键字定义。

1
2
3
4
5
6
7
8
9
10
enum class Color { RED, GREEN, BLUE };
enum class Status { OK, ERROR };

Color c = Color::RED;
Status s = Status::OK;

// if (c == s) {} // 编译错误!不同类型的枚举不能直接比较
// int x = c; // 编译错误!不能隐式转换为整数

if (c == Color::GREEN) { /* ... */ }

优点:

  • 强类型: 不同枚举类型的值不能隐式转换或直接比较。
  • 作用域: 枚举成员的作用域限定在枚举类型内部,必须通过 EnumType::Member 访问,避免了命名冲突。
  • 可指定底层类型: enum class Color : char { RED, GREEN, BLUE };

18.1.7 对类的修改

(本节主要回顾,但部分内容如委托/继承构造函数、override/final 在 18.3 详细介绍,这里仅提及概念)
C++11 对类定义和使用也进行了一些改进,部分已在前面章节涉及或将在后续章节详细介绍,例如:

  • 默认构造函数和成员初始化: 允许在类定义中直接初始化非静态成员变量。
  • = default= delete: 显式要求编译器生成默认的特殊成员函数(构造、析构、拷贝、移动)或禁用它们。
  • 委托构造函数: 一个构造函数可以调用同一类的另一个构造函数。
  • 继承构造函数: 派生类可以继承基类的构造函数。
  • overridefinal: 用于管理虚函数,override 确保派生类方法确实覆盖了基类虚函数,final 阻止派生类进一步覆盖虚函数或阻止类被继承。

18.1.8 模板和 STL 方面的修改

C++11 对模板和标准库进行了大量增强:

  • 基于范围的 for 循环: (第 5 章, 第 16 章)提供了简洁的遍历容器或序列的方式。
    1
    2
    3
    4
    std::vector<int> v = {1, 2, 3};
    for (int x : v) {
    std::cout << x << " ";
    }
  • std::array: (第 4 章, 第 16 章)提供了固定大小数组的模板类封装。
  • 无序关联容器: (第 16 章)引入了基于哈希表的 unordered_set, unordered_map, unordered_multiset, unordered_multimap
  • std::initializer_list: (第 16 章)使得容器和其他类能够支持使用 {} 进行列表初始化。
  • 新的 STL 算法: 增加了一些新的算法(如 copy_if, move, shuffle 等)。
  • Lambda 表达式: (第 16 章简单使用,第 18.4 节详细介绍)允许就地定义匿名函数对象。
  • 模板别名 (using): (第 14 章)提供了比 typedef 更清晰、更强大的为模板定义别名的方式。
    1
    2
    template<typename T>
    using Vec = std::vector<T>; // Vec<int> 等价于 std::vector<int>

18.1.9 右值引用

(本节主要回顾,但右值引用和移动语义在 18.2 详细介绍,这里仅提及概念)
C++11 引入了一个重要的底层概念——右值引用 (Rvalue Reference)**,使用 && 表示。右值引用主要用于实现移动语义 (Move Semantics)** 和**完美转发 (Perfect Forwarding)**。移动语义允许资源(如动态分配的内存)从一个对象“移动”到另一个对象,而不是进行昂贵的复制,这对于优化涉及临时对象或资源转移的操作至关重要(例如 unique_ptr 的所有权转移,vector 增长时的元素移动)。移动语义将在 18.2 节详细探讨。

这些 C++11 特性共同使得 C++ 代码可以写得更现代、更安全、更高效。

18.2 移动语义和右值引用

C++11 引入了右值引用 (Rvalue Reference) 和**移动语义 (Move Semantics)**,这是 C++11 最重要的特性之一,旨在提高性能,特别是对于管理资源的类(如动态分配内存、文件句柄、网络连接等)。

18.2.1 为何需要移动语义

考虑一个管理动态内存的类,例如一个简单的字符串类或一个持有大缓冲区的类。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <cstring> // for strlen, strcpy
#include <utility> // for std::swap

class LegacyBuffer {
private:
char* buffer;
size_t size;
public:
// Constructor
LegacyBuffer(const char* s = "") : size(std::strlen(s)), buffer(new char[size + 1]) {
std::cout << "Constructor called for '" << s << "'\n";
std::strcpy(buffer, s);
}

// Destructor
~LegacyBuffer() {
std::cout << "Destructor called for buffer at " << (void*)buffer << "\n";
delete[] buffer;
}

// Copy Constructor (Deep Copy)
LegacyBuffer(const LegacyBuffer& other) : size(other.size), buffer(new char[size + 1]) {
std::cout << "Copy Constructor called from " << (void*)other.buffer << " to " << (void*)buffer << "\n";
std::strcpy(buffer, other.buffer);
}

// Copy Assignment Operator (Deep Copy)
LegacyBuffer& operator=(const LegacyBuffer& other) {
std::cout << "Copy Assignment called from " << (void*)other.buffer << " to " << (void*)buffer << "\n";
if (this == &other) { // Self-assignment check
return *this;
}
delete[] buffer; // Release old resource
size = other.size;
buffer = new char[size + 1]; // Allocate new resource
std::strcpy(buffer, other.buffer); // Copy data
return *this;
}

void print() const {
std::cout << "Buffer content: " << (buffer ? buffer : "null") << std::endl;
}

friend void swap(LegacyBuffer& first, LegacyBuffer& second) noexcept {
using std::swap;
swap(first.size, second.size);
swap(first.buffer, second.buffer);
std::cout << "Buffer swapped via friend swap\n";
}
};

LegacyBuffer createBuffer(const char* s) {
return LegacyBuffer(s); // 返回一个临时对象 (rvalue)
}

在 C++11 之前,当处理临时对象 (Temporary Objects) 或即将销毁的对象时,复制构造函数和复制赋值运算符会导致不必要的**深拷贝 (Deep Copy)**:

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
int main() {
LegacyBuffer b1("Original");
std::cout << "------\n";

LegacyBuffer b2 = b1; // 调用复制构造函数 (深拷贝) - 合理,需要独立副本
std::cout << "------\n";

LegacyBuffer b3("Temporary Source");
b1 = b3; // 调用复制赋值运算符 (深拷贝) - 合理
std::cout << "------\n";

// 问题所在:处理临时对象
LegacyBuffer b4 = createBuffer("Temporary"); // 1. createBuffer 返回临时对象
// 2. 临时对象被 *复制* 到 b4 (调用复制构造函数)
// 3. 临时对象被销毁
// (编译器优化 RVO/NRVO 可能消除这次复制,但概念上存在)
std::cout << "------\n";

b1 = createBuffer("Another Temp"); // 1. createBuffer 返回临时对象
// 2. 临时对象被 *复制* 赋值给 b1 (调用复制赋值)
// 3. 临时对象被销毁
std::cout << "------\n";

return 0;
}

在处理 createBuffer 返回的临时对象时,我们实际上只是想把临时对象内部管理的资源(buffer 指针和 size转移给新的对象 (b4b1),因为临时对象马上就要被销毁了,它的资源没用了。进行深拷贝(重新分配内存并复制内容)是一种浪费。

移动语义就是为了解决这个问题:允许我们“窃取”“移动”来自临时对象或明确标记为可移动对象的资源,而不是复制它们。

右值引用 (&&)

为了区分可以安全“窃取”资源的临时对象和不能窃取的持久对象(左值),C++11 引入了**右值引用 (Rvalue Reference)**,用 && 表示。

  • 左值 (Lvalue): 通常指那些有名字、可以取地址、在表达式结束后仍然存在的对象。例如,变量名 b1, b2
  • 右值 (Rvalue): 通常指那些临时的、没有名字、在表达式结束后即将销毁的值。例如,函数返回值 createBuffer("Temporary"),字面常量 10, "Hello",算术表达式的结果 x + y
  • 左值引用 (&): 只能绑定到左值。LegacyBuffer& ref = b1; (OK), LegacyBuffer& ref = createBuffer("Temp"); (错误!)。 (const 左值引用 const LegacyBuffer& 是个例外,它可以绑定到右值)。
  • 右值引用 (&&): 只能绑定到右值。LegacyBuffer&& rref = createBuffer("Temp"); (OK), LegacyBuffer&& rref = b1; (错误!)。

右值引用 && 的引入使得我们可以重载函数(特别是构造函数和赋值运算符),让它们能够区分接收的是左值还是右值,从而对右值(临时对象)采取不同的、更高效的操作(移动)。

18.2.2 一个移动示例

现在我们为 LegacyBuffer 类添加移动构造函数和移动赋值运算符。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <iostream>
#include <cstring>
#include <utility> // for std::move and std::swap

class ModernBuffer {
private:
char* buffer;
size_t size;
public:
// Constructor
ModernBuffer(const char* s = "") : size(std::strlen(s)), buffer(new char[size + 1]) {
std::cout << "Constructor called for '" << s << "'\n";
std::strcpy(buffer, s);
}

// Destructor
~ModernBuffer() {
std::cout << "Destructor called for buffer at " << (void*)buffer << "\n";
delete[] buffer;
}

// Copy Constructor (Deep Copy)
ModernBuffer(const ModernBuffer& other) : size(other.size), buffer(new char[size + 1]) {
std::cout << "Copy Constructor called from " << (void*)other.buffer << " to " << (void*)buffer << "\n";
std::strcpy(buffer, other.buffer);
}

// Copy Assignment Operator (Deep Copy)
ModernBuffer& operator=(const ModernBuffer& other) {
std::cout << "Copy Assignment called from " << (void*)other.buffer << " to " << (void*)buffer << "\n";
if (this == &other) {
return *this;
}
// 使用 copy-and-swap idiom 更安全
ModernBuffer temp(other); // 调用复制构造
swap(*this, temp); // 交换资源
return *this;
// 旧方式:
// delete[] buffer;
// size = other.size;
// buffer = new char[size + 1];
// std::strcpy(buffer, other.buffer);
// return *this;
}

// *** Move Constructor (C++11) ***
ModernBuffer(ModernBuffer&& other) noexcept // 接收右值引用,标记为 noexcept
: size(other.size), buffer(other.buffer) // 1. 窃取资源 (浅拷贝指针和大小)
{
std::cout << "Move Constructor called from " << (void*)other.buffer << " to " << (void*)buffer << "\n";
// 2. 将源对象置于有效但可析构的状态 (通常是置空)
other.size = 0;
other.buffer = nullptr;
}

// *** Move Assignment Operator (C++11) ***
ModernBuffer& operator=(ModernBuffer&& other) noexcept // 接收右值引用,标记为 noexcept
{
std::cout << "Move Assignment called from " << (void*)other.buffer << " to " << (void*)buffer << "\n";
if (this == &other) { // 自赋值检查 (虽然对右值不太可能,但保持良好习惯)
return *this;
}
// 1. 释放当前对象的资源
delete[] buffer;

// 2. 窃取源对象的资源
size = other.size;
buffer = other.buffer;

// 3. 将源对象置于有效但可析构的状态
other.size = 0;
other.buffer = nullptr;

return *this;
// 或者使用 swap:
// swap(*this, other);
// return *this;
}


void print() const {
std::cout << "Buffer content: " << (buffer ? buffer : "null") << std::endl;
}

friend void swap(ModernBuffer& first, ModernBuffer& second) noexcept {
using std::swap;
swap(first.size, second.size);
swap(first.buffer, second.buffer);
std::cout << "Buffer swapped via friend swap\n";
}
};

ModernBuffer createModernBuffer(const char* s) {
return ModernBuffer(s); // 返回临时对象 (rvalue)
}

int main() {
ModernBuffer mb1("Original");
std::cout << "------\n";

// 调用移动构造函数 (因为 createModernBuffer 返回右值)
ModernBuffer mb2 = createModernBuffer("Temporary");
std::cout << "------\n";
mb2.print();
std::cout << "------\n";

// 调用移动赋值运算符 (因为 createModernBuffer 返回右值)
mb1 = createModernBuffer("Another Temp");
std::cout << "------\n";
mb1.print();
std::cout << "------\n";

return 0;
}

现在,当用 createModernBuffer 返回的临时对象来初始化 mb2 或赋值给 mb1 时,会调用移动构造函数移动赋值运算符。这些操作不再进行深拷贝,而是直接“窃取”临时对象的 buffer 指针,并将临时对象的指针置为 nullptr,避免了内存分配和数据复制,效率大大提高。

18.2.3 移动构造函数解析

1
2
3
4
5
6
7
ModernBuffer(ModernBuffer&& other) noexcept
: size(other.size), buffer(other.buffer) // 1. 窃取资源
{
// 2. 将源对象置空
other.size = 0;
other.buffer = nullptr;
}
  1. 参数类型 ModernBuffer&& other: 接收一个右值引用,表示它只能绑定到右值(如临时对象)。
  2. 资源窃取: 构造函数通过初始化列表直接复制源对象 other 的指针 buffer 和大小 size。这是一个浅拷贝,非常快速。
  3. 置空源对象: 关键步骤! 必须将源对象 other 的指针成员(buffer)设置为 nullptr(或其他有效但表示“空”状态的值)。这确保了当 other(临时对象)随后被析构时,它的析构函数 delete[] buffer; 不会释放已经被新对象“窃取”走的内存,从而避免了重复释放 (double free) 的错误。源对象必须被置于一个有效的、可析构的状态
  4. noexcept: 移动操作通常不应该抛出异常(因为它们主要涉及指针和基本类型的赋值)。将移动构造函数和移动赋值运算符标记为 noexcept 非常重要。这允许 STL 容器等在需要重新分配内存时(如 vector 增长)安全地移动元素而不是复制它们,从而获得显著的性能提升。如果移动操作可能抛异常,STL 通常会回退到使用(保证异常安全的)复制操作。

18.2.4 赋值

移动赋值运算符的逻辑与移动构造函数类似:

1
2
3
4
5
6
7
8
9
10
11
12
ModernBuffer& operator=(ModernBuffer&& other) noexcept {
// ... 自赋值检查 ...
// 1. 释放当前资源
delete[] buffer;
// 2. 窃取源资源
size = other.size;
buffer = other.buffer;
// 3. 置空源对象
other.size = 0;
other.buffer = nullptr;
return *this;
}

它首先释放自己当前持有的资源,然后窃取源对象的资源,最后将源对象置空。使用 swap 实现通常更简洁且能自动处理自赋值和异常安全(如果 swapnoexcept 的话)。

18.2.5 强制移动 (std::move)

移动构造函数和移动赋值运算符通常只对右值(如临时对象)起作用。但有时我们想从一个左值(有名字的对象)那里“窃取”资源,即使这个左值在之后还会存在(但我们明确知道不再需要它的资源了)。

例如,将一个大的 vector 的内容转移给另一个 vector

1
2
3
4
5
6
7
8
9
10
11
12
std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> destination;

// destination = source; // 这会调用复制赋值,复制所有元素

// 我们想移动 source 的内容到 destination,即使 source 是左值
// 使用 std::move() 将 source 强制转换为右值引用类型
destination = std::move(source);

// 现在,destination 拥有了原来的元素 {1, 2, 3, 4, 5}
// source 的状态是有效的,但内容未指定 (通常为空)
std::cout << "Source size after move: " << source.size() << std::endl; // 通常输出 0

std::move (定义在 <utility>) 本身并不执行任何移动操作。它只是一个类型转换,它无条件地将其实参(无论是左值还是右值)转换为右值引用类型 (T&&)。

这个转换使得被转换的对象可以被绑定到接受右值引用的函数(如移动构造函数或移动赋值运算符),从而触发移动语义。

使用 std::move 的注意事项:

  • 调用 std::move(x) 后,你不应该再对 x 的值做任何假设(除了它可以被安全地销毁或重新赋值)。它的资源可能已经被“偷走”了。
  • 只在你确定不再需要源对象(左值)的资源,或者源对象本身就是临时的(虽然对临时对象用 std::move 通常是多余的)时,才使用 std::move

移动语义和右值引用是 C++11 中实现资源高效转移的关键机制,对于编写高性能的 C++ 代码至关重要,尤其是在处理大型数据结构或管理系统资源时。

18.3 新的类功能

C++11 不仅引入了移动语义,还对类的定义和使用方式进行了一些重要的增强,使得类的设计更加灵活和可控。

18.3.1 特殊的成员函数

对于一个类,编译器在特定条件下可以自动生成一些特殊的成员函数:

  1. 默认构造函数 (Default Constructor): 如果你没有提供任何构造函数,编译器会生成一个。
  2. 析构函数 (Destructor): 如果你没有提供析构函数,编译器会生成一个。
  3. 复制构造函数 (Copy Constructor): 如果你没有提供复制构造函数,编译器会生成一个,执行逐成员复制。
  4. 复制赋值运算符 (Copy Assignment Operator): 如果你没有提供复制赋值运算符,编译器会生成一个,执行逐成员赋值。
  5. 移动构造函数 (Move Constructor) (C++11): 仅当没有显式声明任何复制操作(复制构造、复制赋值)且没有显式声明移动操作(移动构造、移动赋值)且没有显式声明析构函数时,编译器才会生成。它执行逐成员移动。
  6. 移动赋值运算符 (Move Assignment Operator) (C++11): 生成条件与移动构造函数类似。它执行逐成员移动。

规则总结 (Rule of Three/Five/Zero):

  • Rule of Three (C++98): 如果你需要显式定义析构函数、复制构造函数或复制赋值运算符中的任何一个(通常是因为类管理了需要深拷贝或特殊清理的资源),那么你几乎肯定需要同时定义这三个。
  • Rule of Five (C++11): 如果你需要显式定义上述三个中的任何一个,或者显式定义了移动构造函数或移动赋值运算符,那么你应该考虑定义或删除 (delete) 所有五个(析构、复制构造、复制赋值、移动构造、移动赋值),以确保类的行为符合预期。因为显式定义任何一个复制/移动/析构函数都会阻止编译器自动生成移动操作。
  • Rule of Zero (现代 C++ 推荐): 尽量设计你的类,使其不需要自定义析构函数、复制/移动构造函数或复制/移动赋值运算符。这通常通过使用 RAII(资源获取即初始化)原则和依赖标准库组件(如智能指针 unique_ptr, shared_ptr 和容器 vector, string)来实现,这些组件已经正确地处理了资源的复制、移动和释放。如果遵循 Rule of Zero,编译器生成的默认版本通常就能正常工作。

18.3.2 默认的方法和禁用的方法 (= default, = delete)

C++11 允许你更明确地控制特殊成员函数的生成:

  • = default: 显式地告诉编译器生成该特殊成员函数的默认实现。即使因为你定义了其他构造函数或移动操作而导致编译器原本不会生成它,= default 也可以强制生成默认版本(如果可能的话)。这对于希望拥有默认行为但又需要自定义其他构造函数的情况很有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class MyClassDefault {
    public:
    MyClassDefault(int val) : data(val) {} // 自定义构造函数

    // 即使定义了其他构造函数,仍显式要求编译器生成默认构造函数
    MyClassDefault() = default;

    // 显式要求编译器生成默认的复制构造函数
    MyClassDefault(const MyClassDefault&) = default;
    // ... 其他特殊成员函数也可以 = default
    private:
    int data;
    };
  • = delete: 显式地禁用某个成员函数(可以是特殊成员函数,也可以是普通成员函数)。如果代码尝试调用被 = delete 的函数,编译器会报错。这常用于阻止对象的复制或防止不期望的类型转换。

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    #include <memory> // for unique_ptr

    class NonCopyable {
    public:
    NonCopyable() = default;

    // 禁用复制构造函数
    NonCopyable(const NonCopyable&) = delete;
    // 禁用复制赋值运算符
    NonCopyable& operator=(const NonCopyable&) = delete;

    // 移动操作通常仍可默认生成或显式 default
    NonCopyable(NonCopyable&&) = default;
    NonCopyable& operator=(NonCopyable&&) = default;
    };

    class ResourceManager {
    private:
    std::unique_ptr<int> ptr; // unique_ptr 本身是不可复制的
    public:
    ResourceManager(int val) : ptr(std::make_unique<int>(val)) {}
    // 由于 unique_ptr 不可复制,编译器不会生成默认的复制操作
    // 我们可以显式禁用它们,使意图更明确
    ResourceManager(const ResourceManager&) = delete;
    ResourceManager& operator=(const ResourceManager&) = delete;

    // 移动操作是允许的 (unique_ptr 可移动)
    ResourceManager(ResourceManager&&) = default;
    ResourceManager& operator=(ResourceManager&&) = default;

    // 防止通过整数进行隐式转换构造
    explicit ResourceManager(long long) = delete; // 禁止 long long 构造
    };

    int main() {
    NonCopyable nc1;
    // NonCopyable nc2 = nc1; // 编译错误!复制构造函数被删除
    // NonCopyable nc3;
    // nc3 = nc1; // 编译错误!复制赋值运算符被删除

    ResourceManager rm1(10);
    // ResourceManager rm2 = rm1; // 编译错误!
    ResourceManager rm3 = std::move(rm1); // OK,移动构造函数是默认的

    // ResourceManager rm4(100LL); // 编译错误!long long 构造函数被删除

    return 0;
    }

    = default= delete 提高了代码的清晰度和对类行为的控制力。

18.3.3 委托构造函数

C++11 允许一个构造函数调用同一个类的另一个构造函数,这称为**委托构造函数 (Delegating Constructor)**。这有助于减少构造函数之间的代码重复。

  • 被委托的构造函数(目标构造函数)会先执行。
  • 目标构造函数执行完毕后,委托构造函数函数体内的代码(如果有的话)才会执行。
  • 委托调用必须出现在构造函数的初始化列表位置。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <string>
#include <iostream>

class Report {
private:
int id;
std::string title;
std::string content;

public:
// 目标构造函数 (执行实际初始化)
Report(int i, const std::string& t, const std::string& c)
: id(i), title(t), content(c) {
std::cout << "Target constructor called (id=" << id << ").\n";
// ... 可能还有其他初始化逻辑 ...
}

// 委托构造函数 1: 提供默认 content
Report(int i, const std::string& t)
: Report(i, t, "Default Content") { // 委托给三个参数的构造函数
std::cout << "Delegating constructor 1 called.\n";
// 这里可以添加此构造函数特有的逻辑
}

// 委托构造函数 2: 提供默认 title 和 content
Report(int i)
: Report(i, "Default Title") { // 委托给两个参数的构造函数
std::cout << "Delegating constructor 2 called.\n";
}

// 委托构造函数 3: 提供所有默认值
Report()
: Report(0) { // 委托给一个参数的构造函数
std::cout << "Delegating constructor 3 (default) called.\n";
}

void print() const {
std::cout << "ID: " << id << ", Title: " << title << ", Content: " << content << std::endl;
}
};

int main() {
std::cout << "Creating r1:\n";
Report r1(101, "Monthly Report", "Details..."); // 调用目标构造
r1.print();
std::cout << "------\n";

std::cout << "Creating r2:\n";
Report r2(102, "Weekly Update"); // 调用委托构造 1 -> 目标构造
r2.print();
std::cout << "------\n";

std::cout << "Creating r3:\n";
Report r3(103); // 调用委托构造 2 -> 委托构造 1 -> 目标构造
r3.print();
std::cout << "------\n";

std::cout << "Creating r4:\n";
Report r4; // 调用委托构造 3 -> 委托构造 2 -> 委托构造 1 -> 目标构造
r4.print();
std::cout << "------\n";

return 0;
}

委托构造函数使得初始化逻辑可以集中在一个或少数几个构造函数中,其他构造函数只需提供默认值并委托即可。

18.3.4 继承构造函数

在 C++11 之前,如果派生类想使用基类的构造函数,它必须在自己的构造函数初始化列表中显式调用基类构造函数,并且为每个需要的基类构造函数签名提供一个对应的派生类构造函数。

C++11 允许派生类使用 using 声明来继承基类的构造函数(但有一些例外,如涉及虚基类的构造函数通常不被继承)。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <string>
#include <iostream>

class Base {
private:
int value;
std::string name;
public:
Base(int v, std::string n) : value(v), name(n) {
std::cout << "Base(int, string) called.\n";
}
Base(int v) : Base(v, "DefaultName") { // 使用委托构造
std::cout << "Base(int) called.\n";
}
Base() : Base(0) { // 使用委托构造
std::cout << "Base() called.\n";
}
void display() const {
std::cout << "Base - Value: " << value << ", Name: " << name << std::endl;
}
};

class Derived : public Base {
private:
double extra_data;
public:
// 继承 Base 的所有构造函数
using Base::Base; // 编译器会生成对应的 Derived 构造函数,它们调用匹配的 Base 构造函数

// 可以添加新的构造函数,或者覆盖继承来的构造函数 (如果签名相同)
Derived(double d) : Base(999, "Special"), extra_data(d) {
std::cout << "Derived(double) called.\n";
}

// 如果需要对继承来的构造函数添加额外初始化,需要显式定义
// 例如,如果想让 Derived(int) 初始化 extra_data
Derived(int v) : Base(v), extra_data(0.0) { // 显式调用基类构造并初始化成员
std::cout << "Derived(int) explicitly defined.\n";
}


void display_derived() const {
display(); // 调用基类的 display
std::cout << "Derived - Extra Data: " << extra_data << std::endl;
}
};

int main() {
std::cout << "Creating d1:\n";
Derived d1(10, "Object1"); // 调用继承来的 Base(int, string) 对应的构造函数
d1.display_derived();
std::cout << "------\n";

std::cout << "Creating d2:\n";
Derived d2(20); // 调用 Derived 自己定义的 Derived(int) 构造函数
d2.display_derived();
std::cout << "------\n";

std::cout << "Creating d3:\n";
Derived d3; // 调用继承来的 Base() 对应的构造函数
d3.display_derived();
std::cout << "------\n";

std::cout << "Creating d4:\n";
Derived d4(3.14); // 调用 Derived 自己定义的 Derived(double) 构造函数
d4.display_derived();
std::cout << "------\n";

return 0;
}

继承构造函数简化了派生类的编写,特别是当基类有多个构造函数时,避免了编写大量仅仅是转发参数的派生类构造函数。

18.3.5 管理虚方法:overridefinal

C++11 提供了两个新的**上下文关键字 (Contextual Keywords)**(只在特定位置有特殊含义,其他地方可用作标识符)来帮助管理类继承体系中的虚函数:overridefinal

  • override:

    • 显式地放在派生类中重写 (override) 的虚函数声明或定义之后。
    • 作用:让编译器检查该函数是否确实覆盖了基类中的某个虚函数(函数签名、const 限定符、引用限定符必须完全匹配)。
    • 如果派生类函数标记为 override 但并未覆盖任何基类虚函数(例如因为拼写错误、参数类型不匹配、const 不匹配),编译器会报错。
    • 这有助于防止因意外签名不匹配而导致的覆盖失败(变成隐藏或定义新函数)。
  • final (用于虚函数):

    • 显式地放在派生类中重写的虚函数声明或定义之后。
    • 作用:阻止任何后续的派生类进一步覆盖这个虚函数。
  • final (用于类):

    • 放在类定义的 class 关键字之后,类名之前或之后。
    • 作用:阻止该类被任何其他类继承
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>

class Document {
public:
virtual void print() const {
std::cout << "Printing a generic document.\n";
}
virtual void save(const char* filename) {
std::cout << "Saving generic document to " << filename << "\n";
}
virtual ~Document() = default; // 虚析构函数很重要
};

// Report 继承自 Document
// Report 本身也禁止被进一步继承 (final class)
class Report final : public Document {
public:
// 使用 override 确保正确覆盖
void print() const override { // OK,覆盖了基类的 print
std::cout << "Printing a specific report.\n";
}

// void save(char* filename) override; // 编译错误!参数类型 const char* vs char* 不匹配

// 使用 final 阻止后续派生类覆盖 save
void save(const char* filename) override final {
std::cout << "Saving report securely to " << filename << "\n";
}

// void nonVirtualMethod() override; // 编译错误!基类没有这个虚函数
};

/*
// 编译错误!Report 被标记为 final,不能被继承
class SpecialReport : public Report {
public:
// 编译错误!save 在 Report 中被标记为 final
// void save(const char* filename) override {
// std::cout << "Trying to save special report...\n";
// }
};
*/

int main() {
Document* doc1 = new Document();
Document* doc2 = new Report();

doc1->print(); // Output: Printing a generic document.
doc2->print(); // Output: Printing a specific report. (多态)

doc1->save("doc.txt");
doc2->save("report.txt"); // 调用 Report::save

delete doc1;
delete doc2;

return 0;
}

overridefinal 提高了代码的健壮性和可维护性,使得在复杂的继承体系中更容易正确地管理虚函数,并明确设计意图。

18.4 Lambda 函数

C++11 引入了一个非常强大的特性:Lambda 表达式 (Lambda Expression)**,通常简称为 **Lambda 函数Lambda。Lambda 表达式允许我们在需要可调用对象(如函数指针、函数对象)的地方就地定义一个匿名的函数对象

Lambda 的主要目的是提供一种简洁的方式来定义简短的、通常只在局部范围内使用的函数或操作,特别是在将它们作为参数传递给 STL 算法时。

Lambda 表达式语法

Lambda 表达式的基本语法如下:

1
2
3
[capture_clause](parameters) -> return_type {
// function body
}
  • [capture_clause] (捕获子句): 这是 Lambda 表达式的开始。方括号 [] 用于指定如何从定义 Lambda 的外部作用域捕获变量(即让 Lambda 内部可以访问外部变量)。
    • []: 不捕获任何外部变量。
    • [=]: 以值拷贝方式捕获所有外部作用域中的自动变量(局部变量和参数)。
    • [&]: 以引用方式捕获所有外部作用域中的自动变量
    • [var]: 以值拷贝方式捕获指定的变量 var
    • [&var]: 以引用方式捕获指定的变量 var
    • [this]: 捕获当前对象的 this 指针(仅在类的非静态成员函数内部有效)。
    • 可以混合使用,例如 [=, &var1, &var2] (默认值捕获,但 var1var2 引用捕获),[&, var1, var2] (默认引用捕获,但 var1var2 值捕获)。
  • (parameters) (参数列表): 可选。与普通函数的参数列表类似,定义 Lambda 接受的参数。如果 Lambda 不需要参数,可以省略 ()
  • -> return_type (返回类型): 可选。用于显式指定 Lambda 的返回类型。如果省略,编译器会尝试根据函数体中的 return 语句自动推断返回类型(如果函数体只有一个 return 语句,或者所有 return 语句返回相同类型,或者没有 return 语句则推断为 void)。如果无法推断或需要特定类型,则必须显式指定。
  • { function body } (函数体): 包含 Lambda 执行的代码,与普通函数的函数体类似。

最简单的 Lambda:

1
[](){ std::cout << "Hello Lambda!\n"; } // 一个不捕获、不接受参数、无返回值的 Lambda

Lambda 的类型: 每个 Lambda 表达式都会生成一个唯一的、未命名的函数对象类型。这意味着即使两个 Lambda 表达式的文本完全相同,它们的类型也是不同的。

1
2
3
auto lambda1 = [](){};
auto lambda2 = [](){};
// decltype(lambda1) != decltype(lambda2)

我们可以将 Lambda 赋值给 auto 变量,或者存储在 std::function 包装器中。

18.4.1 比较函数指针、函数符和 Lambda 函数

在需要传递可调用实体的场景(如 STL 算法)中,我们可以使用函数指针、函数对象(函数符)或 Lambda 函数。

  • 函数指针:
    • 优点:语法简单,对于已存在的普通函数很方便。
    • 缺点:不能携带状态,通常无法内联,灵活性差。
  • 函数对象 (Functor):
    • 优点:可以携带状态(通过成员变量),可以内联,类型安全。
    • 缺点:需要单独定义一个类,代码相对冗长,特别是对于简单的操作。
  • Lambda 函数:
    • 优点:语法简洁,可以直接在调用点定义,易于阅读;可以通过捕获子句方便地访问(携带)外部状态;通常可以内联;功能强大灵活。
    • 缺点:对于复杂或需要在多处重用的逻辑,单独定义函数或函数对象可能更清晰。

示例对比 (用于 std::sort 的自定义比较):

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <vector>
#include <algorithm>
#include <iostream>

// 1. 使用函数指针
bool compareDescendingPtr(int a, int b) {
return a > b;
}

// 2. 使用函数对象
struct CompareDescendingFunctor {
bool operator()(int a, int b) const {
return a > b;
}
};

int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::vector<int> v_copy1 = v;
std::vector<int> v_copy2 = v;
std::vector<int> v_copy3 = v;

// 使用函数指针排序
std::sort(v_copy1.begin(), v_copy1.end(), compareDescendingPtr);

// 使用函数对象排序
std::sort(v_copy2.begin(), v_copy2.end(), CompareDescendingFunctor());

// 使用 Lambda 函数排序
std::sort(v_copy3.begin(), v_copy3.end(),
[](int a, int b) -> bool { return a > b; } // -> bool 可省略
);

std::cout << "Sorted (ptr): ";
for(int x : v_copy1) std::cout << x << " ";
std::cout << std::endl;

std::cout << "Sorted (functor): ";
for(int x : v_copy2) std::cout << x << " ";
std::cout << std::endl;

std::cout << "Sorted (lambda): ";
for(int x : v_copy3) std::cout << x << " ";
std::cout << std::endl;

return 0;
}

在这个例子中,Lambda 表达式提供了最简洁的语法,将比较逻辑直接写在了 sort 调用处。

18.4.2 为何使用 lambda

Lambda 表达式的主要优势在于其简洁性局部性

  1. 简洁性: 对于只需要一两行代码的简单操作或谓词,定义一个完整的函数或函数对象类显得过于繁琐。Lambda 允许用非常紧凑的语法直接表达这些逻辑。

  2. 局部性: Lambda 可以直接定义在使用它的地方(例如,作为算法的参数)。这使得代码更易于阅读和理解,因为操作逻辑和调用它的代码紧密地放在一起,不需要跳转到其他地方去查找函数或类的定义。

  3. 状态捕获: Lambda 的捕获机制提供了一种非常方便的方式来访问定义 Lambda 时所处作用域的局部变量,而无需手动将这些变量包装到函数对象中。

示例 (使用捕获):

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <vector>
#include <algorithm>
#include <iostream>

int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int threshold = 5;
std::vector<int> results;

// 使用 lambda 查找所有大于 threshold 的数,并复制到 results
// 捕获 threshold (值拷贝) 和 results (引用)
std::for_each(nums.begin(), nums.end(),
[threshold, &results](int x) {
if (x > threshold) {
results.push_back(x); // 可以修改引用捕获的 results
// threshold = 10; // 编译错误!值捕获的变量默认是 const 的
}
});

std::cout << "Numbers greater than " << threshold << ": ";
for (int r : results) {
std::cout << r << " "; // 输出: 6 7 8 9 10
}
std::cout << std::endl;

// 演示 mutable lambda (允许修改值捕获的变量的副本)
int counter = 0;
auto mutable_lambda = [counter]() mutable { // 使用 mutable
counter++; // 现在可以修改 counter 的副本了
std::cout << "Lambda counter (copy): " << counter << std::endl;
};

mutable_lambda(); // 输出 1
mutable_lambda(); // 输出 2
std::cout << "Original counter: " << counter << std::endl; // 输出 0 (原始 counter 未变)


// 演示引用捕获修改外部变量
int external_sum = 0;
std::for_each(nums.begin(), nums.end(),
[&external_sum](int x){ // 引用捕获
external_sum += x;
});
std::cout << "Sum calculated via lambda: " << external_sum << std::endl; // 输出 55

return 0;
}

mutable 关键字: 默认情况下,通过值捕获的变量在 Lambda 函数体内部是 const 的,不能被修改。如果你需要在 Lambda 内部修改值捕获变量的副本(这种修改不会影响外部原始变量),可以在参数列表 () 之后(或捕获列表 [] 之后,如果没有参数)加上 mutable 关键字。

Lambda 表达式是现代 C++ 中编写简洁、高效且易读代码的重要工具,尤其是在与 STL 算法结合使用时。

18.5 包装器

在 C++ 中,有多种“可调用 (Callable)”的实体:普通函数、函数指针、函数对象(Functors)、Lambda 表达式、类的成员函数指针等。虽然它们都可以被调用,但它们的类型各不相同。这在需要存储或传递未知类型的可调用实体时会带来不便。

例如,你可能想创建一个回调函数列表,列表中的函数可以来自不同的源(有些是普通函数,有些是 Lambda,有些是对象的成员函数),但它们都接受相同的参数并返回相同的类型。如何用一个统一的类型来存储它们呢?

C++11 在 <functional> 头文件中提供了 std::function 模板类,它是一个通用的、多态的函数包装器 (Function Wrapper)**。std::function 的对象可以存储、复制和调用**任何符合其指定函数签名的可调用实体。

18.5.1 包装器 function

std::function 的模板参数是它所要包装的函数的**签名 (Signature)**。

语法:

1
2
3
#include <functional>

std::function<ReturnType(ArgType1, ArgType2, ...)> func_wrapper;
  • ReturnType: 被包装函数的返回类型。
  • ArgType1, ArgType2, ...: 被包装函数接受的参数类型列表。

基本用法:

你可以将任何具有兼容签名的可调用实体赋值给 std::function 对象。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <functional>
#include <iostream>
#include <string>
#include <vector>

// 1. 普通函数
void print_message(const std::string& msg) {
std::cout << "Function Pointer: " << msg << std::endl;
}

// 2. 函数对象 (Functor)
struct Printer {
std::string prefix;
Printer(std::string p) : prefix(p) {}
void operator()(const std::string& msg) const {
std::cout << "Functor (" << prefix << "): " << msg << std::endl;
}
};

// 3. 类的成员函数
struct Greeter {
void greet(const std::string& name) {
std::cout << "Member Function: Hello, " << name << "!" << std::endl;
}
};

int main() {
// 定义一个可以包装 void(const std::string&) 类型函数的 std::function
std::function<void(const std::string&)> callback;

// a) 包装普通函数指针
callback = print_message;
callback("Hello from function pointer!");

// b) 包装 Lambda 表达式
callback = [](const std::string& msg) {
std::cout << "Lambda: " << msg << std::endl;
};
callback("Hello from lambda!");

// c) 包装函数对象
Printer my_printer("LOG");
callback = my_printer; // Functor 对象可以直接赋值
callback("Hello from functor!");

// d) 包装类的成员函数 (需要绑定对象)
Greeter greeter_obj;
// 使用 std::bind (或者 Lambda) 来绑定 this 指针
callback = std::bind(&Greeter::greet, &greeter_obj, std::placeholders::_1);
// 或者使用 Lambda 捕获对象
// callback = [&greeter_obj](const std::string& name){ greeter_obj.greet(name); };
callback("Alice"); // 调用 greeter_obj.greet("Alice")

// 检查 std::function 是否为空 (是否持有可调用对象)
if (callback) {
std::cout << "Callback is holding a callable entity." << std::endl;
}

// 清空 std::function
callback = nullptr;
if (!callback) {
std::cout << "Callback is now empty." << std::endl;
}

// 存储不同类型的回调函数
std::vector<std::function<void(const std::string&)>> callbacks;
callbacks.push_back(print_message);
callbacks.push_back([](const std::string& s){ std::cout << "Another Lambda: " << s << std::endl; });
callbacks.push_back(my_printer);

std::cout << "\n--- Calling stored callbacks ---" << std::endl;
for (const auto& cb : callbacks) {
cb("Test message");
}

return 0;
}

std::function 通过类型擦除 (Type Erasure) 技术实现了这种通用性。它内部可以存储不同类型的可调用对象,并在调用时通过某种机制(通常涉及堆分配和虚函数调用,特别是对于捕获了数据的 Lambda 或大型函数对象)来执行实际的调用。

18.5.1 (续) function 及模板的低效性 (潜在开销)

虽然 std::function 非常灵活,但这种灵活性是有代价的:

  1. 类型擦除开销: 为了能够存储任意类型的可调用对象,std::function 通常需要在内部处理类型信息。这可能涉及到:

    • 堆分配: 如果被包装的可调用对象(特别是带捕获的 Lambda 或函数对象)比较大,或者不能通过“小对象优化 (Small Object Optimization, SOO)”直接存储在 std::function 对象内部,就可能需要在堆上分配内存来存储它。堆分配和释放是有开销的。
    • 间接调用: 调用存储在 std::function 中的函数通常需要通过指针或虚函数进行间接调用,这比直接函数调用或模板实例化产生的内联调用要慢。
  2. 相比模板的低效性: 如果在编译时就知道具体的可调用类型,使用模板通常会更高效。模板允许编译器为每种具体的类型生成专门的代码,并且更容易进行内联优化,避免了 std::function 的类型擦除和间接调用开销。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 使用模板,更高效
    template <typename Callable>
    void process_with_template(Callable func, const std::string& data) {
    func(data); // 直接调用,可能内联
    }

    // 使用 std::function,更灵活,但可能有开销
    void process_with_function(std::function<void(const std::string&)> func, const std::string& data) {
    func(data); // 间接调用
    }

    int main() {
    auto my_lambda = [](const std::string& s){ /*...*/ };
    process_with_template(my_lambda, "data"); // 高效
    process_with_function(my_lambda, "data"); // 灵活,但可能稍慢
    return 0;
    }

何时使用 std::function

  • 当你需要在运行时确定要调用哪个函数,或者需要存储不同类型的可调用对象在同一个容器中时(如回调系统、事件处理)。
  • 当你需要定义一个接受任何符合特定签名的可调用对象的接口时。

何时避免使用 std::function (如果性能是关键)?

  • 当你在编译时就知道具体的可调用类型时,优先使用模板或直接调用。
  • 在性能极其敏感的代码路径中(如紧密循环内部),需要仔细评估 std::function 带来的开销。

18.5.2 修复问题 / 18.5.3 其他方式

这里的“修复问题”主要是指理解 std::function 的开销并根据场景选择合适的技术。“其他方式”则包括:

  1. 使用模板: 如上所述,当类型在编译时已知时,模板是最高效的选择。

  2. 使用函数指针: 对于简单的、无状态的函数,直接使用函数指针类型 ReturnType(*)(ArgTypes...) 仍然是有效的,并且开销很小。

  3. 使用 Lambda: Lambda 本身是高效的(它们是匿名的函数对象)。只有当它们被存储在 std::function 中时,才可能引入 std::function 的开销。如果可以将 Lambda 直接传递给模板化的函数(如 STL 算法),通常不会有额外开销。

  4. std::bind: (在 <functional> 中) std::bind 可以用来绑定函数的参数(包括成员函数的 this 指针)或重新排列参数顺序,生成一个新的可调用对象 (函数对象)。虽然 std::bind 也能被 std::function 存储,但现代 C++ 中,Lambda 通常提供了更简洁、更易读的方式来完成类似的任务。

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

    void func(int a, int b, int c) {
    std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
    }

    int main() {
    // 绑定第一个参数为 100,第二个参数由调用者提供 (_1),第三个参数为 300
    auto bound_func = std::bind(func, 100, std::placeholders::_1, 300);
    bound_func(200); // 调用 func(100, 200, 300)

    // 使用 Lambda 实现相同效果
    auto lambda_func = [](int b){ func(100, b, 300); };
    lambda_func(200); // 调用 func(100, 200, 300)

    return 0;
    }
  5. std::refstd::cref: (在 <functional> 中) 当你想通过值传递的包装器(如 std::bind, std::thread 构造函数, 甚至某些情况下的 std::function)传递参数的引用时,需要使用 std::ref (用于非 const 引用) 或 std::cref (用于 const 引用) 来包装参数。它们创建了一个轻量级的引用包装器对象。

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #include <functional>
    #include <iostream>
    #include <vector>
    #include <thread>

    void modify_vector(std::vector<int>& vec) { // 接受引用
    if (!vec.empty()) {
    vec[0] = 100;
    }
    }

    int main() {
    std::vector<int> my_vec = {1, 2, 3};

    // std::bind 默认按值复制参数
    // auto bound_copy = std::bind(modify_vector, my_vec); // 错误或无效,bind 复制了 vector
    // bound_copy(); // 修改的是副本

    // 使用 std::ref 传递引用
    auto bound_ref = std::bind(modify_vector, std::ref(my_vec));
    bound_ref(); // 正确调用 modify_vector(my_vec)

    std::cout << "Vector after bind+ref: " << my_vec[0] << std::endl; // 输出 100

    // std::thread 构造函数也复制参数,需要 std::ref 传递引用
    my_vec[0] = 1; // Reset
    // std::thread t1(modify_vector, my_vec); // 线程操作的是 my_vec 的副本
    std::thread t2(modify_vector, std::ref(my_vec)); // 线程操作的是 my_vec 的引用
    // t1.join();
    t2.join();
    std::cout << "Vector after thread+ref: " << my_vec[0] << std::endl; // 输出 100

    return 0;
    }

总之,std::function 是一个强大的通用函数包装器,提供了极大的灵活性,但在性能敏感的场景下需要注意其潜在开销,并考虑使用模板、函数指针或直接传递 Lambda 等替代方案。

18.6 可变参数模板

在 C++11 之前,模板(函数模板和类模板)通常只能接受固定数量的模板参数。如果你想编写一个可以接受任意数量参数的函数(类似于 C 语言中的 printf),通常需要依赖 C 风格的可变参数机制 (<cstdarg>),这种机制不是类型安全的。

C++11 引入了可变参数模板 (Variadic Templates)**,允许模板(包括函数模板和类模板)接受任意数量、任意类型**的模板参数,并在编译时进行类型安全的处理。

18.6.1 模板和函数参数包

可变参数模板的核心是**参数包 (Parameter Pack)**。参数包有两种:

  1. 模板参数包 (Template Parameter Pack): 代表零个或多个模板参数(类型参数、非类型参数或模板参数)。

    • 语法:typename... Argsclass... Args (对于类型参数包),Type... args (对于非类型参数包)。
    • ... (省略号) 是关键部分,表示这是一个参数包。
  2. 函数参数包 (Function Parameter Pack): 代表零个或多个函数参数。

    • 语法:Args... args,其中 Args 是一个模板参数包。

示例:定义一个接受任意数量参数的函数模板

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

// 定义可变参数函数模板 show_list
// Args 是一个模板参数包 (代表零个或多个类型)
// args 是一个函数参数包 (代表零个或多个对应类型的参数)
template<typename... Args>
void show_list(Args... args) {
// 如何处理 args?见下文
std::cout << "Number of arguments: " << sizeof...(Args) << std::endl;
// 或者 sizeof...(args) 也可以
}

int main() {
show_list(); // Args 为空, args 为空. 输出: Number of arguments: 0
show_list(1); // Args = {int}, args = {1}. 输出: Number of arguments: 1
show_list(1, "hello"); // Args = {int, const char*}, args = {1, "hello"}. 输出: Number of arguments: 2
show_list(1, 3.14, 'c'); // Args = {int, double, char}, args = {1, 3.14, 'c'}. 输出: Number of arguments: 3

return 0;
}

sizeof...(Args)sizeof...(args) 运算符可以在编译时获取参数包中的参数数量。

18.6.2 展开参数包

仅仅能接受任意数量的参数还不够,我们还需要一种方法来处理 (展开, Unpack) 参数包中的每一个参数。在 C++11 中,展开参数包通常需要使用递归 (Recursion) 或其他一些模板技巧。C++17 引入了更简洁的**折叠表达式 (Fold Expressions)**,但这里我们主要关注 C++11 的方法。

展开参数包的关键在于模式匹配递归调用:设计一个函数模板,它处理参数包中的第一个参数,然后用剩余的参数递归调用自身。还需要一个处理参数包为空(递归终止)的基本情况。

18.6.3 在可变参数模板函数中使用递归

最常见的展开方式是定义两个函数模板:

  1. 一个递归版本,接受至少一个参数,处理第一个参数,然后用剩余参数调用自身。
  2. 一个基本情况版本,接受零个参数,作为递归的终止条件。

示例:递归打印所有参数

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

// 基本情况:当参数包为空时调用此版本,终止递归
void print_values() {
std::cout << std::endl; // 递归结束时打印换行
}

// 递归版本:处理第一个参数,然后用剩余参数递归调用
// Args 是模板参数包 (代表剩余参数的类型)
// args 是函数参数包 (代表剩余参数的值)
template<typename T, typename... Args>
void print_values(const T& first_arg, const Args&... rest_args) {
// 1. 处理第一个参数
std::cout << first_arg;

// 2. 如果还有剩余参数,打印分隔符并递归调用
if constexpr (sizeof...(Args) > 0) { // C++17 if constexpr, 简化条件编译
std::cout << ", ";
print_values(rest_args...); // 将剩余参数包展开并传递给下一次调用
} else {
print_values(); // 调用基本情况版本,打印换行
}

// C++11 写法 (没有 if constexpr):
// if (sizeof...(Args) > 0) {
// std::cout << ", ";
// }
// print_values(rest_args...); // 递归调用,最终会调用到 print_values() 版本
}


int main() {
std::cout << "Printing values:\n";
print_values(1, "hello", 3.14, 'a'); // 输出: 1, hello, 3.14, a
print_values("Single argument"); // 输出: Single argument
print_values(); // 输出: (空行)

return 0;
}

调用过程分析 print_values(1, "hello", 3.14, 'a'):

  1. 调用 print_values<int, const char*, double, char>(1, "hello", 3.14, 'a')
    • first_arg = 1, rest_args... = {“hello”, 3.14, ‘a’}
    • 输出 “1, “
    • 递归调用 print_values("hello", 3.14, 'a')
  2. 调用 print_values<const char*, double, char>("hello", 3.14, 'a')
    • first_arg = “hello”, rest_args... = {3.14, ‘a’}
    • 输出 “hello, “
    • 递归调用 print_values(3.14, 'a')
  3. 调用 print_values<double, char>(3.14, 'a')
    • first_arg = 3.14, rest_args... = {‘a’}
    • 输出 “3.14, “
    • 递归调用 print_values('a')
  4. 调用 print_values<char>('a')
    • first_arg = ‘a’, rest_args... = {} (空包)
    • 输出 “a”
    • 递归调用 print_values()
  5. 调用 print_values() (基本情况)
    • 输出 std::endl
    • 递归结束

展开语法 rest_args...: 当在函数调用中对函数参数包使用 ... 时,它会将包中的每个元素展开,作为独立的参数传递给函数。

其他展开技巧 (C++11):

虽然递归是最常见的方式,但也可以使用其他技巧,例如利用初始化列表或数组的逗号运算符特性来展开,但这些技巧通常更复杂且可读性较差。

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

// 使用初始化列表展开 (技巧性较强)
template<typename... Args>
void print_values_trick(const Args&... args) {
// 创建一个临时 int 数组,利用初始化列表和逗号运算符
// 对包中的每个参数执行 (std::cout << arg << " ", 0)
// 结果是 {0, 0, 0, ...},但副作用是打印了参数
int dummy[] = {0, ( (std::cout << args << " "), 0 )... };
// (void)dummy; // 避免未使用变量警告 (可选)
std::cout << std::endl;
}

int main() {
print_values_trick(10, "world", 0.5); // 输出: 10 world 0.5
return 0;
}

这种技巧利用了 C++11 中 ... 可以在表达式中展开参数包的特性 (pattern)...,其中 pattern 会对包中的每个元素应用一次。

可变参数模板是 C++ 元编程和泛型编程的强大工具,使得编写能够处理任意数量参数的类型安全函数(如自定义的 printfmake_tupleemplace_back 等)成为可能。

18.7 C++11新增的其他功能

除了前面章节已经详细讨论或回顾的主要特性(如 auto, nullptr, 统一初始化, 移动语义, Lambda, 智能指针, 范围 for, 新类功能, 可变参数模板等)之外,C++11 标准还引入了许多其他重要的功能和库,进一步增强了语言的能力。本节将简要介绍其中的一些。

18.7.1 并行编程

C++11 首次在标准库层面提供了对多线程并发编程的支持,主要定义在 <thread>, <mutex>, <condition_variable>, <future>, <atomic> 等头文件中。

  • std::thread: 用于创建和管理线程。允许函数或可调用对象在独立的线程中执行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <thread>
    #include <iostream>

    void worker_thread() {
    std::cout << "Worker thread running.\n";
    }

    int main() {
    std::thread t(worker_thread); // 创建新线程并执行 worker_thread
    std::cout << "Main thread running.\n";
    t.join(); // 等待 worker_thread 执行完毕
    return 0;
    }
  • 互斥量 (std::mutex, std::lock_guard, std::unique_lock): 用于保护共享数据,防止多个线程同时访问导致的数据竞争。lock_guardunique_lock 提供了 RAII 风格的锁管理,确保互斥量能被正确释放。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <mutex>
    #include <vector>
    #include <thread>

    std::mutex data_mutex;
    std::vector<int> shared_data;

    void add_data(int val) {
    std::lock_guard<std::mutex> lock(data_mutex); // 自动加锁,离开作用域时自动解锁
    shared_data.push_back(val);
    }
  • 条件变量 (std::condition_variable): 用于线程间的同步,允许一个线程等待某个条件变为真(由另一个线程通知)。通常与 std::mutexstd::unique_lock 配合使用。
  • 原子操作 (std::atomic<T>): 提供对基本类型的原子操作(如读取、写入、增减、比较交换),保证这些操作在多线程环境下不会被打断,避免了数据竞争,通常比使用互斥量更高效。
    1
    2
    3
    #include <atomic>
    std::atomic<int> counter = 0;
    // counter++; // 原子地自增
  • 异步操作 (std::async, std::future, std::promise): 提供了一种更高级的并发模型,允许异步地执行任务并获取其结果。std::async 可以启动一个异步任务,返回一个 std::future 对象,通过 future 可以在稍后获取任务的返回值或等待任务完成。promise 则用于在一个线程中设置值或异常,供另一个线程通过关联的 future 获取。

C++11 的并发支持使得编写可移植的多线程程序成为可能。

18.7.2 新增的库

C++11 标准库增加了几个实用的新组件:

  • <chrono>: 提供了处理时间时间段的类型安全、精确的库。包括时钟 (system_clock, steady_clock, high_resolution_clock)、时间点 (time_point) 和时间段 (duration)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <chrono>
    #include <iostream>
    #include <thread> // for sleep_for

    int main() {
    auto start = std::chrono::high_resolution_clock::now();
    std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 休眠 50 毫秒
    auto end = std::chrono::high_resolution_clock::now();

    std::chrono::duration<double, std::milli> elapsed = end - start; // 计算毫秒差
    std::cout << "Elapsed time: " << elapsed.count() << " ms\n";
    return 0;
    }
  • <random>: 提供了比 C 风格 rand() 更强大、更灵活的随机数生成工具。包括多种随机数引擎(如 mt19937)和分布(如 uniform_int_distribution, normal_distribution)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <random>
    #include <iostream>

    int main() {
    std::random_device rd; // 用于生成种子 (可能基于硬件)
    std::mt19937 gen(rd()); // Mersenne Twister 引擎
    std::uniform_int_distribution<> distrib(1, 6); // 均匀分布 [1, 6]

    std::cout << "Rolling a die: " << distrib(gen) << std::endl; // 生成随机数
    return 0;
    }
  • <regex>: 提供了对正则表达式 (Regular Expressions) 的支持,用于强大的文本模式匹配和搜索/替换。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <regex>
    #include <string>
    #include <iostream>

    int main() {
    std::string text = "Email: example@test.com";
    std::regex email_regex(R"(\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b)"); // 原始字符串字面量
    std::smatch match;

    if (std::regex_search(text, match, email_regex)) {
    std::cout << "Found email: " << match.str(0) << std::endl;
    }
    return 0;
    }
  • <tuple>: 提供了元组 (Tuple) 类型 std::tuple,可以看作是匿名的、固定大小的异构值集合,类似于 std::pair 的泛化。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <tuple>
    #include <string>
    #include <iostream>

    int main() {
    std::tuple<int, std::string, double> t1(10, "hello", 3.14);
    std::cout << "Tuple element 0: " << std::get<0>(t1) << std::endl; // 使用 std::get<i> 访问
    std::cout << "Tuple element 1: " << std::get<1>(t1) << std::endl;
    // C++17 结构化绑定更方便: auto [id, name, value] = t1;
    return 0;
    }

18.7.3 低级编程

C++11 也增强了对底层内存布局和编译时计算的支持:

  • 对齐控制 (alignas, alignof):
    • alignas: 用于指定变量或类型的内存对齐要求
    • alignof: 用于查询类型或对象的对齐要求(返回一个 size_t)。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      #include <iostream>

      struct alignas(16) AlignedStruct { // 要求按 16 字节对齐
      char data[32];
      };

      int main() {
      std::cout << "Alignment of int: " << alignof(int) << std::endl;
      std::cout << "Alignment of AlignedStruct: " << alignof(AlignedStruct) << std::endl; // 输出 16
      return 0;
      }
  • constexpr: 用于声明函数或变量可以在编译时求值。constexpr 函数可以在编译时用于常量表达式,也可以在运行时像普通函数一样使用。constexpr 变量必须用常量表达式初始化。这使得更多的计算可以在编译阶段完成,提高了运行时性能并增强了元编程能力。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <array>

    // constexpr 函数,可在编译时计算
    constexpr int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
    }

    int main() {
    constexpr int five_factorial = factorial(5); // 编译时计算结果为 120
    std::array<int, factorial(4)> arr; // 数组大小在编译时确定为 24

    int runtime_val = 6;
    int six_factorial = factorial(runtime_val); // 运行时计算

    return 0;
    }

18.7.4 杂项

  • 用户定义字面量 (User-defined Literals): 允许程序员为字面量(如 "hello", 123, 3.14)定义后缀,从而创建具有特定类型或意义的对象。后缀通常以下划线 _ 开头。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <chrono>
    #include <iostream>

    // 定义一个将秒转换为毫秒的用户定义字面量后缀 _ms
    constexpr std::chrono::milliseconds operator"" _ms(unsigned long long ms) {
    return std::chrono::milliseconds(ms);
    }

    int main() {
    auto duration = 150_ms; // duration 的类型是 std::chrono::milliseconds,值为 150ms
    std::cout << "Duration: " << duration.count() << "ms\n";
    return 0;
    }
  • 原始字符串字面量 (Raw String Literals): 用于简化包含大量特殊字符(如反斜杠 \、引号 ")的字符串的定义,避免了繁琐的转义。
    • 语法:R"delimiter(raw_characters)delimiter"
    • delimiter 是可选的分隔符序列(不能包含括号、反斜杠或空格),用于区分字符串内容和结束标记。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      #include <string>
      #include <iostream>

      int main() {
      // 无需转义反斜杠和引号
      std::string path = R"(C:\Program Files\My App\data.txt)";
      std::string html = R"delimiter(
      <html>
      <head><title>"Example"</title></head>
      <body>Hello, world!</body>
      </html>
      )delimiter";

      std::cout << path << std::endl;
      std::cout << html << std::endl;
      return 0;
      }

这些只是 C++11 新增功能的一部分,它们共同构成了现代 C++ 的基础,使得 C++ 成为一门功能更强大、表达力更强、更易于使用的编程语言。

18.8 语言变化

C++ 是一门不断发展的语言。从 C++98/03 到 C++11,再到后续的 C++14, C++17, C++20 等标准,语言本身和标准库都在持续演进,以满足现代软件开发的需求。C++11 是一个里程碑式的版本,引入了大量重要特性,其中许多特性并非凭空出现,而是在标准化之前经过了社区的广泛讨论和实践检验。Boost 库和 TR1 在这一过程中扮演了重要角色。

18.8.1 Boost 项目

Boost C++ 库 (Boost C++ Libraries) 是一个广受推崇的、高质量的、经过同行评审的 C++ 库集合。它由 C++ 社区的众多开发者共同维护和贡献。

  • 目的: Boost 旨在提供各种通用和特定领域的库,扩展 C++ 标准库的功能。它既包含可以直接使用的工具(如日期时间处理、文件系统操作、正则表达式、测试框架等),也包含一些实验性的、可能在未来被纳入 C++ 标准的库。
  • 影响力: Boost 对 C++ 标准的发展产生了深远影响。许多 C++11 及后续标准中的新特性(如智能指针 shared_ptr、函数包装器 function、线程库、正则表达式库、元组 tuplearray 等)都起源于 Boost 库中的对应组件,并在 Boost 中得到了广泛的应用和验证。
  • “试炼场”: Boost 常常被视为 C++ 新特性的“试炼场”。一个库如果在 Boost 中被证明是稳定、有用且设计良好的,那么它被 C++ 标准委员会考虑并最终纳入官方标准的可能性就会大大增加。
  • 许可证: Boost 库通常采用非常宽松的 Boost Software License,允许在商业和非商业项目中自由使用。

即使某些功能已被纳入 C++ 标准库,Boost 仍然提供了标准库中尚未包含的许多有用工具,并且有时会提供比标准库版本更早或功能更丰富的实现。

18.8.2 TR1 (Technical Report 1)

**TR1 (Technical Report 1)**,正式名称是 ISO/IEC TR 19768:2007,是 C++ 标准委员会发布的一份技术报告,旨在扩展 C++03 标准库。它并不是 C++ 标准本身的一部分,但它定义了一系列推荐添加的库组件。

  • 内容: TR1 包含了许多后来被正式纳入 C++11 标准库的特性,其中大部分源自 Boost 库。主要内容包括:
    • 引用包装器 (ref, cref)
    • 智能指针 (shared_ptr, weak_ptr - unique_ptr 是 C++11 新设计的)
    • 函数对象包装器 (function)
    • 函数对象绑定器 (bind)
    • 类型萃取 (<type_traits>)
    • 随机数生成 (<random>)
    • 元组 (<tuple>)
    • 固定大小数组 (<array>)
    • 哈希表容器 (<unordered_set>, <unordered_map>)
    • 正则表达式 (<regex>)
  • 命名空间: TR1 中的组件通常被放置在 std::tr1 命名空间下。例如,TR1 的智能指针是 std::tr1::shared_ptr
  • 过渡角色: TR1 起到了一个重要的过渡作用。它允许编译器厂商和库开发者在 C++11 标准正式发布之前,就开始实现和提供这些即将标准化的重要库功能,让用户可以提前体验和使用。当 C++11 标准发布后,这些组件被正式移入了 std 命名空间,std::tr1 命名空间则逐渐被废弃。

了解 Boost 和 TR1 有助于理解 C++11 中许多库特性的来源和演变过程。它们展示了 C++ 社区驱动语言发展的模式:通过第三方库(如 Boost)进行探索和实践,通过技术报告(如 TR1)进行预标准化,最终将成熟的特性纳入官方标准。

18.8.3 使用 Boost

虽然许多 Boost 库的功能已被 C++11 及后续标准吸收,但 Boost 仍然是一个非常有价值的资源库。

如何使用 Boost (概念性步骤):

  1. 下载: 从 Boost 官方网站 (boost.org) 下载最新的 Boost 发行版。
  2. 解压: 将下载的压缩包解压到你选择的目录。
  3. 编译 (部分库需要): Boost 中的许多库是仅头文件 (Header-only) 的,这意味着你只需要在你的代码中 #include 相应的头文件,并将 Boost 的根目录添加到编译器的包含路径 (Include Path) 中即可使用。然而,也有一些库(如 Boost.Thread, Boost.Filesystem, Boost.Regex, Boost.Program_options 等)需要编译成静态库或动态库文件。
    • 编译通常需要运行 Boost 提供的 bootstrap 脚本(根据你的操作系统选择 .bat.sh),然后运行生成的 b2 (或 bjam) 构建工具。你需要指定你的编译器、构建类型(debug/release)、链接方式(static/shared)等。具体步骤请参考 Boost 官方文档。
  4. 配置项目:
    • 包含路径: 在你的项目设置中,将 Boost 库的根目录添加到编译器的头文件搜索路径。
    • 库路径 (如果编译了库): 将生成的库文件所在的目录添加到链接器的库文件搜索路径。
    • 链接库 (如果编译了库): 将你需要使用的 Boost 库文件(如 libboost_thread-vc142-mt-gd-x64-1_79.lib 等,文件名会根据版本、编译器、配置等变化)添加到链接器的输入中。
  5. 包含头文件: 在你的 C++ 代码中使用 #include <boost/xxx.hpp> 来包含所需的 Boost 头文件。

示例 (使用 Boost.Lexical_Cast):
Boost.Lexical_Cast 是一个仅头文件的库,用于字符串和数值之间的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 假设 Boost 根目录已添加到包含路径
#include <boost/lexical_cast.hpp>
#include <string>
#include <iostream>

int main() {
try {
std::string s = "12345";
int i = boost::lexical_cast<int>(s);
std::cout << "String '" << s << "' cast to int: " << i << std::endl;

double d = 3.14159;
std::string sd = boost::lexical_cast<std::string>(d);
std::cout << "Double " << d << " cast to string: '" << sd << "'" << std::endl;

std::string bad_s = "hello";
int bad_i = boost::lexical_cast<int>(bad_s); // 会抛出 boost::bad_lexical_cast 异常
} catch (const boost::bad_lexical_cast& e) {
std::cerr << "Lexical cast error: " << e.what() << std::endl;
}
return 0;
}

使用 Boost 库可以极大地扩展 C++ 的能力,但需要注意正确配置编译和链接环境。建议详细阅读你所使用的具体 Boost 库的文档。

18.9 总结

本章深入探讨了 C++11 标准引入的一系列重要特性,这些特性极大地改变了 C++ 的编程方式,使其更高效、更安全、更易用。

主要内容回顾:

  1. C++11 特性回顾: 复习了之前章节已介绍的 C++11 功能,包括新类型 (long long, char16_t, char32_t, nullptr)、统一初始化 ({}), 声明 (auto, decltype)、智能指针 (unique_ptr, shared_ptr, weak_ptr)、异常规范 (noexcept)、作用域内枚举 (enum class) 以及对类、模板和 STL 的改进(如范围 forarray、无序容器、initializer_list、模板别名 using)。

  2. 移动语义和右值引用:

    • 右值引用 (&&): 引入用于区分即将销毁的临时对象(右值)和持久对象(左值)。
    • 移动语义: 允许从右值或标记为可移动的对象那里“窃取”资源(如动态内存指针),而不是进行昂贵的复制。
    • 移动构造函数和移动赋值运算符: 特殊成员函数,接收右值引用参数,实现资源转移并将源对象置于有效的空状态。应标记为 noexcept
    • std::move: 将一个左值强制转换为右值引用类型,以触发移动语义(但本身不执行移动)。使用后不应对源对象的值做假设。
  3. 新的类功能:

    • 特殊成员函数控制 (= default, = delete): 显式要求编译器生成默认实现或禁用特定成员函数(特别是复制/移动操作)。
    • 委托构造函数: 允许一个构造函数调用同一类的另一个构造函数,减少代码重复。
    • 继承构造函数 (using Base::Base;): 允许派生类继承基类的构造函数,简化派生类编写。
    • 虚函数管理 (override, final): override 确保派生类方法正确覆盖基类虚函数;final 阻止虚函数被进一步覆盖或类被继承。
  4. Lambda 函数:

    • 提供简洁的语法,用于在需要可调用对象的地方就地定义匿名函数对象
    • [capture](params) -> ret {body}: 包含捕获子句、参数列表(可选)、返回类型(可选)和函数体。
    • 捕获: 可以通过值 (=, var) 或引用 (&, &var) 捕获外部作用域的变量。mutable 关键字允许修改值捕获变量的副本。
    • 常用于 STL 算法,提高代码的局部性和可读性。
  5. 包装器 (std::function):

    • 定义在 <functional> 中的通用函数包装器。
    • 可以存储、复制和调用任何具有兼容签名的可调用实体(函数指针、Lambda、函数对象、成员函数等)。
    • 通过类型擦除实现,提供了灵活性,但在性能敏感场景下可能有开销(堆分配、间接调用)。
    • 替代方案包括模板(编译时已知类型时更高效)、函数指针、直接传递 Lambda。
  6. 可变参数模板:

    • 允许函数模板和类模板接受任意数量、任意类型的模板参数(模板参数包 typename... Args)和函数参数(函数参数包 Args... args)。
    • 使用 sizeof...(Args) 获取参数数量。
    • 通常需要通过递归或 C++17 的折叠表达式来展开参数包,以处理每个参数。
  7. C++11 新增的其他功能:

    • 并发编程: 标准库支持 (<thread>, <mutex>, <atomic>, <future> 等)。
    • 新库: <chrono> (时间), <random> (随机数), <regex> (正则表达式), <tuple> (元组)。
    • 低级编程: 对齐控制 (alignas, alignof), 编译时计算 (constexpr)。
    • 杂项: 用户定义字面量, 原始字符串字面量 (R"(...)")。
  8. 语言变化:

    • 强调了 C++ 语言的持续演进。
    • 介绍了 Boost 库作为 C++ 新特性的试验场和重要补充。
    • 介绍了 TR1 作为 C++11 库特性的前身和过渡。

C++11 是 C++ 发展的一个重要分水岭,它引入的这些特性深刻地影响了现代 C++ 的编程风格和实践。

评论