C++17 被认为是自 C++11 以来最重要的版本,它并非一场革命,而是一次全面的"生活质量"提升。此版本专注于简化日常编码、增强语言表达力、提高代码安全性与性能,为开发者带来了大量实用工具,并为 C++20 的重大变革奠定了坚实基础。 本文将深入探讨 C++17 的核心特性,通过丰富的代码示例和实践场景,助您全面掌握这一现代 C++ 的关键版本。
- C++17的新特性
- 核心语言特性增强
- [1. 结构化绑定 (Structured Bindings)](#1. 结构化绑定 (Structured Bindings) "#1-%E7%BB%93%E6%9E%84%E5%8C%96%E7%BB%91%E5%AE%9A-structured-bindings")
- [2. if 和 switch 初始化语句](#2. if 和 switch 初始化语句 "#2-if-%E5%92%8C-switch-%E5%88%9D%E5%A7%8B%E5%8C%96%E8%AF%AD%E5%8F%A5")
- [3. 内联变量 (Inline Variables)](#3. 内联变量 (Inline Variables) "#3-%E5%86%85%E8%81%94%E5%8F%98%E9%87%8F-inline-variables")
- [4. 折叠表达式 (Fold Expressions)](#4. 折叠表达式 (Fold Expressions) "#4-%E6%8A%98%E5%8F%A0%E8%A1%A8%E8%BE%BE%E5%BC%8F-fold-expressions")
- [5. constexpr lambda 表达式](#5. constexpr lambda 表达式 "#5-constexpr-lambda-%E8%A1%A8%E8%BE%BE%E5%BC%8F")
- [6. 类模板参数推导 (CTAD - Class Template Argument Deduction)](#6. 类模板参数推导 (CTAD - Class Template Argument Deduction) "#6-%E7%B1%BB%E6%A8%A1%E6%9D%BF%E5%8F%82%E6%95%B0%E6%8E%A8%E5%AF%BC-ctad---class-template-argument-deduction")
- [7. std::optional](#7. std::optional "#7-stdoptional")
- [8. std::variant](#8. std::variant "#8-stdvariant")
- [9. std::string_view](#9. std::string_view "#9-stdstring_view")
- [10. std::filesystem](#10. std::filesystem "#10-stdfilesystem")
- [11. 嵌套命名空间 (Nested Namespaces)](#11. 嵌套命名空间 (Nested Namespaces) "#11-%E5%B5%8C%E5%A5%97%E5%91%BD%E5%90%8D%E7%A9%BA%E9%97%B4-nested-namespaces")
- [12. if constexpr](#12. if constexpr "#12-if-constexpr")
- [13. 属性扩展 (Attribute Extensions)](#13. 属性扩展 (Attribute Extensions) "#13-%E5%B1%9E%E6%80%A7%E6%89%A9%E5%B1%95-attribute-extensions")
- 总结
- 核心语言特性增强
核心语言特性增强
1. 结构化绑定 (Structured Bindings)
允许使用 auto
关键字一次性声明多个变量,并从元组、结构体或数组中直接解构赋值,极大提升了代码的可读性和简洁性。
C++17 之前
cpp
#include <tuple>
#include <iostream>
std::tuple<int, std::string, double> get_student() {
return {1, "Alice", 3.8};
}
int main() {
auto student = get_student();
int id = std::get<0>(student);
std::string name = std::get<1>(student);
double gpa = std::get<2>(student);
std::cout << "ID: " << id << ", Name: " << name << ", GPA: " << gpa << '\n';
}
使用 C++17 结构化绑定
cpp
#include <iostream>
#include <tuple>
#include <map>
#include <string>
struct Point { double x, y; };
int main() {
// 解构元组 (Tuple)
auto [id, name, gpa] = std::make_tuple(1, "Alice", 3.8);
std::cout << "ID: " << id << ", Name: " << name << ", GPA: " << gpa << '\n';
// 2. 解构结构体/类 (Struct/Class)
Point p{3.5, 7.2};
auto& [x_ref, y_ref] = p; // 可以绑定为引用,直接修改原对象
x_ref = 4.0;
std::cout << "Point: (" << p.x << ", " << p.y << ")\n"; // 输出 (4, 7.2)
// 3. 解构原生数组 (C-style Array)
int arr[] = {10, 20, 30};
auto [a, b, c] = arr;
std::cout << "Array: " << a << ", " << b << ", " << c << '\n';
// 4. 在循环中解构 map 元素 (常用场景)
std::map<std::string, int> scores{{"Bob", 85}, {"Carol", 92}};
for (const auto& [student_name, score] : scores) {
std::cout << student_name << " got " << score << " points.\n";
}
// 5. 结合 if 初始化语句与 map::try_emplace
if (auto [iter, inserted] = scores.try_emplace("David", 95); inserted) {
std::cout << "David inserted with score: " << iter->second << '\n';
} else {
std::cout << "David already exists with score: " << iter->second << '\n';
}
}
2. if 和 switch 初始化语句
允许在 if
和 switch
控制结构中直接声明和初始化变量。这个变量的作用域被严格限制在 if-else
或 switch
块内,有效避免了变量作用域泄漏和命名冲突。
C++17 之前
cpp
#include <mutex>
std::mutex mtx;
void legacy_func() {
mtx.lock(); // 必须手动管理锁
// ... 临界区代码 ...
// 如果这里发生异常或提前返回,unlock 可能不会被调用
mtx.unlock();
}
// 稍好的做法,但 lock_guard 变量作用域仍然过大
void slightly_better() {
std::lock_guard<std::mutex> guard(mtx);
// ... 临界区代码 ...
} // guard 在函数结束时解锁
使用 C++17 if 初始化语句
cpp
#include <iostream>
#include <fstream>
#include <cctype>
#include <mutex>
std::mutex file_mutex;
int main() {
// if 初始化:变量 `file` 的作用域仅限于 if-else 块
if (std::ifstream file("data.txt"); file.is_open()) {
std::cout << "File opened successfully.\n";
// file 在此可见
} else {
std::cout << "Failed to open file.\n";
// file 在此也可见
}
// file 在此已销毁,无法访问
// 2. switch 初始化:变量 `c` 的作用域仅限于 switch 块
switch (char c = static_cast<char>(std::getchar()); std::tolower(c)) {
case 'y':
std::cout << "You chose yes.\n";
break;
case 'n':
std::cout << "You chose no.\n";
break;
default:
std::cout << "Invalid choice. You entered: " << c << '\n';
}
// 3. 结合 lock_guard 实现更精细的锁作用域
if (std::lock_guard<std::mutex> lock(file_mutex); true) {
std::cout << "Critical section is locked.\n";
// ... 执行受保护的操作 ...
} // lock 在 if 语句结束时自动解锁
std::cout << "Critical section is now unlocked.\n";
}
3. 内联变量 (Inline Variables)
解决了 C++17 之前在头文件中初始化静态成员变量的"ODR-use"(单一定义规则使用)问题。使用 inline
关键字,可以在头文件中直接定义和初始化静态成员变量,而不会在多个翻译单元中引发链接错误。
C++17 之前 (mylib.h)
cpp
// mylib.h
#pragma once
struct Constants {
static const int MAX_BUFFER_SIZE; // 声明
};
// mylib.cpp
#include "mylib.h"
const int Constants::MAX_BUFFER_SIZE = 1024; // 定义和初始化必须在源文件中
使用 C++17 内联变量 (mylib.h)
cpp
// mylib.h
#pragma once
#include <string>
// 内联全局常量
inline constexpr double PI = 3.1415926535;
// 2. 内联类静态成员变量
struct AppConfig {
static inline int version_major = 1;
static inline std::string app_name = "MyApp";
};
// main.cpp
#include <iostream>
#include "mylib.h"
int main() {
std::cout << "PI: " << PI << '\n';
std::cout << "App Name: " << AppConfig::app_name << '\n';
std::cout << "Version: " << AppConfig::version_major << '\n';
// 可以在任何包含头文件的地方修改
AppConfig::app_name = "MyAwesomeApp";
std::cout << "New App Name: " << AppConfig::app_name << '\n';
}
4. 折叠表达式 (Fold Expressions)
极大地简化了对参数包的操作,使得编写可变参数模板函数(如求和、打印所有参数等)变得异常简洁和直观。
C++17 之前 (使用递归)
cpp
#include <iostream>
// 递归终止条件
long long sum_recursive() {
return 0;
}
// 递归模板
template<typename T, typename... Args>
long long sum_recursive(T first, Args... rest) {
return first + sum_recursive(rest...);
}
使用 C++17 折叠表达式
cpp
#include <iostream>
#include <type_traits>
// 一元右折叠 (Unary Right Fold)
template<typename... Args>
auto sum(Args... args) {
// (arg1 + (arg2 + (arg3 + ...)))
return (args + ...);
}
// 2. 一元左折叠 (Unary Left Fold)
template<typename... Args>
void print_left(Args... args) {
// (((std::cout << arg1) << arg2) << ...)
(std::cout << ... << args) << '\n';
}
// 3. 二元右折叠 (Binary Right Fold)
template<typename... Args>
auto subtract_from(int initial, Args... args) {
// (initial - (arg1 - (arg2 - ...)))
return (initial - ... - args);
}
// 4. 编译期检查所有类型是否相同
template<typename T, typename... Args>
constexpr bool all_same() {
return (std::is_same_v<T, Args> && ...);
}
int main() {
std::cout << "Sum: " << sum(1, 2, 3, 4.5, 5) << '\n';
std::cout << "Printing elements: ";
print_left("Hello", ", ", 2024, '!');
std::cout << "Binary fold: " << subtract_from(100, 10, 5) << '\n'; // 100 - (10 - 5) = 95
static_assert(all_same<int, int, int>(), "All types must be int");
// static_assert(all_same<int, double, int>(), "This will fail");
std::cout << "Compile-time type check passed.\n";
}
5. constexpr lambda 表达式
允许 lambda 表达式在编译期求值。这使得原本需要在运行时计算的逻辑可以提前到编译阶段完成,从而提升运行时性能,并能用于更多编译期元编程场景。
C++17 之前:lambda 表达式只能在运行时执行。
使用 C++17 constexpr lambda
cpp
#include <iostream>
#include <array>
// 编译期计算阶乘的 lambda
constexpr auto factorial = [](int n) {
long long result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
};
// 编译期创建并填充数组的 lambda
constexpr auto create_powers_of_two(size_t N) {
return [N]() {
std::array<int, N> arr{};
for (size_t i = 0; i < N; ++i) {
arr[i] = 1 << i;
}
return arr;
}();
}
int main() {
// 在编译期计算常量
constexpr long long fact5 = factorial(5);
static_assert(fact5 == 120, "Factorial calculation failed at compile time");
std::cout << "5! = " << fact5 << " (calculated at compile time)\n";
// 2. 在编译期生成查找表
constexpr auto powers = create_powers_of_two(8);
static_assert(powers[3] == 8, "Array initialization failed at compile time");
std::cout << "Powers of two (generated at compile time):\n";
for (size_t i = 0; i < powers.size(); ++i) {
std::cout << "2^" << i << " = " << powers[i] << '\n';
}
}
6. 类模板参数推导 (CTAD - Class Template Argument Deduction)
允许编译器根据构造函数参数自动推导类模板的模板参数,使得创建模板类对象时无需显式指定类型,语法更简洁,类似于普通类的实例化。
C++17 之前
cpp
#include <vector>
#include <utility>
#include <mutex>
#include <thread>
// 必须显式指定模板参数
std::vector<int> numbers = {1, 2, 3};
std::pair<int, std::string> person(1, "Alice");
std::lock_guard<std::mutex> lock(my_mutex);
使用 C++17 CTAD
cpp
#include <vector>
#include <utility>
#include <string>
#include <mutex>
#include <iostream>
// 自定义模板类
template<typename T1, typename T2>
struct MyPair {
T1 first;
T2 second;
MyPair(T1 f, T2 s) : first(f), second(s) {}
};
// 推导指引 (Deduction Guide) - 可选,用于复杂情况
template<typename T>
MyPair(T, const char*) -> MyPair<T, std::string>;
int main() {
// 标准库中的 CTAD
std::vector v = {1, 2, 3, 4}; // 自动推导为 std::vector<int>
std::pair p(101, "Bob"); // 自动推导为 std::pair<int, const char*>
std::mutex mtx;
std::lock_guard lock(mtx); // 自动推导为 std::lock_guard<std::mutex>
// 2. 自定义模板类的 CTAD
MyPair mp1(1, 3.14); // 推导为 MyPair<int, double>
MyPair mp2(2, "hello"); // 使用推导指引,推导为 MyPair<int, std::string>
std::cout << "Vector type: " << typeid(v).name() << '\n';
std::cout << "Pair type: " << typeid(p).name() << '\n';
std::cout << "MyPair1 type: " << typeid(mp1).name() << '\n';
std::cout << "MyPair2 type: " << typeid(mp2).name() << '\n';
}
7. std::optional
提供了一种类型安全的方式来表示一个"可能包含值"或"不包含值"的对象,避免了使用裸指针、魔术数字(如 -1
)或抛出异常来表示缺失值的情况。
C++17 之前 (常见做法)
cpp
#include <string>
// 使用特殊值 -1 表示未找到
int find_user_id_legacy(const std::string& username) {
if (username == "admin") return 0;
return -1; // -1 是一个"魔术数字"
}
// 使用指针,可能引入空指针风险
int* find_user_id_pointer(const std::string& username) {
if (username == "admin") {
static int id = 0;
return &id;
}
return nullptr;
}
使用 C++17 std::optional
cpp
#include <iostream>
#include <optional>
#include <string>
#include <map>
std::map<std::string, int> user_database = {{"Alice", 101}, {"Bob", 102}};
// 返回一个 optional<int>,清晰地表达了"可能没有"的语义
std::optional<int> find_user_id(const std::string& username) {
if (auto it = user_database.find(username); it != user_database.end()) {
return it->second; // 返回包含值的 optional
}
return std::nullopt; // 返回空的 optional
}
int main() {
// 查找存在的用户
if (auto id_opt = find_user_id("Alice"); id_opt.has_value()) {
std::cout << "Alice's ID: " << id_opt.value() << '\n';
}
// 2. 查找不存在的用户
auto guest_id_opt = find_user_id("Guest");
if (!guest_id_opt) {
std::cout << "Guest user not found.\n";
}
// 3. 使用 value_or 提供默认值
int bob_id = find_user_id("Bob").value_or(0);
int charlie_id = find_user_id("Charlie").value_or(-1);
std::cout << "Bob's ID (with default): " << bob_id << '\n';
std::cout << "Charlie's ID (with default): " << charlie_id << '\n';
}
8. std::variant
提供了一个类型安全的、可区分的联合体(discriminated union)。它可以在一个对象中存储不同类型的值,但任何时候只持有一种类型,并能安全地查询和访问当前存储的类型。
C++17 之前 (使用 union
和 enum
)
cpp
#include <iostream>
#include <string>
enum class Type { Int, String };
struct OldVariant {
Type type;
union {
int i;
std::string s; // 错误:union 不能包含非 POD 类型
} data; // 必须手动管理构造和析构
};
使用 C++17 std::variant
cpp
#include <iostream>
#include <variant>
#include <string>
#include <vector>
// 定义一个可以持有不同网络消息类型的 variant
using NetworkMessage = std::variant<int, std::string, std::vector<char>>;
void process_message(const NetworkMessage& msg) {
std::cout << "Processing message: ";
std::visit([](const auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "Heartbeat signal: " << value << '\n';
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "Text message: '" << value << "'\n";
} else if constexpr (std::is_same_v<T, std::vector<char>>) {
std::cout << "Binary data of size: " << value.size() << " bytes\n";
}
}, msg);
}
int main() {
NetworkMessage msg1 = 404;
NetworkMessage msg2 = "Hello, server!";
NetworkMessage msg3 = std::vector<char>{0xDE, 0xAD, 0xBE, 0xEF};
process_message(msg1);
process_message(msg2);
process_message(msg3);
// 查询当前类型
if (std::holds_alternative<std::string>(msg2)) {
std::cout << "Message 2 is a string: " << std::get<std::string>(msg2) << '\n';
}
}
9. std::string_view
提供了一个对字符串的非拥有(non-owning)引用。它像指针和长度一样工作,允许你查看和操作字符串数据,而无需进行昂贵的内存分配或拷贝。特别适用于函数参数传递和子字符串操作。
C++17 之前 (使用 const std::string&
或 const char*
)
cpp
#include <string>
// 每次调用都会创建一个新的 std::string 子串,导致内存分配
std::string get_first_word_legacy(const std::string& s) {
size_t pos = s.find(' ');
return s.substr(0, pos);
}
使用 C++17 std::string_view
cpp
#include <iostream>
#include <string_view>
// 函数接受 string_view,避免了不必要的拷贝
void print_header(std::string_view header) {
std::cout << "[Header] " << header << " [End Header]\n";
}
// 返回 string_view,无任何内存分配
std::string_view get_username(std::string_view url) {
size_t start = url.find("user=");
if (start == std::string_view::npos) return {};
start += 5;
size_t end = url.find('&', start);
return url.substr(start, end - start);
}
int main() {
std::string full_url = "https://example.com?user=Alice&session=12345";
const char* c_style_url = "ftp://test.net?user=Bob&token=xyz";
// 传递不同类型的字符串,无拷贝
print_header("Content-Type: application/json");
print_header(full_url);
// 2. 高效提取子串
std::string_view user_alice = get_username(full_url);
std::string_view user_bob = get_username(c_style_url);
std::cout << "Username from std::string: " << user_alice << '\n';
std::cout << "Username from const char*: " << user_bob << '\n';
// 3. 修改 string_view (不修改原始字符串)
user_alice.remove_prefix(2);
std::cout << "Trimmed username: " << user_alice << '\n';
std::cout << "Original URL is unchanged: " << full_url << '\n';
}
10. std::filesystem
提供了一套标准的、跨平台的工具,用于操作文件和目录。在 C++17 之前,这些操作通常需要依赖特定于操作系统的 API(如 POSIX 的 <unistd.h>
或 Windows 的 <windows.h>
)或第三方库(如 Boost.Filesystem)。
C++17 之前 (示例:使用 POSIX API)
cpp
#include <unistd.h>
#include <sys/stat.h>
void create_dir_legacy() {
mkdir("legacy_dir", 0755); // 平台相关
}
使用 C++17 std::filesystem
cpp
#include <iostream>
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
int main() {
// 路径构建与操作
fs::path base_dir = "/tmp/cpp17_demo";
fs::path sub_dir = base_dir / "sub";
fs::path file_path = sub_dir / "file.txt";
std::cout << "Full path: " << file_path << '\n';
std::cout << "Filename: " << file_path.filename() << '\n';
std::cout << "Extension: " << file_path.extension() << '\n';
try {
// 2. 创建和删除目录
fs::create_directories(sub_dir);
std::cout << "Created directory: " << sub_dir << '\n';
// 3. 创建文件
std::ofstream(file_path) << "Hello, filesystem!";
// 4. 检查文件状态
if (fs::exists(file_path) && fs::is_regular_file(file_path)) {
std::cout << "File size: " << fs::file_size(file_path) << " bytes\n";
}
// 5. 遍历目录
std::cout << "\nListing contents of " << base_dir << ":\n";
for (const auto& entry : fs::directory_iterator(base_dir)) {
std::cout << entry.path() << (entry.is_directory() ? " [DIR]" : " [FILE]") << '\n';
}
// 清理
// fs::remove_all(base_dir);
// std::cout << "\nCleaned up directory: " << base_dir << '\n';
} catch (const fs::filesystem_error& e) {
std::cerr << "Filesystem error: " << e.what() << '\n';
}
}
11. 嵌套命名空间 (Nested Namespaces)
提供了一种更简洁的语法来定义嵌套的命名空间,减少了代码的冗余和缩进层级,提高了可读性。
C++17 之前
cpp
namespace MyLib {
namespace Core {
namespace Utils {
void helper_function() { /* ... */ }
}
}
}
使用 C++17 嵌套命名空间
cpp
#include <iostream>
// 使用 :: 分隔符一次性定义嵌套命名空间
namespace MyOrg::Networking::Protocols {
void send_http_request() {
std::cout << "Sending HTTP request via MyOrg::Networking::Protocols...\n";
}
}
int main() {
// 调用嵌套命名空间中的函数
MyOrg::Networking::Protocols::send_http_request();
}
12. if constexpr
在编译期进行条件判断。如果条件为 false
,则 if constexpr
块内的代码将被完全丢弃,不会进行语法分析或模板实例化。这解决了传统 if
语句在模板元编程中可能导致编译错误的痛点。
C++17 之前 (使用 SFINAE 或模板特化)
cpp
#include <string>
#include <type_traits>
// SFINAE 版本
template<typename T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
std::string to_string_legacy(T value) {
return std::to_string(value);
}
template<typename T, std::enable_if_t<!std::is_arithmetic_v<T>, int> = 0>
std::string to_string_legacy(T value) {
return std::string(value);
}
使用 C++17 if constexpr
cpp
#include <iostream>
#include <string>
#include <type_traits>
// 一个函数处理所有情况,更清晰
template<typename T>
std::string to_string_universal(T value) {
if constexpr (std::is_pointer_v<T>) {
// 如果 T 是指针,解引用后再转换
return to_string_universal(*value);
} else if constexpr (std::is_arithmetic_v<T>) {
// 如果 T 是算术类型,使用 std::to_string
return std::to_string(value);
} else {
// 否则,假定它可以隐式转换为 string
return std::string(value);
}
}
int main() {
int x = 42;
int* p = &x;
const char* s = "hello";
std::cout << to_string_universal(x) << '\n';
std::cout << to_string_universal(p) << '\n';
std::cout << to_string_universal(s) << '\n';
}
13. 属性扩展 (Attribute Extensions)
C++17 引入或标准化了几个有用的属性,以增强代码的静态分析和可读性。
cpp
#include <iostream>
#include <vector>
// [[nodiscard]]: 提示编译器,函数的返回值不应被忽略。
// 常用于工厂函数或返回错误码的函数。
[[nodiscard]] bool connect_to_database(const std::string& host) {
if (host.empty()) return false;
// ... 连接逻辑 ...
return true;
}
void setup_connection() {
// 如果不使用返回值,编译器会发出警告
connect_to_database("localhost"); // 警告: ignoring return value of function declared with 'nodiscard' attribute
if (!connect_to_database("localhost")) {
std::cerr << "Connection failed!\n";
}
}
int main() {
// 2. [[maybe_unused]]: 向编译器表明一个变量可能未被使用,以抑制警告。
// 常用于回调函数中未使用的参数或功能开关控制的变量。
int [[maybe_unused]] debug_level = 0;
#ifdef ENABLE_DEBUG
debug_level = 1;
#endif
// 3. [[fallthrough]]: 明确告知编译器,switch case 的穿透是故意的。
char command = 'a';
switch (command) {
case 'a': // admin
std::cout << "Admin access granted.\n";
[[fallthrough]];
case 'u': // user
std::cout << "User access granted.\n";
break;
default:
std::cout << "Guest access.\n";
}
setup_connection();
}
总结
C++17 是一次重大的版本迭代,其核心价值在于提升代码的现代化水平、可读性和安全性。它通过引入一系列"质量改进"特性,使得开发者能用更少的代码、更清晰的逻辑来表达复杂的意图。
核心影响:
- 代码更简洁 :结构化绑定、
if/switch
初始化、内联变量和折叠表达式等特性,显著减少了样板代码。 - 类型安全 :
std::optional
、std::variant
和if constexpr
提供了更强大的类型安全保障,减少了运行时错误。 - 性能提升 :
std::string_view
和constexpr lambda
等特性避免了不必要的开销,提升了程序性能。 - 标准库更强大 :
std::filesystem
的加入填补了标准库在文件系统操作上的空白。
最佳实践建议 : 拥抱结构化绑定 :在处理 pair
、tuple
和 map
等返回多个值的场景时,优先使用结构化绑定。 2. 善用 if constexpr
:在模板元编程中,用 if constexpr
替代复杂的 SFINAE 和标签分发技术。 3. 传递 string_view
:对于只读的字符串参数,优先使用 std::string_view
,避免不必要的内存分配。 4. 用 optional
和 variant
建模 :当函数可能不返回值或返回多种类型时,使用 std::optional
和 std::variant
来清晰地表达这种不确定性。 5. 利用属性 :为关键函数加上 [[nodiscard]]
,为意图明确的代码加上 [[maybe_unused]]
和 [[fallthrough]]
,让编译器成为你的代码审查伙伴。
注意:运行示例代码需要支持 C++17 的编译器(GCC 7+、Clang 5+、MSVC 19.15+)