ZaynPei Lv6

std::optional (可选类型)

std::optional 是 C++17 引入的一个“包装器”类型(在 头文件中)。它解决了一个在 C++ 中存在已久的古老问题:一个函数或变量如何表示它“可能没有值”? > 包装器类型指的是一种数据结构,它可以“包装”一个值,使其具有额外的语义或功能。

它的设计理念是:通过将一个类型 T 包装在 std::optional 中,可以明确地表示“这个值可能存在也可能不存在”。这比使用裸指针(如 T*)或特殊的哨兵值(如 -1 或 nullptr)更安全、更直观。

std::optional 简单地封装了一个 T 类型的对象和一个 bool 标记

  • 如果它包含值,你可以像普通 T 一样访问它
  • 如果它不包含值(即“为空”),它就是 std::nullopt 状态。

它在语义上清晰地表达了:“我是一个 T,或者我什么都不是”。并且在内部进行优化:它的大小通常就是 sizeof(T) 加上一个 bool(以及一些对齐字节)。它不会在堆上分配内存(除非 T 自己这么做)。

其函数接口包括: - 构造函数: - std::optional<T> opt; // 默认构造,表示“空”状态 - std::optional<T> opt(value); // 构造时提供一个 T 类型的值 - 检查状态: - bool has_value() const; // 检查是否包含值 - explicit operator bool() const; // 可以作为 bool 使用,表示是否有值, 例如 if(opt) {...} - 访问值: - T& value(); // 返回对包含值的引用,如果为空则抛出异常 - const T& value() const; // 常量版本 - T& operator*(); // 解引用操作符,返回对值的引用(不检查是否有值) - const T& operator*() const; // 常量版本 - T* operator->(); // 指针访问操作符,返回指向值的指针(不检查是否有值) - const T* operator->() const; // 常量版本 - 其他有用的方法: - T value_or(const T& default_value) const; // 如果有值则返回值,否则返回提供的默认值 - void reset(); // 重置为“空”状态

下面是一个使用 std::optional 的简单示例:

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
#include <iostream>
#include <optional> // 1. 引入头文件
#include <string>
#include <map>

// 一个函数,尝试根据ID查找用户名
std::optional<std::string> find_username(int user_id, const std::map<int, std::string>& db) {
auto it = db.find(user_id);

if (it == db.end()) {
// 2. 如果没找到,返回 "空" 状态
return std::nullopt;
}

// 3. 如果找到了,返回包含值的 optional
return it->second;
}

int main() {
std::map<int, std::string> user_database = {
{101, "Alice"},
{102, "Bob"}
};

// --- 示例 1: 成功查找到 ---
std::optional<std::string> user_101 = find_username(101, user_database);

// 4. 检查是否有值 (两种方式)
if (user_101.has_value()) { // 方式 A: has_value()
std::cout << "ID 101: " << user_101.value() << std::endl; // 5. 安全地取值
}

if (user_101) { // 方式 B: 直接作为 bool 判断 (推荐)
std::cout << "ID 101: " << *user_101 << std::endl; // 6. 像指针一样解引用
}

// --- 示例 2: 未查找到 ---
std::optional<std::string> user_103 = find_username(103, user_database);

if (!user_103) { // 直接判断 "空"
std::cout << "ID 103: Not found." << std::endl;
}

// --- 示例 3: 带默认值的安全获取 ---
// value_or(): 如果有值,返回值;如果为空,返回你提供的默认值。
std::string user_103_name = user_103.value_or("Guest");
std::cout << "ID 103 login as: " << user_103_name << std::endl;

// --- 示例 4: 危险的取值 (!! 注意 !!) ---
try {
// .value() 在 "空" 状态下调用,会抛出 std::bad_optional_access 异常
std::cout << user_103.value() << std::endl;
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << std::endl;
}

// * (解引用) 在 "空" 状态下调用,是 未定义行为 (Undefined Behavior),程序可能会崩溃!
// std::cout << *user_103 << std::endl; // (!! 千万不要这么做 !!)
}

std::variant<Ts…> (变体类型)

std::optional 是 T 或 空,而 std::variant 是 T 或 U 或 V…。它们共同构成了现代 C++ 中强大的代数数据类型工具。

std::variant<T, U, ...> 是 C++17 引入的一个模板类(在 头文件中),它代表一个“类型安全的 union”。一个 variant 对象在任何时刻都只能持有其模板参数列表(T, U, …)中某一种类型的值。

