C++17
- [C++17 核心特性详解](#C++17 核心特性详解)
-
- 目录
- [1. 1结构化绑定](#1. 1结构化绑定)
-
- [1.1 工作原理](#1.1 工作原理)
- [1.2 基本语法](#1.2 基本语法)
- [2. 强制省略拷贝](#2. 强制省略拷贝)
-
- [2.1 URVO vs NRVO](#2.1 URVO vs NRVO)
- [2.2 代码示例](#2.2 代码示例)
- [2.3 不同标准下的行为对比](#2.3 不同标准下的行为对比)
-
- [2.3.1 函数 `f()` 的行为](#2.3.1 函数
f()的行为) - [2.3.2 函数 `ff()` 的行为](#2.3.2 函数
ff()的行为)
- [2.3.1 函数 `f()` 的行为](#2.3.1 函数
- [3. if constexpr](#3. if constexpr)
-
- [3.1 基本语法](#3.1 基本语法)
- [3.2 与普通if的区别](#3.2 与普通if的区别)
- [3.3 使用示例](#3.3 使用示例)
- [4. 折叠表达式](#4. 折叠表达式)
-
- [4.1 四种基本语法形式](#4.1 四种基本语法形式)
- [4.2 使用示例](#4.2 使用示例)
- [4.3 核心规则](#4.3 核心规则)
- [5. 类模板参数自动推导](#5. 类模板参数自动推导)
-
- [5.1 基本使用](#5.1 基本使用)
- [5.2 CTAD与函数模板推导的对比](#5.2 CTAD与函数模板推导的对比)
- [6. 推导指引](#6. 推导指引)
-
- [6.1 基本定义](#6.1 基本定义)
- [6.2 使用示例](#6.2 使用示例)
- [6.3 核心规则](#6.3 核心规则)
- [6.4 花括号初始化与CTAD](#6.4 花括号初始化与CTAD)
- [7. 非类型模板参数](#7. 非类型模板参数)
-
- [7.1 类型支持演变](#7.1 类型支持演变)
- [8. 嵌套命名空间定义](#8. 嵌套命名空间定义)
- [9. __has_include](#9. __has_include)
-
- [9.1 核心用途](#9.1 核心用途)
- [9.2 重要说明](#9.2 重要说明)
- [9.3 C++17之前的解决方案](#9.3 C++17之前的解决方案)
- [10. 属性](#10. 属性)
-
- [10.1 C++98的属性(编译器扩展)](#10.1 C++98的属性(编译器扩展))
- [10.2 C++11属性(标准化)](#10.2 C++11属性(标准化))
- [10.3 [[noreturn]] (C++11)](#10.3 [[noreturn]] (C++11))
- [10.4 [[deprecated]] (C++14)](#10.4 [[deprecated]] (C++14))
- [10.5 C++17新增属性](#10.5 C++17新增属性)
- [10.6 C++20新增属性](#10.6 C++20新增属性)
-
- [10.6.1 `[[likely]]` 与 `[[unlikely]]`](#10.6.1
[[likely]]与[[unlikely]]) - [10.6.2 `[[no_unique_address]]`](#10.6.2
[[no_unique_address]])
- [10.6.1 `[[likely]]` 与 `[[unlikely]]`](#10.6.1
- [11. 新的求值顺序规则](#11. 新的求值顺序规则)
-
- [11.1 关键变化](#11.1 关键变化)
- [11.2 C++17求值顺序规则总结](#11.2 C++17求值顺序规则总结)
- [12. std::optional](#12. std::optional)
-
- [12.1 解决的问题](#12.1 解决的问题)
- [12.2 基本使用](#12.2 基本使用)
- [12.3 实践场景](#12.3 实践场景)
- [12.4 核心接口](#12.4 核心接口)
- [12.5 优势](#12.5 优势)
- [13. std::variant](#13. std::variant)
-
- [13.1 解决的问题](#13.1 解决的问题)
- [13.2 核心优势](#13.2 核心优势)
- [13.3 基本使用](#13.3 基本使用)
- [13.4 访问方式](#13.4 访问方式)
-
- [13.4.1 std::get(抛出异常)](#13.4.1 std::get(抛出异常))
- [13.4.2 std::get_if(返回指针)](#13.4.2 std::get_if(返回指针))
- [13.4.3 std::visit(推荐方式)](#13.4.3 std::visit(推荐方式))
- [13.5 综合示例:哈希表实现](#13.5 综合示例:哈希表实现)
- [14. std::any](#14. std::any)
-
- [14.1 核心特性](#14.1 核心特性)
- [14.2 基本使用](#14.2 基本使用)
- [14.3 any_cast 的三种取值方式](#14.3 any_cast 的三种取值方式)
- [14.4 综合示例:哈希表实现](#14.4 综合示例:哈希表实现)
- [14.5 std::any 与 std::variant 对比](#14.5 std::any 与 std::variant 对比)
- [15. std::string_view](#15. std::string_view)
-
- [15.1 核心特性](#15.1 核心特性)
- [15.2 基本使用](#15.2 基本使用)
- [15.3 性能对比](#15.3 性能对比)
- [15.4 使用场景](#15.4 使用场景)
- [15.5 常见问题与注意事项](#15.5 常见问题与注意事项)
- [16. std::filesystem(文件系统库)](#16. std::filesystem(文件系统库))
-
- [16.1 核心类](#16.1 核心类)
- [16.2 路径操作](#16.2 路径操作)
- [16.3 文件状态](#16.3 文件状态)
- [16.4 文件权限](#16.4 文件权限)
- [16.5 文件或目录操作](#16.5 文件或目录操作)
- [16.6 目录遍历](#16.6 目录遍历)
- [16.7 异常和错误码处理](#16.7 异常和错误码处理)
- [16.8 核心功能总结](#16.8 核心功能总结)
- [17. 并行算法](#17. 并行算法)
-
- [17.1 执行策略](#17.1 执行策略)
- [17.2 支持并行化的算法](#17.2 支持并行化的算法)
- [17.3 使用示例](#17.3 使用示例)
- [17.4 更多示例](#17.4 更多示例)
- [17.5 注意事项](#17.5 注意事项)
- [17.6 并行算法与异常](#17.6 并行算法与异常)
- [17.7 并行算法与复杂度](#17.7 并行算法与复杂度)
- [17.8 并行算法的限制](#17.8 并行算法的限制)
- [17.9 并行算法的性能考虑](#17.9 并行算法的性能考虑)
- [17.10 并行算法的未来](#17.10 并行算法的未来)
C++17 核心特性详解
C++17 是 C++ 编程语言的一个重要版本,它在 C++14 的基础上,新增了多项语言和库特性。该标准于 2017 年 12 月正式发布。
目录
C++17
- [C++17 核心特性详解](#C++17 核心特性详解)
-
- 目录
- [1. 1结构化绑定](#1. 1结构化绑定)
-
- [1.1 工作原理](#1.1 工作原理)
- [1.2 基本语法](#1.2 基本语法)
- [2. 强制省略拷贝](#2. 强制省略拷贝)
-
- [2.1 URVO vs NRVO](#2.1 URVO vs NRVO)
- [2.2 代码示例](#2.2 代码示例)
- [2.3 不同标准下的行为对比](#2.3 不同标准下的行为对比)
-
- [2.3.1 函数 `f()` 的行为](#2.3.1 函数
f()的行为) - [2.3.2 函数 `ff()` 的行为](#2.3.2 函数
ff()的行为)
- [2.3.1 函数 `f()` 的行为](#2.3.1 函数
- [3. if constexpr](#3. if constexpr)
-
- [3.1 基本语法](#3.1 基本语法)
- [3.2 与普通if的区别](#3.2 与普通if的区别)
- [3.3 使用示例](#3.3 使用示例)
- [4. 折叠表达式](#4. 折叠表达式)
-
- [4.1 四种基本语法形式](#4.1 四种基本语法形式)
- [4.2 使用示例](#4.2 使用示例)
- [4.3 核心规则](#4.3 核心规则)
- [5. 类模板参数自动推导](#5. 类模板参数自动推导)
-
- [5.1 基本使用](#5.1 基本使用)
- [5.2 CTAD与函数模板推导的对比](#5.2 CTAD与函数模板推导的对比)
- [6. 推导指引](#6. 推导指引)
-
- [6.1 基本定义](#6.1 基本定义)
- [6.2 使用示例](#6.2 使用示例)
- [6.3 核心规则](#6.3 核心规则)
- [6.4 花括号初始化与CTAD](#6.4 花括号初始化与CTAD)
- [7. 非类型模板参数](#7. 非类型模板参数)
-
- [7.1 类型支持演变](#7.1 类型支持演变)
- [8. 嵌套命名空间定义](#8. 嵌套命名空间定义)
- [9. __has_include](#9. __has_include)
-
- [9.1 核心用途](#9.1 核心用途)
- [9.2 重要说明](#9.2 重要说明)
- [9.3 C++17之前的解决方案](#9.3 C++17之前的解决方案)
- [10. 属性](#10. 属性)
-
- [10.1 C++98的属性(编译器扩展)](#10.1 C++98的属性(编译器扩展))
- [10.2 C++11属性(标准化)](#10.2 C++11属性(标准化))
- [10.3 [[noreturn]] (C++11)](#10.3 [[noreturn]] (C++11))
- [10.4 [[deprecated]] (C++14)](#10.4 [[deprecated]] (C++14))
- [10.5 C++17新增属性](#10.5 C++17新增属性)
- [10.6 C++20新增属性](#10.6 C++20新增属性)
-
- [10.6.1 `[[likely]]` 与 `[[unlikely]]`](#10.6.1
[[likely]]与[[unlikely]]) - [10.6.2 `[[no_unique_address]]`](#10.6.2
[[no_unique_address]])
- [10.6.1 `[[likely]]` 与 `[[unlikely]]`](#10.6.1
- [11. 新的求值顺序规则](#11. 新的求值顺序规则)
-
- [11.1 关键变化](#11.1 关键变化)
- [11.2 C++17求值顺序规则总结](#11.2 C++17求值顺序规则总结)
- [12. std::optional](#12. std::optional)
-
- [12.1 解决的问题](#12.1 解决的问题)
- [12.2 基本使用](#12.2 基本使用)
- [12.3 实践场景](#12.3 实践场景)
- [12.4 核心接口](#12.4 核心接口)
- [12.5 优势](#12.5 优势)
- [13. std::variant](#13. std::variant)
-
- [13.1 解决的问题](#13.1 解决的问题)
- [13.2 核心优势](#13.2 核心优势)
- [13.3 基本使用](#13.3 基本使用)
- [13.4 访问方式](#13.4 访问方式)
-
- [13.4.1 std::get(抛出异常)](#13.4.1 std::get(抛出异常))
- [13.4.2 std::get_if(返回指针)](#13.4.2 std::get_if(返回指针))
- [13.4.3 std::visit(推荐方式)](#13.4.3 std::visit(推荐方式))
- [13.5 综合示例:哈希表实现](#13.5 综合示例:哈希表实现)
- [14. std::any](#14. std::any)
-
- [14.1 核心特性](#14.1 核心特性)
- [14.2 基本使用](#14.2 基本使用)
- [14.3 any_cast 的三种取值方式](#14.3 any_cast 的三种取值方式)
- [14.4 综合示例:哈希表实现](#14.4 综合示例:哈希表实现)
- [14.5 std::any 与 std::variant 对比](#14.5 std::any 与 std::variant 对比)
- [15. std::string_view](#15. std::string_view)
-
- [15.1 核心特性](#15.1 核心特性)
- [15.2 基本使用](#15.2 基本使用)
- [15.3 性能对比](#15.3 性能对比)
- [15.4 使用场景](#15.4 使用场景)
- [15.5 常见问题与注意事项](#15.5 常见问题与注意事项)
- [16. std::filesystem(文件系统库)](#16. std::filesystem(文件系统库))
-
- [16.1 核心类](#16.1 核心类)
- [16.2 路径操作](#16.2 路径操作)
- [16.3 文件状态](#16.3 文件状态)
- [16.4 文件权限](#16.4 文件权限)
- [16.5 文件或目录操作](#16.5 文件或目录操作)
- [16.6 目录遍历](#16.6 目录遍历)
- [16.7 异常和错误码处理](#16.7 异常和错误码处理)
- [16.8 核心功能总结](#16.8 核心功能总结)
- [17. 并行算法](#17. 并行算法)
-
- [17.1 执行策略](#17.1 执行策略)
- [17.2 支持并行化的算法](#17.2 支持并行化的算法)
- [17.3 使用示例](#17.3 使用示例)
- [17.4 更多示例](#17.4 更多示例)
- [17.5 注意事项](#17.5 注意事项)
- [17.6 并行算法与异常](#17.6 并行算法与异常)
- [17.7 并行算法与复杂度](#17.7 并行算法与复杂度)
- [17.8 并行算法的限制](#17.8 并行算法的限制)
- [17.9 并行算法的性能考虑](#17.9 并行算法的性能考虑)
- [17.10 并行算法的未来](#17.10 并行算法的未来)
1. 1结构化绑定
结构化绑定是C++17引入的语法糖,允许开发者从一个数组、结构体、元组或其他拥有多个数据成员的对象中,一次性声明并初始化多个变量,使代码更加简洁易读。
1.1 工作原理
结构化绑定依赖被绑定对象的类型,主要分为三种情况:
- 绑定到数组元素
- 绑定到实现了
tuple协议的类型 - 直接绑定到类(或结构体)的可访问数据成员
注意:引入的变量数量必须与被绑定对象的"结构化绑定大小"相匹配。自C++26起,语法扩展支持使用省略号引入结构化绑定包。
1.2 基本语法
cpp
// 一般写法
auto [identifier1, identifier2, ... , identifierN] = expression;
// ()初始化
auto [identifier1, identifier2, ... , identifierN] (expression);
// {}统一初始化
auto [identifier1, identifier2, ... , identifierN] {expression};
// 以引用方式结构化绑定
auto& [identifier1, identifier2, ... , identifierN] = expression;
auto&& [identifier1, identifier2, ... , identifierN] = expression;
const auto& [identifier1, identifier2, ... , identifierN] = expression;
2. 强制省略拷贝
C++17将特定场景下的拷贝省略标准化为强制省略拷贝,主要针对无名返回值优化(URVO)。
2.1 URVO vs NRVO
| 优化类型 | 全称 | C++17标准 | 说明 |
|---|---|---|---|
| URVO | 无名返回值优化 | 强制保证 | 返回纯右值时创建临时对象的优化 |
| NRVO | 命名返回值优化 | 编译器优化(可选) | 返回具名局部对象的优化 |
2.2 代码示例
cpp
#include <iostream>
using namespace std;
struct Noisy {
Noisy() { std::cout << "constructed at " << this << '\n'; }
Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
// 命名返回值(NRVO,编译器优化)
Noisy f() {
Noisy v = Noisy();
return v; // 自C++17起"保证的拷贝省略"
}
// 返回匿名对象(URVO,C++17强制保证)
Noisy ff() {
return Noisy(); // 强制省略拷贝
}
2.3 不同标准下的行为对比
2.3.1 函数 f() 的行为
| 标准版本 | 构造/拷贝次数 | 说明 |
|---|---|---|
| C++98 | 1次构造 + 3次拷贝构造 | |
| C++11 | 1次构造 + 3次移动构造 | |
| C++17 | 1次构造 + 2次移动构造 | 强制省略第一次拷贝 |
2.3.2 函数 ff() 的行为
| 标准版本 | 构造/拷贝次数 | 说明 |
|---|---|---|
| C++98 | 1次构造 + 2次拷贝构造 | |
| C++11 | 1次构造 + 2次移动构造 | |
| C++17 | 1次构造 | 完全无拷贝/移动 |
重要变化:C++17之前,即使编译器优化,代码仍需满足拷贝/移动构造函数的可访问性。C++17的强制拷贝省略使返回不可移动/不可拷贝类型的代码合法化。
cpp
struct NonMoveable {
NonMoveable() = default;
NonMoveable(const NonMoveable&) = delete; // 禁止拷贝
NonMoveable(NonMoveable&&) = delete; // 禁止移动
};
NonMoveable make() {
return NonMoveable(); // C++14: ❌ 编译错误; C++17: ✅ 正常工作
}
3. if constexpr
if constexpr是C++17引入的编译时条件判断语句,根据常量表达式的结果在编译期决定代码分支的取舍。
3.1 基本语法
cpp
if constexpr (常量表达式) {
// 条件为true时编译的代码
} else if constexpr(常量表达式) {
// 条件为true时编译的代码(可选)
} else {
// 条件为false时编译的代码(可选)
}
3.2 与普通if的区别
| 方面 | if constexpr |
普通 if |
|---|---|---|
| 实例化范围 | 只实例化匹配分支的代码 | 实例化所有分支的代码 |
| 模板参数依赖 | 可以直接依赖模板参数做条件判断 | 所有分支代码必须合法 |
| 编译安全性 | 高:无效代码不会被实例化 | 低:所有代码都必须语法正确 |
| 典型应用 | 基于类型特性的条件编译 | 基于值的运行时条件判断 |
3.3 使用示例
cpp
// 示例1: 基本用法
#include <iostream>
#include <type_traits>
template<typename T>
auto printTypeInfo(const T& value) {
if constexpr (std::is_integral_v<T>) {
std::cout << value << " is an integral type" << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << value << " is a floating point type" << std::endl;
} else {
std::cout << value << " is some other type" << std::endl;
}
}
// 示例2: 在模板元编程中使用
template<typename T, typename... Args>
void printArgs(T&& arg, Args&&... args) {
std::cout << arg;
if constexpr (sizeof...(args) > 0) {
std::cout << ", ";
printArgs(std::forward<Args>(args)...);
} else {
std::cout << std::endl;
}
}
4. 折叠表达式
折叠表达式是C++17引入的特性,用于简化可变参数模板中对参数包的操作,允许将二元运算符应用于参数包的所有元素。
4.1 四种基本语法形式
| 形式 | 语法 | 展开形式 | 结合方向 |
|---|---|---|---|
| 一元右折叠 | (pack op ...) |
(E₁ op (E₂ op (E₃ op ... (Eₙ₋₁ op Eₙ)))) |
从右向左 |
| 一元左折叠 | (... op pack) |
(((E₁ op E₂) op E₃) op ...) op Eₙ |
从左向右 |
| 二元右折叠 | (pack op ... op init) |
(E₁ op (E₂ op (E₃ op ... (Eₙ op init)))) |
从右向左 |
| 二元左折叠 | (init op ... op pack) |
((((init op E₁) op E₂) op ...) op Eₙ) |
从左向右 |
4.2 使用示例
cpp
#include <iostream>
#include <vector>
#include <string>
// 1. 一元左折叠
template <typename... Args>
bool all(Args... args) {
return (... && args); // 展开: (((args1 && args2) && args3) && ...)
}
// 2. 一元右折叠
template <typename... Args>
auto sum(Args... args) {
return (args + ...); // 展开: (args1 + (args2 + (args3 + ...))
}
// 3. 二元左折叠
template <typename... Strings>
std::string concat_left(Strings... strs) {
return (std::string("%%") + ... + strs);
}
// 4. 二元右折叠
template <typename... Strings>
std::string concat_right(Strings... strs) {
return (strs + ... + std::string("%%"));
}
// 5. 使用逗号运算符
template <typename... Args>
void print_with_separator(Args&&... args) {
auto print_elem = const auto& x {
std::cout << x << " ";
};
(..., print_elem(args)); // 一元左折叠
std::cout << std::endl;
}
4.3 核心规则
- 必须用括号包裹:开头和结尾的圆括号不可省略
- 允许的32种运算符 :
+ - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->* - 空参数包处理 :
- 一元折叠:仅
&&、||、,允许,值分别为true、false、void() - 二元折叠:结果就是初始值
init
- 一元折叠:仅
5. 类模板参数自动推导
类模板参数自动推导允许编译器根据构造函数的参数自动推断模板参数的类型,简化类模板的实例化。
5.1 基本使用
cpp
#include <vector>
#include <tuple>
#include <array>
void containerDeduction() {
// C++17之前需要指定模板参数
std::vector<int> v1 = {1, 2, 3};
// C++17可以自动推导
std::vector v2 = {4, 5, 6}; // 推导为 vector<int>
std::vector v3{"hello", "world"}; // 推导为 vector<const char*>
std::vector v4(10, 1); // 推导为 vector<int>
std::array a = {1, 2, 3}; // 推导为 array<int, 3>
std::pair p(1, 2.0); // 推导为 pair<int, double>
std::tuple t(1, 2.0, "three"); // 推导为 tuple<int, double, const char*>
}
5.2 CTAD与函数模板推导的对比
- 相似点:都遵循模板参数推导的标准规则,支持引用折叠、数组到指针退化等
- 区别 :
- CTAD可以从多个构造函数参数推导
- CTAD支持用户定义的推导指引
6. 推导指引
推导指引是C++17引入的显式规则,用于指导编译器在类模板参数推导中如何从构造函数参数推断模板参数类型。
6.1 基本定义
cpp
template<typename T>
class MyContainer {
public:
MyContainer(std::initializer_list<T> init) : data(init) {}
private:
std::vector<T> data;
};
// 推导指引:告诉编译器如何从初始化列表推导T
MyContainer(std::initializer_list<T>) -> MyContainer<T>;
6.2 使用示例
cpp
#include <iostream>
#include <vector>
#include <initializer_list>
template<typename T>
class SimpleBox {
std::vector<T> items;
public:
SimpleBox(std::initializer_list<T> init) : items(init) {}
void print() const {
for (const auto& item : items) {
std::cout << item << " ";
}
std::cout << std::endl;
}
};
// 推导指引
SimpleBox(std::initializer_list<int>) -> SimpleBox<int>;
SimpleBox(std::initializer_list<double>) -> SimpleBox<double>;
6.3 核心规则
- 语法格式 :
模板类名(构造函数参数列表) -> 推导出的模板实例化; - 作用时机:在类模板参数推导过程中使用
- 优先级:编译器尝试匹配构造函数和推导指引,选择最合适的
- 重要限制:推导指引不是函数,不能有函数体
6.4 花括号初始化与CTAD
即使类模板没有std::initializer_list构造函数,也可以使用花括号初始化:
cpp
template<typename T>
class Box {
T value;
public:
// 普通构造函数,不是initializer_list
Box(const T& v) : value(v) {
std::cout << "Box constructed with " << v << std::endl;
}
};
int main() {
Box<int> b1{42}; // ✅ 花括号调用普通构造函数
Box b2{3.14}; // ✅ C++17 CTAD: Box<double>
return 0;
}
匹配规则:
- 有
std::initializer_list构造函数:花括号优先匹配它 - 没有
std::initializer_list构造函数:花括号匹配普通构造函数 - 两者都没有但类是聚合:花括号进行聚合初始化
- 以上都不满足:编译错误
7. 非类型模板参数
非类型模板参数是指具有特定类型的常量值参数。C++17引入了auto作为占位符,允许编译器根据实例化时提供的实参自动推导类型,从而简化代码。
7.1 类型支持演变
- C++20之前 :类型被限制为整型、枚举、指针、左值引用及
std::nullptr_t - C++20开始:支持浮点数及满足特定条件的"字面量类类型"对象作为编译期常量
"字面量类类型"指能在编译期被完整构造和析构的简单数据结构,如std::array或由公开成员构成的简单结构体。
cpp
#include <iostream>
#include <string>
#include <string_view>
#include <array>
#include <typeinfo>
// Value是一个非类型模板参数
template<auto Value>
void printValue() {
std::cout << "Type: " << typeid(Value).name() << std::endl;
std::cout << "---" << std::endl;
}
// 简单的字面量类类型
struct Point {
int x;
int y;
// 编译器会生成隐式的constexpr构造函数
};
// 非类型模板参数自动类型推导(通过auto关键字)
template<auto... Values>
struct ValueList {
ValueList() {
// 逗号折叠表达式
((std::cout << Values << ' '), ...);
}
};
const char arr[] = "hello";
int main() {
printValue<42>(); // auto推导为int
printValue<3.14>(); // auto推导为double(C++20开始支持浮点型)
printValue<'A'>(); // auto推导为char
printValue<true>(); // auto推导为bool
printValue<nullptr>(); // auto推导为std::nullptr_t
// 字符串字面量的类型是const char[N],在模板参数推导中退化为const char*
// 禁止直接使用字符串字面量主要是为了避免ODR(单一定义规则)违规
// printValue<"hello">(); // ❌ 不能直接使用字符串字面量
// 使用全局字符数组变量
printValue<arr>(); // auto推导为const char*
// C++20开始支持字面量类类型作为非类型模板参数
printValue<Point{10, 20}>();
constexpr std::array arr2{1, 2, 3, 4, 5};
printValue<arr2>();
// 计算值列表中的值的类型
ValueList<1, 2.2, 'a'> vl;
}
8. 嵌套命名空间定义
C++17引入的简化嵌套命名空间定义语法以语法糖形式,使多层嵌套命名空间的定义更加简洁方便。
cpp
#include <iostream>
// C++17之前的嵌套命名空间
namespace A {
namespace B {
namespace C {
void foo() {
std::cout << "Old nested namespace style" << std::endl;
}
}
}
}
// C++17的简洁嵌套命名空间
namespace A::B::C {
void bar() {
std::cout << "New nested namespace style" << std::endl;
}
}
namespace A::B {
void qux() {
std::cout << "Partially nested namespace" << std::endl;
}
}
// 也可以定义别名
namespace inner = A::B::C;
int main() {
A::B::C::foo();
A::B::C::bar();
A::B::qux();
return 0;
}
9. __has_include
__has_include是C++17引入的预处理期特性,用于在编译时检测特定头文件是否可被包含。其结果是布尔值,常与#if或#ifdef配合使用实现条件编译。
9.1 核心用途
在编写跨平台或可移植代码时,安全地检查头文件是否存在。如果存在则包含并使用其功能;否则提供回退方案或跳过相关代码,避免因缺少头文件导致的编译错误。
cpp
#include <iostream>
// 示例1: 检查标准库头文件
#if __has_include(<optional>)
#include <optional>
#define HAS_OPTIONAL 1
#else
#define HAS_OPTIONAL 0
#endif
// 示例2: 检查自定义头文件
#if __has_include("my_header.h")
#include "my_header.h"
#define HAS_MY_HEADER 1
#else
#define HAS_MY_HEADER 0
#endif
// 示例3:检查是否支持文件系统相关库
#if __has_include(<filesystem>)
#include <filesystem>
namespace fs = std::filesystem;
#elif __has_include(<experimental/filesystem>)
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
#else
#error "需要 filesystem 支持"
#endif
int main() {
std::cout << "Optional support: " << HAS_OPTIONAL << std::endl;
std::cout << "My header support: " << HAS_MY_HEADER << std::endl;
// 实际使用
#if HAS_OPTIONAL
std::optional<int> opt = 42;
std::cout << "Optional value: " << *opt << std::endl;
#else
std::cout << "Optional not available" << std::endl;
#endif
return 0;
}
9.2 重要说明
- 它是预处理指令
- 检查的是"可包含性",即编译器是否能找到该文件
- 即使文件存在,如果内容有语法错误导致无法被成功包含,
__has_include仍可能返回"真"
9.3 C++17之前的解决方案
#include "xxx.h"会在预处理阶段直接检查文件存在性,若文件不存在,编译器将error并终止整个编译流程。__has_include是一种主动的预处理期探测机制,通过返回布尔值供#if条件判断,使代码能够先检测头文件是否可用,再决定是否包含或转向备用实现,从而实现源代码的优雅降级与跨平台兼容。
10. 属性
属性用于为代码中的各种实体添加额外的注解信息。
10.1 C++98的属性(编译器扩展)
cpp
// GCC/Clang:
__attribute__((noreturn)) void func() {}
__attribute__((deprecated)) void old_func() {}
// MSVC
__declspec(noreturn) void func() {}
__declspec(deprecated) void old_func() {}
10.2 C++11属性(标准化)
从C++11开始引入统一的[[...]]语法:
cpp
[[noreturn]] void func() {}
[[deprecated]] void old_func() {} // deprecated是C++14加入的
// 语法形式
[[attribute]] // 单个属性
[[attribute1, attribute2]] // 多个属性
[[namespace::attribute]] // 带命名空间的属性
10.3 [[noreturn]] (C++11)
标记了[[noreturn]]的函数绝不能以任何方式执行到返回调用者的点。主要用于优化和错误检测:
- 编译器可以优化代码逻辑,跳过函数调用之后的所有代码
- 不为该函数调用生成标准的返回准备代码
- 在静态分析时,将其后的代码路径视为"不可达"
警告 :如果错误地为一个实际上可能返回的函数标记
[[noreturn]],则程序行为是未定义的。
cpp
#include <cstdlib>
#include <iostream>
#include <stdexcept>
// 1. 终止程序的函数
[[noreturn]] void fatal_error(const std::string& message) {
std::cerr << "Fatal error: " << message << std::endl;
std::exit(EXIT_FAILURE);
// 不需要 return 语句
}
// 2. 总是抛出异常的函数
[[noreturn]] void throw_runtime_error(const char* message) {
throw std::runtime_error(message);
// 不会执行到这里
}
// 3. 无限循环的函数
[[noreturn]] void event_loop() {
while (true) {
// 处理事件...
}
// 不会执行到这里
}
int main() {
fatal_error("error exit");
std::cout << "[noreturn]" << std::endl; // 这行代码永远不会执行
}
10.4 [[deprecated]] (C++14)
用于标记某个程序实体为已废弃,告知开发者该实体在未来版本中可能被移除。使用时编译器会产生警告而非错误。
cpp
#include <iostream>
// 标记一个函数为弃用
[[deprecated]]
int old_function() { return 0; };
// 提供自定义的警告信息
[[deprecated("Use the new_function() instead, which is safer and faster.")]]
void legacy_function() {};
// 标记类型别名
[[deprecated]]
typedef char* CString;
// 标记结构体或类
struct [[deprecated]] OldStruct {};
class [[deprecated("改用 DatabaseConnection")]] OldConnection {};
// 标记变量
[[deprecated]]
int obsolete_variable;
// 版本迁移示例
[[deprecated("这是v1.0版本,请使用v2.改用connect(url, options)")]]
void connect(std::string url) {}
// 命名空间(C++17起支持)
namespace [[deprecated("迁移到 utils 命名空间")]] helpers { /* ... */ }
// 枚举值(C++17起支持)
enum class Color {
Red,
Green [[deprecated("使用 Blue 替代")]],
Blue
};
// v2.0
void connect(std::string url, int options) {}
int main() {
int x = old_function(); // 编译时会产生警告
CString ptr = nullptr;
connect("https://en.cppreference.com");
return 0;
}
10.5 C++17新增属性
C++17引入了三个关键属性:
| 属性 | 用途 | 说明 |
|---|---|---|
[[fallthrough]] |
switch语句 | 明确表示有意不写break的"贯穿"行为 |
[[nodiscard]] |
函数返回值 | 强制开发者必须处理函数的返回值 |
[[maybe_unused]] |
变量/参数 | 抑制未使用变量或参数的警告 |
cpp
#include <iostream>
// [[fallthrough]] 属性
void checkValue(int x) {
switch (x) {
case 1:
std::cout << "Case 1" << std::endl;
[[fallthrough]]; // 故意不break,明确告知编译器
case 2:
std::cout << "Case 2" << std::endl;
break;
default:
std::cout << "Default case" << std::endl;
}
}
// [[nodiscard]] 属性
[[nodiscard]] int computeSomethingImportant() {
return 42;
}
// [[maybe_unused]] 属性
void someFunction([[maybe_unused]] int unusedParam) {
// 当有意定义一个可能不会使用的变量时,避免编译器产生警告
[[maybe_unused]] int unusedVar = 10;
}
int main() {
checkValue(1);
// 忽略[[nodiscard]]结果的警告
computeSomethingImportant(); // 编译器可能会警告
// 正确的方式是使用返回值
int result = computeSomethingImportant();
std::cout << "Result: " << result << std::endl;
someFunction(100);
return 0;
}
注意 :
[[maybe_unused]]提供了一种比传统的(void)param;更清晰的方式来表达"此参数可能未被使用是预期行为"。
10.6 C++20新增属性
10.6.1 [[likely]] 与 [[unlikely]]
用于向编译器提供静态分支预测提示,指引编译器优化指令布局。
cpp
#include <iostream>
int process(int value) {
if (value > 0) [[likely]] {
// 这个分支在绝大多数情况下都会执行
return value * 2;
} else [[unlikely]] {
// 错误处理路径,很少执行
return -1;
}
}
void func() {
int status = 0;
std::cin >> status;
// 也可以用于 switch 语句
switch (status) {
case 1: [[likely]]
// handle_success();
break;
case 2: [[unlikely]]
// handle_error();
break;
default: [[unlikely]]
// handle_unknown();
break;
}
}
int main() {
std::cout << process(5) << std::endl;
return 0;
}
重要:这些属性仅是优化提示,编译器可完全忽略。应在有确凿证据(如性能剖析数据)时谨慎使用,避免误导编译器优化决策。
10.6.2 [[no_unique_address]]
用于优化类的内存布局,减少空类型成员的内存占用。
cpp
#include <memory>
#include <iostream>
struct Empty1 {};
struct Empty2 {};
struct Foo {
int x; // 4 bytes
[[no_unique_address]] Empty1 e1; // 可能被优化掉
[[no_unique_address]] Empty2 e2; // 可能被优化掉
};
// 空基类优化 (EBO - Empty Base Optimization)
struct Fxx : Empty1 {
int x;
};
struct Fyy : Empty1, Empty2 {
int y;
};
// 实践使用场景
template<typename Allocator = std::allocator<int>>
class Vector {
private:
int* data;
size_t size;
size_t capacity;
[[no_unique_address]] Allocator alloc; // 分配器没有非静态成员变量
};
int main() {
std::cout << sizeof(Empty1) << std::endl; // 通常输出 1
std::cout << sizeof(Foo) << std::endl; // 可能输出 4(优化后)
std::cout << sizeof(Fxx) << std::endl; // 通常输出 4(EBO优化)
std::cout << sizeof(Fyy) << std::endl; // 通常输出 8
std::cout << sizeof(std::allocator<int>) << std::endl; // 通常输出 1
return 0;
}
注意事项:
- 优化仅在成员类型为空类时有效
- 同一类中多个相同类型的空成员可能无法优化
- 编译器实现存在差异(GCC/Clang通常能优化,MSVC较保守)
11. 新的求值顺序规则
C++17通过明确定义核心表达式的求值顺序,提升了代码行为的可预测性与安全性。
11.1 关键变化
在C++17之前,函数参数求值顺序未指定可能导致内存泄漏等问题:
cpp
// C++17之前的危险写法
void foo(std::shared_ptr<MyClass> ptr, int priority);
foo(std::shared_ptr<MyClass>(new MyClass()), calculate_priority());
问题:编译器可能按以下顺序执行:
new MyClass()分配内存- 调用
calculate_priority()(抛出异常) - 构造
shared_ptr<MyClass>(从未执行)
导致内存泄漏。
C++17解决方案:
- 每个实参的求值在该函数调用之前完成
- 实参间求值不交错
- 但仍未规定实参之间的相互顺序
安全写法(始终推荐):
cpp
foo(std::make_shared<MyClass>(), calculate_priority());
11.2 C++17求值顺序规则总结
| 表达式类型 | C++17 之前的情况 | C++17 的规定 | 示例与说明 |
|---|---|---|---|
| 函数调用参数 | 实参间的求值顺序完全未指定,允许交错执行 | 每个实参的求值在函数调用前完成,且实参间求值不交错 | f(a(), b()):可先a()后b()或相反,但不会交错 |
| 赋值运算符 | 未指定 | 从右向左求值,右侧在左侧之前 | a = b:先求b,再求a |
| 成员访问运算符 | 未指定 | 对象表达式在成员表达式之前求值 | obj.mem:先求obj,再求mem |
new表达式 |
分配、初始化顺序未完全指定 | 严格按顺序:分配内存→构造函数参数求值→初始化对象→返回指针 | 避免内存分配成功但初始化异常导致的泄漏 |
| 逻辑运算符 | 短路求值已保证,但操作数内顺序未指定 | 从左到右,且短路行为明确 | a && b:a为假则不执行b |
12. std::optional
std::optional<T>是C++17标准库中引入的类模板,用于表示一个可能包含 类型T的值,也可能不包含任何值的对象。
12.1 解决的问题
传统处理"可能无返回值"的模式存在的问题:
- 返回特殊值 :如
-1、nullptr等,缺乏类型系统保障 - 使用输出参数:语法笨拙,不够直观
- 抛出异常:不是所有"无结果"都算异常,性能开销大
12.2 基本使用
cpp
#include <iostream>
#include <optional>
#include <map>
#include <vector>
#include <string>
void test_optional_basics() {
// 1. 定义optional对象
std::optional<int> maybeInt; // 默认构造(空状态)
std::optional<std::string> maybeString = "Hello"; // 初始有值
std::optional<double> empty = std::nullopt; // 赋值设置为空
// 2. 检查是否有值
if (maybeInt.has_value()) {
std::cout << "has_value1" << std::endl;
}
if (maybeString) { // 通过operator bool
std::cout << "has_value2: " << *maybeString << std::endl;
}
// 3. 访问值
maybeInt = 1;
// 不安全但快速的访问
int value1 = *maybeInt; // 若容器无值则行为未定义
// 带默认值的访问
int value2 = maybeInt.value_or(2); // 无值时返回2
std::cout << "value2: " << value2 << std::endl;
// 4. 修改值
maybeInt = 42; // 赋新值
maybeInt = std::nullopt; // 设为空
maybeInt.reset(); // 设为空
}
12.3 实践场景
cpp
// 查找示例
void test_optional_practical() {
std::map<std::string, int> indexMap = {
{"张庄", 1},
{"王村", 2},
{"李家村", 3}
};
auto findIndex = const std::string& str -> std::optional<int> {
auto it = indexMap.find(str);
if (it != indexMap.end()) {
return it->second;
} else {
return std::nullopt;
}
};
std::string x = "王村";
std::optional<int> index = findIndex(x);
if (index) {
std::cout << x << "对应的编号为:" << *index << std::endl;
} else {
std::cout << x << "是非法顶点" << std::endl;
}
// 安全访问示例
std::vector<std::string> v = {"张庄", "李庄", ""};
auto access = int i -> std::optional<std::string> {
if (i < v.size()) {
return v[i];
} else {
return std::nullopt; // 以前只能抛异常或断言
}
};
auto ret = access(1);
if (ret) {
std::cout << "访问结果: " << ret.value() << std::endl;
}
}
12.4 核心接口
cpp
// 创建
std::optional<T> opt; // 空optional
std::optional<T> opt(value); // 包含值
std::optional<T> opt = value; // 同上
std::optional<T> opt = std::nullopt; // 空
// 检查
bool has = opt.has_value(); // 是否有值
bool has = static_cast<bool>(opt); // 同上
bool empty = !opt; // 是否为空
// 访问
T& value = opt.value(); // 访问值(空则抛异常)
T& value = *opt; // 访问值(空则未定义行为)
T value = opt.value_or(default); // 返回值或默认值
// 修改
opt.emplace(args...); // 原位构造新值
opt.reset(); // 重置为空
opt = value; // 赋值
opt = std::nullopt; // 设为空
12.5 优势
- 类型安全:强制调用者显式处理"无值"情况
- 表达力强:意图通过类型一目了然
- 性能良好:通常实现为直接存储值+bool标志
- 无动态分配:值直接存储在optional对象中
cpp
int main() {
test_optional_basics();
test_optional_practical();
return 0;
}
13. std::variant
std::variant 是 C++17 标准库中引入的类型安全的联合体。它可以持有其模板参数列表中指定的任何一种类型的值,并且在任何时刻,它都恰好存储其中一种类型的值,或者处于"无值"的特殊异常状态。
13.1 解决的问题
传统 C 风格或 C++ 的普通 union 存在显著缺陷:
- 不是类型安全的,程序员必须自行记录当前存储的类型
- 如果访问错误类型,会导致未定义行为
- 无法直接处理非平凡类型(如
std::string)
13.2 核心优势
- 类型安全 :
variant内部自动维护类型索引,访问错误类型时抛出std::bad_variant_access异常 - 支持任意类型:可以存储其模板参数列表中的任何类型,无论是否平凡
- 自动生命周期管理:正确构造、拷贝、析构存储的对象
13.3 基本使用
cpp
#include <iostream>
#include <variant>
#include <string>
int main() {
// 定义一个 variant,可以存储 int, double 或 std::string
std::variant<int, double, std::string> v;
v = 42; // 现在持有int
std::cout << "int: " << std::get<int>(v) << std::endl;
v = 3.14; // 现在持有double
std::cout << "double: " << std::get<double>(v) << std::endl;
v = "hello"; // 现在持有std::string
std::cout << "string: " << std::get<std::string>(v) << std::endl;
// 使用index()获取当前持有的类型的索引值
std::cout << "Current index: " << v.index() << std::endl;
return 0;
}
13.4 访问方式
13.4.1 std::get(抛出异常)
cpp
#include <iostream>
#include <variant>
int main() {
std::variant<int, double> v = 42;
try {
std::cout << std::get<int>(v) << std::endl; // 正确
std::cout << std::get<0>(v) << std::endl; // 正确
std::cout << std::get<double>(v) << std::endl; // 抛出异常
} catch (const std::bad_variant_access& e) {
std::cout << "Error: " << e.what() << std::endl;
}
}
13.4.2 std::get_if(返回指针)
cpp
#include <iostream>
#include <variant>
#include <vector>
#include <string>
int main() {
using value_t = std::variant<int, double, std::string>;
std::vector<value_t> vV{1, 3.14, "hello"};
for (auto& v : vV) {
if (auto pval = std::get_if<int>(&v)) {
std::cout << "int value: " << *pval << std::endl;
} else if (auto pval = std::get_if<double>(&v)) {
std::cout << "double value: " << *pval << std::endl;
} else if (auto pval = std::get_if<std::string>(&v)) {
std::cout << "string value: " << *pval << std::endl;
}
}
}
13.4.3 std::visit(推荐方式)
cpp
#include <iostream>
#include <variant>
#include <vector>
#include <string>
#include <iomanip>
using value_t = std::variant<int, double, std::string>;
// 方法1: 传统访问者类
struct VisitorOP {
void operator()(int i) const {
std::cout << "int: " << i << '\n';
}
void operator()(double d) const {
std::cout << "double: " << d << '\n';
}
void operator()(const std::string& s) const {
std::cout << "string: " << s << '\n';
}
};
// 方法2: 使用overloaded组合多个lambda
template<class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int main() {
std::vector<value_t> vec = {10, 1.5, "hello"};
// 使用传统访问者类
for (auto& v : vec) {
std::visit(VisitorOP(), v);
}
// 使用overloaded组合多个lambda
for (auto& v : vec) {
std::visit(overloaded{
int arg { std::cout << arg << ' '; },
double arg { std::cout << arg << ' '; },
const std::string& arg { std::cout << arg << ' '; }
}, v);
}
return 0;
}
13.5 综合示例:哈希表实现
cpp
#include <iostream>
#include <variant>
#include <vector>
#include <list>
#include <set>
#include <algorithm>
template<class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
template<class K, size_t Len = 8>
class HashTable {
using Value = std::variant<std::list<K>, std::set<K>>;
std::vector<Value> _tables;
public:
HashTable(size_t size = 10) : _tables(size) {}
void Insert(const K& key) {
size_t hashi = key % _tables.size();
std::visit(overloaded{
std::list<K>& lt {
if (lt.size() < Len) {
lt.push_back(key);
} else {
// 转换为红黑树
std::set<K> s(lt.begin(), lt.end());
s.insert(key);
_tables[hashi] = std::move(s);
}
},
std::set<K>& s {
s.insert(key);
}
}, _tables[hashi]);
}
bool Find(const K& key) {
size_t hashi = key % _tables.size();
return std::visit(overloaded{
std::list<K>& lt -> bool {
return std::find(lt.begin(), lt.end(), key) != lt.end();
},
std::set<K>& s -> bool {
return s.count(key) > 0;
}
}, _tables[hashi]);
}
};
int main() {
HashTable<int> ht;
for (size_t i = 0; i < 10; i++) {
ht.Insert(i * 10 + 5);
}
std::cout << ht.Find(15) << std::endl; // 应该找到
std::cout << ht.Find(3) << std::endl; // 应该找不到
return 0;
}
14. std::any
std::any 是 C++17 标准库中引入的类,它是一个可以存储任意单个类型值 的容器。与 std::variant 必须预先指定一组固定的类型不同,std::any 对其所容纳的类型几乎是完全开放的,唯一的约束是该类型必须是可拷贝构造的。
14.1 核心特性
- 类型擦除:利用类型擦除技术来记录和管理值
- 类型安全 :通过
std::any_cast进行显式且安全的类型转换 - 自动生命周期管理:负责内部存储对象的构造、拷贝和析构
- 小缓冲区优化:通常采用小缓冲区优化避免为小型对象频繁堆分配
14.2 基本使用
cpp
#include <any>
#include <iostream>
#include <string>
#include <utility>
int main() {
// 1. 基本赋值:存储不同类型的值
std::any a1 = 42; // 存放 int
std::any a2 = 3.14; // 存放 double
std::any a3 = std::string("Hello"); // 存放 std::string
// 2. 赋值复杂类型
std::any a4 = std::pair<std::string, std::string>("xxxx", "yyyy");
// 3. 使用 emplace
a3.emplace<std::string>("World");
// 4. 查看 std::any 对象的大小
std::cout << "Size of a1 (int): " << sizeof(a1) << '\n';
std::cout << "Size of a4 (pair): " << sizeof(a4) << '\n';
// 5. 检查是否有值并获取类型信息
if (a3.has_value()) {
std::cout << "a3 has value" << std::endl;
std::cout << "a3 type: " << a3.type().name() << std::endl;
}
return 0;
}
14.3 any_cast 的三种取值方式
cpp
#include <iostream>
#include <any>
#include <string>
#include <vector>
#include <cassert>
void anyVector() {
std::string str("hello world");
std::vector<std::any> v = {1.1, 2, str};
for (const auto& item : v) {
if (item.type() == typeid(int)) {
std::cout << "整数配置: " << std::any_cast<int>(item) << '\n';
} else if (item.type() == typeid(double)) {
std::cout << "浮点配置: " << std::any_cast<double>(item) << '\n';
} else if (item.type() == typeid(std::string)) {
std::cout << "字符串配置: " << std::any_cast<const std::string&>(item) << '\n';
} else {
assert(false);
}
}
}
int main() {
std::any a1 = 42;
std::any a2 = 3.14;
std::any a3 = std::string("Hello");
// 方式一:转换为值的类型(抛出异常)
try {
int int_value = std::any_cast<int>(a1);
std::cout << "Value: " << int_value << '\n';
// double double_value = std::any_cast<double>(a1); // 错误!抛出异常
} catch (const std::bad_any_cast& e) {
std::cout << "Cast failed: " << e.what() << '\n';
}
// 方式二:转换为值的引用
std::string& str_ref = std::any_cast<std::string&>(a3);
str_ref[0]++;
std::cout << std::any_cast<std::string&>(a3) << '\n'; // 输出 "Iello"
// 方式三:转换为指针(返回nullptr,不抛出异常)
if (auto ptr = std::any_cast<int>(&a1)) {
std::cout << "Value via pointer: " << *ptr << '\n';
} else {
std::cout << "Not an int or is empty.\n";
}
anyVector();
return 0;
}
14.4 综合示例:哈希表实现
cpp
#include <any>
#include <cassert>
#include <iostream>
#include <list>
#include <set>
#include <vector>
#include <algorithm>
template <class K, size_t Len = 8>
class HashTable {
public:
HashTable() : _tables(10, std::list<K>()) {}
void Insert(const K& key) {
size_t hashi = key % _tables.size();
auto listInsert = std::list<K>& lt {
if (lt.size() < Len) {
lt.push_back(key);
} else {
std::set<K> s(lt.begin(), lt.end());
s.insert(key);
_tables[hashi] = std::move(s);
}
};
auto setInsert = std::set<K>& s { s.insert(key); };
if (auto ptr = std::any_cast<std::list<K>>(&_tables[hashi])) {
listInsert(*ptr);
} else if (auto ptr = std::any_cast<std::set<K>>(&_tables[hashi])) {
setInsert(*ptr);
} else {
assert(false);
}
}
bool Find(const K& key) {
size_t hashi = key % _tables.size();
if (!_tables[hashi].has_value())
return false;
auto listFind = std::list<K>& lt -> bool {
return std::find(lt.begin(), lt.end(), key) != lt.end();
};
auto setFind = std::set<K>& s -> bool {
return s.count(key) > 0;
};
if (auto ptr = std::any_cast<std::list<K>>(&_tables[hashi])) {
return listFind(*ptr);
} else if (auto ptr = std::any_cast<std::set<K>>(&_tables[hashi])) {
return setFind(*ptr);
}
return false;
}
private:
std::vector<std::any> _tables;
};
int main() {
HashTable<int> ht;
for (size_t i = 0; i < 10; i++) {
ht.Insert(i * 10 + 5);
}
std::cout << ht.Find(15) << std::endl; // 应该找到
std::cout << ht.Find(3) << std::endl; // 应该找不到
return 0;
}
14.5 std::any 与 std::variant 对比
| 特性 | std::variant |
std::any |
|---|---|---|
| 设计哲学 | 类型枚举的联合体 | 类型擦除的通用容器 |
| 类型确定性 | 编译时确定所有可能类型 | 运行时确定类型 |
| 访问方式 | std::get、std::visit |
std::any_cast |
| 性能 | 接近O(1)的跳转表 | 运行时类型查询 |
| 类型安全 | 编译时类型检查 | 运行时类型检查 |
| 适用场景 | 已知、有限的类型集合 | 编译期完全无法预知的类型 |
| 存储开销 | 所有可选项类型的最大值+管理开销 | 固定开销+类型信息 |
选用建议:
- 优先使用
std::variant:当需要在一组已知、有限的类型之间进行选择时 - 谨慎使用
std::any:仅当需要存储的类型在编译期完全无法预知时才考虑
15. std::string_view
std::string_view 是 C++17 标准库中引入的非拥有(non-owning)的字符串视图类。它提供了一种零开销、轻量级 的方式来"观察"一个已有的字符序列,而无需复制底层数据。
15.1 核心特性
- 非拥有:不分配、不管理、不拥有所指向字符串的内存
- 只读观察 :提供与
std::string相似的只读接口 - 零开销:仅包含指针和长度两个成员
constexpr支持 :大量成员函数标记为constexpr- 统一常量迭代器:迭代器类型统一为常量迭代器
15.2 基本使用
cpp
#include <iostream>
#include <string>
#include <string_view>
// 接受 std::string_view 参数,高效处理各种字符串类型
void print(std::string_view sv) {
std::cout << "String view: " << sv << "\n";
std::cout << "Length: " << sv.size() << "\n";
// 遍历字符
for (auto ch : sv) {
std::cout << ch << " ";
}
std::cout << "\n";
}
int main() {
// 1. 从C风格字符串构造
std::string_view sv1("Hello, world!");
print(sv1);
// 2. 从std::string构造
std::string str = "C++17 string_view";
std::string_view sv2(str);
print(sv2);
// 3. 从部分字符串构造
std::string_view sv3(str.c_str() + 6, 6); // 从第6个字符开始,取6个字符
print(sv3); // 输出: "string"
// 4. 使用字符串字面量后缀构造
using namespace std::literals;
std::string_view sv4 = "Literal"sv;
print(sv4);
return 0;
}
15.3 性能对比
cpp
#include <chrono>
#include <iostream>
#include <string>
#include <string_view>
// 使用 std::string 引用参数
void process_string(const std::string& s) {
// 函数体为空,仅测试参数传递开销
}
// 使用 std::string_view 参数
void process_string_view(std::string_view sv) {
// 函数体为空,仅测试参数传递开销
}
int main() {
const int N = 100'0000; // 循环次数:100万
std::string long_str = "This is a very long string...";
// 测试 std::string 引用参数
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
process_string(long_str); // 无额外开销
process_string("literal"); // 需要创建临时 string 对象
}
auto end1 = std::chrono::high_resolution_clock::now();
// 测试 std::string_view 参数
auto start2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
process_string_view(long_str); // 隐式转换,无拷贝
process_string_view("literal"); // 无临时对象,仅构造视图
}
auto end2 = std::chrono::high_resolution_clock::now();
auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1);
auto duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2);
std::cout << "std::string (引用): " << duration1.count() << " ms\n";
std::cout << "std::string_view: " << duration2.count() << " ms\n";
return 0;
}
15.4 使用场景
cpp
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
// 场景1:函数参数
void process_string(std::string_view sv) {
// 处理字符串,无需关心原始类型
}
// 场景2:解析字符串,零拷贝
std::string_view extract_str(std::string_view input, char delimiter) {
size_t pos = input.find(delimiter);
return input.substr(0, pos);
}
// 场景3:切分字符串
std::vector<std::string_view> split(std::string_view str, char delimiter) {
std::vector<std::string_view> result;
size_t start = 0;
size_t end = str.find(delimiter);
while (end != std::string_view::npos) {
result.push_back(str.substr(start, end - start));
start = end + 1;
end = str.find(delimiter, start);
}
result.push_back(str.substr(start));
return result;
}
int main() {
process_string("C-string");
std::string s("std::string");
process_string(s);
const char* str = "https://chat.deepseek.com/a/chat/s/4ae60e97";
std::string_view sv = extract_str(str, ':');
std::cout << "协议: " << sv << std::endl;
auto v = split(str + 9, '/'); // 跳过"https://"部分
std::cout << "路径部分: " << std::endl;
for (auto e : v) {
std::cout << e << std::endl;
}
return 0;
}
15.5 常见问题与注意事项
cpp
#include <iostream>
#include <string>
#include <string_view>
#include <cstdio>
// 错误示例1:返回局部string的视图,导致悬垂引用
std::string_view get_view() {
std::string temp = "Temporary string";
return temp; // 严重错误!temp将被销毁
}
// 错误示例2:将非空终止的string_view传递给C风格API
void error_example2() {
char buffer[] = {'T', 'e', 's', 't', '.', 't', 'x', 't'}; // 不是空终止的
std::string_view sv(buffer, 4); // 正确指定长度
std::cout << sv << std::endl; // 输出: Test
std::string_view filename(buffer); // 使用整个buffer,但buffer没有'\0'终止符
printf("%s\n", sv.data()); // 危险:sv.data()不是C风格字符串
}
// 错误示例3:在底层字符串修改后使用string_view
void error_example3() {
std::string str = "Hello";
std::string_view sv = str;
str = "New value 111111111111111111111111"; // 可能导致重新分配
std::cout << sv << std::endl; // 危险:sv可能已失效
}
int main() {
// error_example1(); // 将导致未定义行为
error_example2(); // 将导致未定义行为
error_example3(); // 将导致未定义行为
return 0;
}
重要注意事项:
- 生命周期管理 :必须确保
std::string_view的生命周期不超过其底层数据源 - 非空终止 :
sv.data()返回的指针不一定以\0结尾 - 不可修改 :
std::string_view是只读视图 - 底层数据修改:如果底层字符串被修改(如重分配),视图将失效
16. std::filesystem(文件系统库)
std::filesystem 是 C++17 引入的现代化、跨平台的文件系统处理库,简化了文件和目录的操作。
16.1 核心类
fs::path:表示和操作文件系统路径fs::directory_entry:表示目录中的一个条目fs::directory_iterator:遍历目录内容fs::recursive_directory_iterator:递归遍历目录fs::file_status:封装文件状态信息
16.2 路径操作
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path p = R"(C:\Users\user\source\repos\Project38\源.cpp)";
std::cout << p << std::endl;
std::cout << "根名称: " << p.root_name() << std::endl;
std::cout << "根目录: " << p.root_directory() << std::endl;
std::cout << "根路径: " << p.root_path() << std::endl;
std::cout << "相对路径: " << p.relative_path() << std::endl;
std::cout << "父路径: " << p.parent_path() << std::endl;
std::cout << "文件名: " << p.filename() << std::endl;
std::cout << "主文件名: " << p.stem() << std::endl;
std::cout << "扩展名: " << p.extension() << std::endl;
// 路径操作
fs::path p5 = "temp";
p5 /= "subdir";
p5 /= "file.log";
std::cout << p5 << std::endl;
p5.replace_filename("data.json");
std::cout << p5 << std::endl;
p5.remove_filename();
std::cout << p5 << std::endl;
return 0;
}
16.3 文件状态
cpp
#include <filesystem>
#include <iostream>
#include <fstream>
#include <chrono>
namespace fs = std::filesystem;
int main() {
fs::path p = "源.cpp";
if (fs::exists(p)) {
std::cout << "File exists!\n";
if (fs::is_regular_file(p)) {
std::cout << "It's a regular file.\n";
std::cout << "File size: " << fs::file_size(p) << " bytes\n";
} else if (fs::is_directory(p)) {
std::cout << "It's a directory.\n";
}
// 获取最后修改时间
auto ftime = fs::last_write_time(p);
using namespace std::chrono_literals;
// 修改文件的最后修改时间
fs::last_write_time(p, ftime + 1h);
} else {
std::cout << "File does not exist.\n";
}
return 0;
}
16.4 文件权限
cpp
#include <filesystem>
#include <iostream>
#include <fstream>
namespace fs = std::filesystem;
void demo_perms(fs::perms p) {
auto show = char op, fs::perms perm {
std::cout << (fs::perms::none == (perm & p) ? '-' : op);
};
show('r', fs::perms::owner_read);
show('w', fs::perms::owner_write);
show('x', fs::perms::owner_exec);
std::cout << ' ';
show('r', fs::perms::group_read);
show('w', fs::perms::group_write);
show('x', fs::perms::group_exec);
std::cout << ' ';
show('r', fs::perms::others_read);
show('w', fs::perms::others_write);
show('x', fs::perms::others_exec);
std::cout << "\n";
}
int main() {
try {
fs::path file_path = "test.txt";
std::ofstream(file_path) << "hello";
demo_perms(fs::status(file_path).permissions());
// 替换权限
fs::permissions(file_path,
fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read | fs::perms::others_read,
fs::perm_options::replace);
demo_perms(fs::status(file_path).permissions());
// 添加权限
fs::permissions(file_path,
fs::perms::group_write,
fs::perm_options::add);
demo_perms(fs::status(file_path).permissions());
// 移除权限
fs::permissions(file_path,
fs::perms::others_read,
fs::perm_options::remove);
demo_perms(fs::status(file_path).permissions());
} catch (const std::exception& e) {
std::cout << e.what() << '\n';
}
return 0;
}
16.5 文件或目录操作
cpp
#include <filesystem>
#include <iostream>
#include <fstream>
namespace fs = std::filesystem;
int main() {
try {
fs::path p1 = fs::current_path() / "源.cpp";
std::cout << fs::exists(p1) << std::endl;
std::cout << fs::file_size(p1) << std::endl;
// 创建目录
fs::path p2 = fs::current_path() / "xx";
fs::create_directory(p2);
// 递归创建目录
fs::path p3 = p2 / R"(1\2\3)";
fs::create_directories(p3);
// 复制文件
fs::path p4 = p2 / "inset.cpp";
std::ofstream ofs(p4);
ofs.close();
fs::copy_file(p1, p4, fs::copy_options::overwrite_existing);
// 重命名
fs::rename(p4, p2 / "xx.cpp");
// 移动
fs::rename(p2 / "xx.cpp", "xx.cpp");
// 复制目录
fs::copy(p2, "xx_copy");
fs::copy(p2, "xx_copy", fs::copy_options::recursive |
fs::copy_options::overwrite_existing);
// 删除
fs::remove(p3);
fs::remove_all(fs::current_path() / "xx_copy");
} catch (const std::exception& e) {
std::cout << e.what() << '\n';
}
return 0;
}
16.6 目录遍历
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path p1 = fs::current_path();
try {
// 非递归遍历
std::cout << "当前目录内容:\n";
for (const auto& entry : fs::directory_iterator(p1)) {
std::cout << " " << entry.path().filename() << " - "
<< (entry.is_directory() ? "目录" : "文件") << "\n";
}
std::cout << "\n递归遍历Debug目录:\n";
// 递归遍历
for (const auto& entry :
fs::recursive_directory_iterator(fs::current_path() / "Debug")) {
std::cout << " " << entry.path().filename() << " - "
<< (entry.is_directory() ? "目录" : "文件") << "\n";
if (entry.is_regular_file()) {
std::cout << " 大小: " << entry.file_size() << "字节\n";
}
}
} catch (const std::exception& e) {
std::cout << e.what() << '\n';
}
return 0;
}
16.7 异常和错误码处理
cpp
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
// 异常的方式
try {
fs::path p = "nonexistent.txt";
auto size = fs::file_size(p); // 可能抛出异常
std::cout << "文件大小: " << size << std::endl;
} catch (const fs::filesystem_error& ex) {
std::cout << "what(): " << ex.what() << '\n'
<< "path1(): " << ex.path1() << '\n'
<< "path2(): " << ex.path2() << '\n';
}
// 错误码方式
std::error_code ec;
fs::path p = "nonexistent.txt";
auto size = fs::file_size(p, ec);
if (ec) {
std::cout << "错误: " << ec << std::endl;
std::cout << "错误代码: " << ec.value() << std::endl;
std::cout << "错误信息: " << ec.message() << std::endl;
std::cout << "错误类别: " << ec.category().name() << std::endl;
} else {
std::cout << "文件大小: " << size << std::endl;
}
return 0;
}
16.8 核心功能总结
| 功能类别 | 常用函数 | 说明 |
|---|---|---|
| 路径操作 | fs::path 构造函数、/=、replace_filename |
路径的表示与操作 |
| 文件状态 | exists、is_regular_file、is_directory |
检查文件类型和存在性 |
| 文件属性 | file_size、last_write_time |
获取文件属性和时间戳 |
| 权限管理 | permissions、status |
文件权限的查询和设置 |
| 创建删除 | create_directory、remove、remove_all |
目录和文件的创建删除 |
| 复制移动 | copy、copy_file、rename |
文件和目录的复制移动 |
| 目录遍历 | directory_iterator、recursive_directory_iterator |
遍历目录内容 |
| 错误处理 | 异常、std::error_code |
两种错误处理方式 |
跨平台注意事项:
- Windows 使用 ACL 权限模型,
std::filesystem::perms是简化映射 - 路径分隔符自动处理(Windows:
\,POSIX:/) - 文件时间精度可能因系统而异
17. 并行算法
C++17 引入的并行算法是对标准库算法的一个重要扩展,它允许开发者通过一个简单的执行策略参数,就能轻松地将许多常见的算法操作并行化。
17.1 执行策略
C++17 标准定义了三种主要的执行策略:
| 执行策略 | 含义 | 说明 |
|---|---|---|
std::execution::seq |
顺序执行 | 行为与不指定策略的传统算法一致 |
std::execution::par |
并行执行 | 允许算法在多线程中并行执行 |
std::execution::par_unseq |
并行+向量化执行 | 允许并行化和向量化(例如使用SIMD指令) |
C++20 又补充了 std::execution::unsequenced 策略,专门用于允许向量化执行。
17.2 支持并行化的算法
标准库中众多算法都已支持并行执行,主要包括:
- 排序算法 :如
sort、stable_sort - 数值算法 :如
reduce、transform_reduce - 查找算法 :如
find、count - 其他常用算法 :如
for_each、transform、copy等
17.3 使用示例
cpp
#include <algorithm>
#include <execution>
#include <vector>
#include <chrono>
#include <iostream>
void performance_comparison() {
std::vector<int> data1(1000'0000);
std::generate(data1.begin(), data1.end(), { return rand(); });
std::vector<int> data2(data1);
auto start = std::chrono::high_resolution_clock::now();
std::sort(data1.begin(), data1.end()); // 顺序排序
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Sequential sort: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms\n";
start = std::chrono::high_resolution_clock::now();
std::sort(std::execution::par, data2.begin(), data2.end()); // 并行排序
end = std::chrono::high_resolution_clock::now();
std::cout << "Parallel sort: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " ms\n";
}
int main() {
performance_comparison();
return 0;
}
17.4 更多示例
cpp
#include <iostream>
#include <vector>
#include <numeric>
#include <execution>
#include <algorithm>
int main() {
// 示例1:并行累加
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto sum = std::reduce(std::execution::par, v.begin(), v.end());
std::cout << "sum = " << sum << '\n';
// 示例2:并行变换
std::vector<int> v2 = {1, 2, 3, 4, 5};
std::vector<int> result(v2.size());
std::transform(std::execution::par, v2.begin(), v2.end(), result.begin(),
int x { return x * x; });
for (int x : result) {
std::cout << x << ' ';
}
std::cout << '\n';
// 示例3:并行for_each
std::vector<int> v3 = {1, 2, 3, 4, 5};
std::for_each(std::execution::par, v3.begin(), v3.end(),
int& x { x *= 2; });
for (int x : v3) {
std::cout << x << ' ';
}
std::cout << '\n';
return 0;
}
17.5 注意事项
- 并行算法可能引入数据竞争:如果并行执行的操作访问共享数据,必须确保操作是线程安全的。
- 执行策略是可选的:如果不指定执行策略,算法将按顺序执行。
- 性能提升取决于问题规模:对于小规模数据,并行化可能因为线程创建和同步的开销而导致性能下降。
- 编译器支持:需要编译器支持并行算法,并且可能需要链接相应的并行运行时库。
17.6 并行算法与异常
并行算法在执行过程中可能会抛出异常。如果有多个异常被抛出,那么其中一个会被传播,而其他异常则会被捕获并终止算法。具体行为取决于执行策略。
17.7 并行算法与复杂度
并行算法的复杂度通常与顺序算法不同,因为并行算法会利用多个执行单元。例如,std::sort 的顺序版本平均复杂度为 O(N log N),而并行版本在理想情况下可以达到 O(N log N / P),其中 P 是处理单元的数量。
17.8 并行算法的限制
并非所有算法都适合并行化。例如,一些算法具有固有的顺序依赖,无法并行执行。此外,并行算法对迭代器的要求可能更严格,通常要求是前向迭代器或随机访问迭代器。
17.9 并行算法的性能考虑
在使用并行算法时,需要考虑以下性能因素:
- 数据局部性:并行算法可能导致缓存一致性问题和伪共享,从而影响性能。
- 负载均衡:并行任务的分割应尽可能均匀,以避免某些线程过早空闲。
- 同步开销:线程间的同步和通信开销可能抵消并行化带来的收益。
17.10 并行算法的未来
C++20 和未来的标准将继续扩展并行算法的支持,包括更多的算法和更灵活的执行策略。同时,硬件的发展(如更多的核心和更强大的向量指令)也将使并行算法更加重要。