回忆一下传统的 union: union(共用体)就是几个成员共享同一块内存。写入一个成员会覆盖其他成员的数据, 在同一时刻,只有最后一次写入的成员是“有效”的. 也就是说, union 的目的就是节省内存。它让多个可能互斥使用的变量共用同一块存储空间

1
2
3
4
5
6
7
8
9
10
11
12
union U {
int i;
float f;
char c;
};
// ┌──────────────────────────┐
// │ 共享的内存区域(4字节) │ ← sizeof(U) = max(sizeof(i), sizeof(f), sizeof(c))
// └──────────────────────────┘
U u;
u.i = 42; // 现在 u 持有一个 int
u.f = 3.14f; // 现在 u 持有一个 float,之前的 int 值被覆盖
printf("%d\n", u.i); // 未定义行为!因为 u 现在持有 float
而这种实现是有缺陷的:

  • 类型不安全:C++ 编译器(或运行时)不知道 union 当前存储的是 int 还是 float。如果你存入一个 int,却试图按 float 读出来,这是未定义行为 (Undefined Behavior)。你需要一个额外的变量(如 enum)来手动跟踪当前激活的成员
  • 限制严格:不能持有非平凡(non-POD)类型,比如 std::string 或 std::vector(因为不知道该调用哪个析构函数)。

std::variant 完美地解决了上述问题:它在语义上表达了:“我的值要么是 T,要么是 U,要么是…”。

  • 类型安全:variant 始终知道它当前持有的是哪种类型。你不能错误地访问它(访问前必须检查,或者使用安全的 std::get_if)。
  • 支持复杂类型:它可以安全地持有 std::string、std::vector 等,因为它知道在销毁或切换类型时该调用哪个析构函数。

函数接口包括: - 构造函数: - std::variant<Ts...> var; // 默认构造,持有第一个类型的默认值 - std::variant<Ts...> var(value); // 构造时提供某个类型的值 - 检查当前类型: - std::size_t index() const; // 返回当前持有的类型在模板参数列表中的索引 - bool holds_alternative<T>() const; // 检查当前是否持有类型 T - std::variant_npos:一个常量,表示“无效索引”。 - 访问值: - T& std::get<T>(var); // 返回对持有的 T 类型值的引用,如果当前不是 T 则抛出异常 - const T& std::get<T>(const var); // 常量版本 - T* std::get_if<T>(&var); // 返回指向 T 类型值的指针,如果当前不是 T 则返回 nullptr - const T* std::get_if<T>(const &var); // 常量版本

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 <iostream>
#include <variant> // 1. 引入头文件
#include <string>

// 定义一个 "返回值" 类型,它要么是一个 int (错误码),要么是一个 string (数据)
using HttpResult = std::variant<int, std::string>;

HttpResult fetch_data(bool success) {
if (success) {
// 2. 构造并返回一个 string
return "Here is your data!";
} else {
// 3. 构造并返回一个 int
return 404;
}
}

int main() {
HttpResult result_ok = fetch_data(true);
HttpResult result_fail = fetch_data(false);

// --- 示例 1: 检查当前激活的类型 (std::holds_alternative) ---
if (std::holds_alternative<std::string>(result_ok)) { // 如果持有 string, 返回 true
std::cout << "Success: " << std::get<std::string>(result_ok) << std::endl; // 4. 安全获取
}

if (std::holds_alternative<int>(result_fail)) {
std::cout << "Failure Code: " << std::get<int>(result_fail) << std::endl;
}

// --- 示例 2: 按索引访问 (std::get) ---
// 索引 0 对应 int, 索引 1 对应 string
std::cout << "Failure Index: " << result_fail.index() << std::endl; // .index() 返回 0

// 注意:如果类型不匹配,std::get 会抛出 std::bad_variant_access 异常
try {
std::get<std::string>(result_fail); // result_fail 当前是 int
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << std::endl;
}

// --- 示例 3: 安全地按类型获取指针 (std::get_if) (推荐!) ---
// 这是最安全的方式:如果类型匹配,返回值的指针;否则返回 nullptr。
if (int* code = std::get_if<int>(&result_fail)) {
std::cout << "Safe get failure: " << *code << std::endl;
}

if (std::string* data = std::get_if<std::string>(&result_ok)) {
std::cout << "Safe get success: " << *data << std::endl;
}

// --- 示例 4: 使用 "访问者" (std::visit) (最强大的方式!) ---
// std::visit 接受一个 "可调用对象" (如 lambda) 和一个 variant
// 它会自动根据 variant 的当前类型,调用 lambda 对应的重载 (或模板)

std::cout << "Visiting result_ok: ";
std::visit([](const auto& value) {
// 使用 C++14 的泛型 lambda (auto)
using T = std.decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "It's an int: " << value << std::endl;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "It's a string: " << value << std::endl;
}
}, result_ok);

std::cout << "Visiting result_fail: ";
std::visit([](const auto& value) {
if constexpr (std::is_same_v<T, int>) {
std::cout << "It's an int: " << value << std::endl;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "It's a string: " << value << std::endl;
}
}, result_fail);

// (C++20 中使用 std::visit 和重载的 lambda 集合会更优雅)
}

std::string_view:零拷贝的字符串“视图”

std::string_view(在 头文件中)是一个 C++17 引入的只读 (read-only) 视图类,它提供了对已存在的字符串数据的非拥有 (non-owning) 引用。

简单来说:它不是一个字符串,它只是指向某处字符串的“窗口”。它是一个轻量级的对象(通常只有 16 字节:一个 const char* 指针和一个 size_t 长度)。

  • 当它从 std::string 构造时,它不拷贝字符串数据,它只是指向 std::string 内部的数据缓冲区。
  • 当它从 const char* 构造时,它不拷贝字符串数据,它只是指向那个 C 风格字符串。
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
#include <iostream>
#include <string_view> // 1. 引入头文件
#include <string>

// 2. 将参数改为 std::string_view
void print_string(std::string_view sv) {
std::cout << sv << std::endl;
}

int main() {
// --- 场景 1: 传入字面量 ---
// "Hello, World!" (const char*)
// sv 会直接指向这个字面量,没有堆分配,没有拷贝。
print_string("Hello, World!");

// --- 场景 2: 传入 std::string ---
std::string s1 = "Hello, std::string!";
// sv 会指向 s1 的内部缓冲区,没有堆分配,没有拷贝。
print_string(s1);

// --- 场景 3: 传入子字符串 ---
std::string large_message = "HEADER:Some very large data payload...";

// 3. std::string_view 也有 .substr()
// 这里的 .substr() 不会分配内存!
// 它只会返回一个新的 string_view,其内部指针指向 'H',长度为 6。
std::string_view header_view = large_message;
print_string(header_view.substr(0, 6));

// 你甚至可以直接从 std::string 构造 string_view
print_string(std::string_view(large_message.data(), 6));
}

并且, std::string_view 提供了与 std::string 几乎一致的只读 API,例如:

  • size(), length(), empty()
  • data()
  • operator[], front(), back()
  • substr(), remove_prefix(), remove_suffix()
  • find(), rfind(), find_first_of() 等。

不过它没有所有修改字符串的 API(如 push_back, append, clear)。

结构化绑定 (Structured Bindings)

结构化绑定是 C++17 引入的一种语法,它允许你用一个声明来解包一个“复合”对象(如 pair、tuple、struct 或 array)的成员,并将它们绑定到多个新的局部变量上。

它的核心语法是:auto [var1, var2, ...] = object;

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
#include <iostream>
#include <tuple>
#include <map>
int main() {
// --- 示例 1: 结构化绑定 std::pair ---
std::map<std::string, int> age_map = {
{"Alice", 30},
{"Bob", 25}
};

for (const auto& [name, age] : age_map) { // 1. 结构化绑定
std::cout << name << " is " << age << " years old." << std::endl;
}

// --- 示例 2: 结构化绑定 std::tuple ---
std::tuple<std::string, int, double> person = {"Charlie", 28, 75.5};

auto [person_name, person_age, person_weight] = person; // 2. 结构化绑定
std::cout << person_name << " is " << person_age << " years old and weighs " << person_weight << " kg." << std::endl;

// --- 示例 3: 结构化绑定自定义 struct ---
struct Point {
int x;
int y;
};

Point p = {10, 20};
auto [px, py] = p; // 3. 结构化绑定
std::cout << "Point coordinates: (" << px << ", " << py << ")" << std::endl;
}

if constexpr (编译时条件判断)

已有介绍, 不再赘述