语言特性

折叠表达式

C++17中引入了折叠表达式,专门用于简化「可变参数模板」中参数包的遍历 / 计算—— 它用简洁的语法替代了 C++11/14 中繁琐的递归拆分参数包,一行代码就能完成参数包的遍历、累加、拼接等操作。

语法格式(4 种基础形式)

类型 语法 说明
一元左折叠 ( ... op args ) 从左到右计算:((arg1 op arg2) op arg3) op ...
一元右折叠 ( args op ... ) 从右到左计算:arg1 op (arg2 op (arg3 op ...))
二元左折叠 ( init op ... op args ) 带初始值的左折叠:(((init op arg1) op arg2) op arg3) op ...
二元右折叠 ( args op ... op init ) 带初始值的右折叠:arg1 op (arg2 op (arg3 op ... op init))
  • op:支持的运算符(+、-、*、/、<<、&&、||、, 等);
  • args:可变参数模板的参数包;
  • init:二元折叠的初始值(可选)。

案例

1
2
3
4
5
6
template<typename... Args>
bool all(Args... args) { return (... && args); }
bool b = all(true, true, true, false);
// 在 all() 中,一元左折叠展开成
// return ((true && true) && true) && false;
// b 是 false

参数包累加案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

// C++17折叠表达式:一行实现参数包累加
template <typename... Args>
auto sum(Args... args) {
return (... + args); // 一元左折叠:((arg1 + arg2) + arg3) + ...
}

int main() {
cout << sum(1, 2, 3, 4) << endl; // 输出10(1+2+3+4)
cout << sum(1.5, 2.5, 3) << endl; // 输出7.0(1.5+2.5+3)
return 0;
}

对比 C++11 的递归写法(需要递归终止函数):

1
2
3
4
5
6
7
// C++11递归写法(繁琐)
int sum() { return 0; } // 递归终止

template <typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}

常用场景

  1. 通用打印函数(最常用)

    <<运算符折叠,一行实现任意参数的打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

template <typename... Args>
void print(Args... args) {
// 一元左折叠:((cout << arg1) << arg2) << arg3 ... 最后换行
(cout << ... << args) << endl;

// 带分隔符的打印(用逗号运算符)
// (cout << ... << (args, cout << " ")) << endl;
}

int main() {
print(10, "hello", 3.14, 'A'); // 输出:10hello3.14A
// 带分隔符版本输出:10 hello 3.14 A
return 0;
}
  1. 参数包逻辑判断(&&/||)

    判断所有参数是否满足条件(比如全为正数):

1
2
3
4
5
6
7
8
9
10
11
template <typename... Args>
bool all_positive(Args... args) {
return (... && (args > 0)); // 所有参数>0才返回true
}

int main() {
cout << boolalpha; // 输出true/false而非1/0
cout << all_positive(1, 2, 3) << endl; // true
cout << all_positive(1, -2, 3) << endl; // false
return 0;
}
  1. 带初始值的二元折叠

    比如计算参数包的乘积,初始值为 1(避免空参数包报错):

1
2
3
4
5
6
7
8
9
10
11
template <typename... Args>
auto product(Args... args) {
// 二元左折叠:(((1 * arg1) * arg2) * arg3) ...
return (1 * ... * args);
}

int main() {
cout << product(2, 3, 4) << endl; // 24(1*2*3*4)
cout << product() << endl; // 1(空参数包返回初始值1)
return 0;
}

类模版参数推导

类模板实例化时,可以不必显式指定类型,前提是保证类型可以推导

类型模版形参

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
template<class T>
class ClassTest
{
public:
ClassTest(T, T) {};
};
int main() {
auto y = new ClassTest{ 100, 200 }; // 分配的类型是 ClassTest<int>
return 0;
}

auto 占位的非类型模板形参

​ 非类型模板参数(NTTP)是指模板参数不是 “类型”(如int/string),而是 “常量值”(如整数、指针、数组大小等),C++20 前必须显式指定其类型。

​ C++20 允许用auto作为非类型模板参数的类型占位符,编译器会根据传入的常量值自动推导其类型,无需显式指定。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
template <auto T> void func1() {
cout << T << endl;
}
int main() {
func1<100>();
//func1<int>();
return 0;
}

编译期constexpr if语句

constexpr if是 C++17 引入的编译期分支选择工具,核心是 “编译器在编译时就决定保留哪个分支、丢弃哪个分支”,而非运行时判断。

核心定义 & 本质

  • 语法if constexpr (编译期常量表达式) { ... } else { ... }

  • 本质:编译器的 “代码裁剪工具”—— 只保留满足条件的分支,不满足的分支直接从编译后的二进制文件中删除,完全不参与运行。

  • 和普通 if 的核心区别:

    特性 普通 if constexpr if
    判断时机 运行时(程序跑起来才判断) 编译时(程序员写完代码编译阶段)
    未满足分支 编译保留,运行跳过 编译丢弃,完全不存在于最终程序中
    适用场景 运行时逻辑分支(如判断变量值) 编译期类型 / 常量判断(模板、泛型)

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
template <bool ok> constexpr void func2() {
//在编译期进行判断,if和else语句不生成代码
if constexpr (ok == true) {
//当ok为true时,下面的else块不生成汇编代码
cout << "ok" << endl;
}
else {
//当ok为false时,上面的if块不生成汇编代码
cout << "not ok" << endl;
}
}
int main() {
func2<true>(); //输出ok,并且汇编代码中只有 cout << "ok" << endl;
func2<false>(); //输出not ok,并且汇编代码中只有 cout << "not ok" << endl;
return 0;
}

关键规则

  • 条件必须是编译期常量(如is_integral_v<T>constexpr int a=1; if constexpr(a>0)),不能是运行时变量(如int a=1; if constexpr(a>0)会报错);
  • 仅适用于模板 /constexpr上下文(非模板代码用constexpr if意义不大);
  • 被丢弃的分支里,语法必须合法,但语义可以不合法(比如else分支里写val.size(),只要 T 不是整数就不会编译这段代码)。

程序的全生命周期

​ 代码从 “写完” 到 “运行出结果”,会经历5 个核心阶段,每个阶段做完全不同的事

阶段 时间节点 核心工作 通俗类比
预处理期 编译前(预编译阶段) 处理#开头的指令(#include/#define/#ifdef),生成 “纯净源码” 做饭前的食材预处理(洗菜、切菜)
编译期 预处理后 → 生成汇编代码 1. 语法 / 语义检查(报错的主要阶段);2. 把源码翻译成汇编代码;3. constexpr if裁剪代码 把菜谱翻译成 “操作步骤”(只保留需要的步骤)
汇编期 编译后 → 生成目标文件 把汇编代码翻译成机器码(二进制),生成.o/.obj目标文件 把 “操作步骤” 翻译成 “厨师能懂的动作”
链接期 汇编后 → 生成可执行文件 1. 合并多个目标文件;2. 链接系统库 / 第三方库(如cout依赖的标准库);3. 解决函数 / 变量引用 把 “单个动作” 组合成 “完整做饭流程”,并找齐工具(锅碗瓢盆)
运行期 双击可执行文件后 程序加载到内存,CPU 执行机器码,处理运行时逻辑(普通 if、变量赋值、IO 操作等) 厨师按流程做饭,实时调整火候 / 调料

inline变量

inline变量是 C++17 引入的特性,允许变量被标记为 “内联”—— 核心作用是解决「全局变量在多文件包含时的重复定义问题」,同时保留变量的全局可见性,它是inline函数的 “变量版”,规则和inline函数高度相似。

​ 扩展的inline用法,使得可以在头文件或者类内初始化静态成员变量

1
2
3
4
5
6
// mycode.h
inline int value = 100;
// mycode.cpp
class AAA {
inline static int value2 = 200;
};

为什么需要 inline 变量?(解决的核心问题)

​ C++ 中,全局变量如果定义在头文件中,当多个.cpp文件包含该头文件时,会触发 “重复定义” 编译错误(ODR 规则:一个变量只能有一个定义)。

问题示例(无 inline 的坑):

1
2
3
4
5
6
7
8
// header.h 头文件
int global_num = 10; // 全局变量定义在头文件

// a.cpp
#include "header.h" // 定义了global_num

// b.cpp
#include "header.h" // 又定义了global_num → 链接期报错:multiple definition of 'global_num'

​ C++17 前的解决方法是:头文件中声明extern,在一个.cpp文件中定义,例如类的静态成员变量需要 “类内声明、类外定义” —— 但步骤繁琐;inline变量则能直接在头文件中定义,且多文件包含不报错。

inline 变量的核心特性

  1. 唯一性:整个程序中,inline变量只有一个实例(即使多个文件包含定义),所有文件操作的是同一个变量;
  2. 链接属性inline变量默认是外部链接(全局可见),若想限制在当前文件,需加staticstatic inline int num = 10;);
  3. 初始化要求inline变量必须在定义时初始化(如inline int num = 10;,不能只写inline int num;);
  4. 和 inline 函数的类比:
    • inline函数:允许多文件定义,编译器只保留一个实例;
    • inline变量:逻辑完全一致,只是对象从 “函数” 变成 “变量”。

结构化绑定

std::tuple

std::tuple是 C++11 引入的「多元组」容器,能把任意数量、任意类型的元素打包成一个单一对象—— 可以理解为 “增强版的std::pair”(pair只能存 2 个元素,tuple支持任意个),是处理多类型、多值聚合的核心工具。

tuple 的核心特点

  1. 异构性:元素类型可以完全不同(如同时存intstringdouble);
  2. 固定大小:创建后元素数量不可变(和数组类似);
  3. 值语义:元素直接存储在tuple内部,而非引用(除非显式存储引用类型);
  4. 可嵌套tuple内部可以包含另一个tuple(如tuple<int, tuple<string, double>>)。

C++11方案

​ 在C++11中,如果需要获取tuple中元素,需要使用get<>()函数或者tie<>函数,这个函数可以把tuple中的元素值转换为可以绑定到tie<>()左值的集合,也就是说需要已分配好的内存去接收;用起来不方便。

1
2
3
4
5
6
7
8
9
10
int main() {
auto student = make_tuple(string{ "Zhangsan" }, 19, string{ "man" });
string name;
size_t age;
string gender;
tie(name, age, gender) = student;
cout << name << ", " << age << ", " << gender << endl;
// Zhangsan, 19, man
return 0;
}

C++17方案

​ C++17中的结构化绑定,大大方便了类似操作,而且使用引用捕获时,还可以修改捕获对象里面的值,代码也会简洁很多.

1
2
3
4
5
6
7
int main() {
auto student = make_tuple(string{ "Zhangsan" }, 19, string{ "man" });
// 结构化绑定
auto [name, age, gender] = student;
cout << name << "," << age << "," << gender << endl;
return 0;
}

if switch初始化

​ 这是 C++17 引入的语法特性,允许在if/switch语句的条件判断前,先初始化一个变量(仅限当前语句块内有效)—— 解决了 “变量作用域过大”“重复初始化” 的问题,让代码更紧凑、更安全。

1
2
3
4
5
6
7
8
9
10
11
// C++11
unordered_map<string, int> stu1{ {"zhangsan" , 18}, {"wangwu" , 19} };
auto iter = stu1.find("wangwu");
if (iter != stu1.end()) {
cout << iter->second << endl;
}

// C++17
if (auto iter = stu1.find("wangwu"); iter != stu1.end()) {
cout << iter->second << endl;
}

简化的嵌套命名空间

命名空间简介

​ 命名空间是 C++ 的「名字隔离工具」,核心作用是解决 “命名冲突”(比如两个库都有叫func()的函数,用命名空间可以区分)。

  • 本质:给一组变量、函数、类等标识符 “加个前缀”,划分不同的逻辑区域;
  • 核心价值:隔离名字,避免全局作用域的命名冲突,无封装 / 继承等面向对象特性。

新语法

​ 这是 C++17 引入的语法糖,允许用一行代码定义多层嵌套的命名空间,替代传统 “层层嵌套 {}” 的繁琐写法—— 本质是简化命名空间的定义语法,不改变命名空间的语义和作用域规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++17之前定义多层嵌套命名空间需要逐层写namespace和{},代码嵌套层级深、可读性差:
// 传统写法:定义 A::B::C 三层嵌套命名空间
namespace A {
namespace B {
namespace C {
void func1() {}
} // namespace C
} // namespace B
} // namespace A

// C++17简化后,可以一行定义 A::B::C 嵌套命名空间
namespace A::B::C {
void func1() {}
} // namespace A::B::C

using声明语句可以声明多个名称,例如:

1
using std::cout, std::cin;

lambda表达式捕获 *this

​ 一般情况下,lambda表达式访问类成员变量时需要捕获this指针,这个this指针指向原对象,即相当于一个引用,在多线程情况下,有可能lambda的生命周期超过了对象的生命周期,此时,对成员变量的访问是未定义的,会导致 “悬空指针”(访问已销毁的对象)。

[*this]表示按值拷贝当前类的整个对象到 lambda 中,lambda 内部持有对象的副本,而非指针 —— 即使原对象销毁,lambda 仍能安全访问副本的成员,避免悬空指针问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
class ClassTest {
public:
int num;
void func1() {
auto lamfunc = [*this]() { cout << num << endl; };
lamfunc();
}
};
int main() {
ClassTest a;
a.num = 100;
a.func1();
return 0;
}

简化重复命名空间的属性列表

基础理解:

​ C++11 引入了 “属性(Attribute)” 机制(如[[deprecated]]表示废弃),可用于标记命名空间、函数、类等,告知编译器 / 开发者该实体的特性。

​ C++17 前,若一个命名空间被拆分定义(命名空间可分散在多个代码块),且需要给整个命名空间加属性,必须在每个拆分的定义上重复写属性,这就是 “重复命名空间的属性列表” 问题。

属性机制

​ 属性机制是 C++11 开始引入的、给代码(变量 / 函数 / 类 / 命名空间等)贴 “标签” 的工具—— 这些标签不会改变代码的逻辑功能,而是告诉编译器 / 开发者 “这个代码有什么特性、需要怎么处理”,比如 “这个函数已废弃别用了”“这个返回值必须接收”,是一种 “代码注释的升级版”(注释只给人看,属性编译器能识别)

常用的属性主要有:

  1. [[deprecated]]:标记 “废弃 / 过时”,提醒别用了

  2. [[nodiscard]]:标记 “返回值必须接收”,避免浪费。

    当用于描述函数的返回值时,如果调用函数的地方没有获取返回值时,编译器给出警告

  3. [[maybe_unused]]:标记 “可能未使用”,消除无用警告

常用属性形式:

  1. 简单属性:只有名字,不带参数
1
[[noreturn]] void exit(); // 告诉编译器这个函数不会返回
  1. 有命名空间的属性:用命名空间::属性名,避免名字冲突
1
[[gnu::unused]] int temp; // GCC扩展的属性,告诉编译器这个变量可能未用
  1. 有实参的属性:属性名后面带括号传参,提供更多信息
1
[[deprecated("请用new_func()替代")]] void old_func();
  1. 既有命名空间又有实参:上面两种的组合
1
[[gnu::deprecated("use new_func")]] void old_func();

属性机制的核心特点

  1. 不改变代码逻辑:属性只是 “提示”,不是 “指令”—— 比如[[deprecated]]的函数依然能运行,[[nodiscard]]不接收返回值也不会报错,只是给警告;
  2. 编译器识别:注释(//)只有人能看,属性是编译器能理解的 “机器注释”,能触发编译器的特定行为(如警告);
  3. 可选性:属性不是必须写的,只是帮你写出更规范、更少坑的代码;
  4. 版本要求:核心属性(上面 3 个)从 C++11/17 开始支持,主流编译器(GCC/Clang/VS)都能识别。

语法优化

​ 这是 C++17 针对命名空间属性的语法优化,允许将重复写在多个命名空间上的属性(如[[deprecated]]/[[nodiscard]]),通过namespace N [[属性]]的形式一次性标注,替代传统 “每个命名空间定义都加属性” 的繁琐写法—— 本质是简化命名空间属性的声明语法,不改变属性的语义。

​ C++17 起,你可以在属性列表里用using来指定一个默认命名空间,避免重复写:

1
[[using gnu: unused, always_inline]]

等价于:

1
[[gnu::unused, gnu::always_inline]]

__has_include

​ 跨平台项目需要考虑不同平台编译器的实现,使用__has_include可以判断当前环境下是否存在某个头文件。

1
2
3
4
5
6
7
8
9
int main() {
#if __has_include("iostream")
cout << "iostream exist." << endl;
#endif
#if __has_include(<cmath>)
cout << "<cmath> exist." << endl;
#endif
return 0;
}

库相关

charconv

<charconv>是C++17新的标准库头文件,包含了相关类和两个转换函数。

​ 可以完成传统的整数/浮点和字符串互相转换的功能(atoi、itoa、atof、sprintf等),同时支持输出格式控制、整数基底设置并且将整数和浮点类型对字符串的转换整合了起来。

​ 是独立于本地环境、不分配、不抛出的。目的是在常见的高吞吐量环境,例如基于文本的交换( JSON 或 XML )中,允许尽可能快的实现。

charconv解决了传统转换方式的 3 个核心问题:

特性 传统方法(atoi/stoi/sprintf) charconv 库
性能 慢(有额外开销,如内存分配) 极快(纯栈操作,无动态内存分配)
错误处理 不直观(如 stoi 抛异常,atoi 无提示) 返回转换状态,不抛异常,更可控
格式控制 有限(如 sprintf 需手动写格式符) 支持进制、精度等精细控制
安全性 有缓冲区溢出风险(如 sprintf) 明确指定缓冲区范围,无溢出风险

chars_format

chars_format是作为格式控制的类定义在<charconv>头文件中

1
2
3
4
5
6
enum class chars_format {
scientific = /*unspecified*/,
fixed = /*unspecified*/,
hex = /*unspecified*/,
general = fixed | scientific
};
枚举值 含义 示例(以 3.14 为例)
chars_format::fixed 固定小数格式(普通小数形式) “3.14”
chars_format::scientific 科学计数法格式(指数形式) “3.14e0”
chars_format::hex 十六进制浮点数格式(仅适用于二进制浮点数) “0x1.91eb85p+1”
chars_format::general 通用格式(自动选 fixed/scientific,选更短的) “3.14”(短于 3.14e0)

补充:general是默认格式(不指定时自动用这个),会根据数值大小自动选择最简洁的表示方式。

from_chars

底层代码:

1
2
3
4
5
6
7
8
9
10
11
struct from_chars_result {
const char* ptr;
std::errc ec;//error code
};
std::from_chars_result from_chars(const char* first, const char* last, /*see below*/& value, int base = 10);

std::from_chars_result from_chars(const char* first, const char* last, float& value, std::chars_format fmt = std::chars_format::general);

std::from_chars_result from_chars(const char* first, const char* last, double& value, std::chars_format fmt = std::chars_format::general);

std::from_chars_result from_chars(const char* first, const char* last, long double& value, std::chars_format fmt = std::chars_format::general);

相关参数:

  • first, last - 要分析的合法字符范围

  • value - 存储被分析值的输出参数,若分析成功

  • base - 使用的整数基底: 2 与 36 间的值(含上下限)。

  • fmt - 使用的浮点格式, std::chars_format 类型的位掩码

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main() {
const char* str = "6789";
int num;

// from_chars:把str转成int存入num,返回转换结果
auto [ptr, ec] = from_chars(str, str + strlen(str), num);

if (ec == errc{}) { // 转换成功
cout << "转换结果:" << num << endl; // 输出6789
} else if (ec == errc::invalid_argument) {
cout << "错误:字符串不是合法数值" << endl;
} else if (ec == errc::result_out_of_range) {
cout << "错误:数值超出范围" << endl;
}
return 0;
}

to_chars

底层代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct to_chars_result {
char* ptr;
std::errc ec;//error code
};
//整数转字符串
std::to_chars_result to_chars(char* first, char* last, value, int base = 10);

//浮点转字符串
std::to_chars_result to_chars(char* first, char* last, float value,std::chars_format fmt, int precision);

std::to_chars_result to_chars(char* first, char* last, double value, std::chars_format fmt, int precision);

std::to_chars_result to_chars(char* first, char* last, long double value, std::chars_format fmt, int precision);
  • first, last - 要写入的字符范围
  • value - 要转换到其字符串表示的值
  • base - 使用的整数基底: 取值范围[2,36]。
  • fmt - 使用的浮点格式, std::chars_format 类型的位掩码
  • precision - 使用的浮点精度

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
// 准备缓冲区(栈上数组,无动态分配)
array<char, 20> buf;
int num = 12345;

// to_chars:把num转成字符串存入buf,返回转换结果
auto [ptr, ec] = to_chars(buf.data(), buf.data() + buf.size(), num);

if (ec == errc{}) { // 转换成功
*ptr = '\0'; // 手动加结束符(to_chars不自动加)
cout << "转换结果:" << buf.data() << endl; // 输出12345
}
return 0;
}

std::variant

​ C++17中提供了std::variant类型,意为多变的,可变的类型。

​ 有点类似于加强版的union,里面可以存放复合数据类型,且操作元素更为方便。

​ 可以用于表示多种类型的混合体,但同一时间只能用于代表一种类型的实例。

variant提供了index成员函数,该函数返回一个索引,该索引用于表示variant定义对象时模板参数的索引(起始索引为0),同时提供了一个函数holds_alternative<T>(v)用于查询对象v当前存储的值类型是否是T(返回1/0)

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
#include <iostream>
#include <variant>
#include <string>
using namespace std;
int main() {
variant<int, double, string> var;
var = 42;
cout << "var holds int: " << get<int>(var) << endl;
cout << "var holds int: " << get<0>(var) << endl;
cout << "var index: " << var.index() << endl;

var = 3.14;
cout << "var holds double: " << get<double>(var) << endl;
cout << "var holds double: " << get<1>(var) << endl;
cout << "var index: " << var.index() << endl;

var = "Hello, World!";
cout << "var holds string: " << get<string>(var) << endl;
cout << "var holds string: " << get<2>(var) << endl;
cout << "var index: " << var.index() << endl;

cout << holds_alternative<int>(var) << endl; //输出:0
cout << holds_alternative<double>(var) << endl; //输出:0
cout << holds_alternative<string>(var) << endl; //输出:1
return 0;
}

std::optional

​ 在 C 时代以及早期 C++ 时代,语法层面支持的 nullable 类型可以采用指针方式: T* ,如果指针为 NULL (C++11 之后则使用 nullptr ) 就表示无值状态(empty value)。

​ 在编程中,经常遇到这样的情况:可能返回/传递/使用某种类型的对象。也就是说,可以有某个类型的值,也可以没有任何值。因此,需要一种方法来模拟类似指针的语义,在指针中,可以使用nullptr来表示没有值。

​ 处理这个问题的方法是定义一个特定类型的对象,并用一个额外的布尔成员/标志来表示值是否存在。std::optional<>以一种类型安全的方式提供了这样的对象。

注意:每个版本可能对某些特征做了改动。

optional是一个模板类:

1
2
template <class T>
class optional;

​ 它内部有两种状态,要么有值(T类型),要么没有值(std::nullopt)。有点像T*指针,要么指向一个T类型,要么是空指针(nullptr)。

std::optional有以下几种构造方式:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <optional>
using namespace std;
int main() {
optional<int> o1; //什么都不写时默认初始化为nullopt
optional<int> o2 = nullopt; //初始化为无值
optional<int> o3 = 10; //用一个T类型的值来初始化
optional<int> o4 = o3; //用另一个optional来初始化
return 0;
}

​ 查看一个optional对象是否有值,可以直接用if,或者用has_value()

1
2
3
4
5
6
7
optional<int> o1;
if (o1) {
printf("o1 has value\n");
}
if (o1.has_value()) {
printf("o1 has value\n");
}

​ 当一个optional有值时,可以通过用指针的方式(*号和->号)来使用它,或者用.value()/.value_or()拿到它的值:

1
2
3
4
5
6
7
8
9
10
optional<int> o1 = 100;
cout << *o1 << endl;
cout << o1.value() << endl;
cout << o1.value_or(0) <<endl;

optional<string> o2 ="orange";
cout << o2->c_str() << endl;
cout << o2.value().c_str() << endl;
//c_str()是string类型的函数,核心作用是返回指向字符串底层的、以\0(空字符)结尾的const char*类型指针,此处可忽略
cout << o2.value_or("0") <<endl;//有值则返回存储的值,无值则返回传入的默认值;无异常风险,是日常使用的首选。

​ 将一个有值的optional变为无值,用.reset()。该函数会将已存储的T类型对象析构掉

1
2
optional<int> o1 = 500;
o1.reset();

std::any

​ 在C++11中引入的auto自动推导类型变量大大方便了编程,但是auto变量一旦声明,该变量类型不可再改变。

​ C++17中引入了std::any类型,本质是「类型擦除的通用容器」—— 它能存储任意类型的单个值(同一时间仅存一个),且无需提前声明可存储的类型列表(区别于variant),是处理 “完全未知类型值” 的灵活工具。

为什么需要 any?(对比 variant/optional)

  • optional<T>:仅处理 “有 / 无 T 类型值”,类型固定;
  • variant<T1,T2>:处理 “多个已知类型中的一个”,类型列表需提前定义;
  • std::any:处理 “任意类型的值”,无需提前知道类型(比如接收用户输入的未知类型数据)。

std::any的核心价值:完全的类型灵活性,代价是轻微的性能开销(类型擦除和动态内存分配)。

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
#include <any>
#include <iostream>
using namespace std;
int main() {
cout << boolalpha; //将bool值用 "true" 和 "false"显示
any a; //定义一个空的any,即一个空的容器


//有两种方法来判断一个any是否是空的
cout << a.has_value() << endl; // any是空的时,has_value 返回值为 false
cout << a.type().name() << endl; //any类型


//几种创建any的方式
any b = 1; //b 为存了int类型的值的any
auto c = make_any<float>(5.0f); //c为存了float类型的any
any d(6.0); //d为存储了double类型的any

cout << b.has_value() << endl; //true
cout << b.type().name() << endl; //int
cout << c.has_value() << endl; //true
cout << c.type().name() << endl; //float
cout << d.has_value() << endl; //true
cout << d.type().name() << endl; //double


//更改any的值
a = 2; //直接重新赋值
auto e = c.emplace<float>(4.0f); //调用emplace函数,e为新生成的对象引用


//清空any的值
b.reset();
cout << b.has_value() << endl; //false
cout << b.type().name() << endl; //int


//使用any的值
try
{
//any_cast<T>(x)见下文
auto f = any_cast<int>(a); //f为int类型,其值为2
cout << f << endl; //2
}
catch (const bad_any_cast& e)
{
cout << e.what() << endl;
}

try
{
auto g = any_cast<float>(a); //抛出std::bad_any_cat 异常
cout << g << endl; //该语句不会执行
}
catch (const bad_any_cast& e)
{
cout << e.what() << endl; //可能输出Bad any_cast
}
return 0;
}

std::any_cast

std::any_cast是 C++17 为std::any提供的类型转换工具,核心作用是「安全地从std::any对象中提取 / 转换指定类型的值」—— 它是访问std::any内部数据的唯一合法方式,本质是 “类型检查 + 值提取” 的组合操作

  1. 取指针(any_cast<T>(&any_obj))→ 最安全(推荐)

    核心逻辑:传入std::any对象的指针,返回目标类型T的指针;类型不匹配 / 空对象时返回nullptr,无异常风险。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 类型匹配:返回有效指针
if (int* p = any_cast<int>(&a)) {
cout << "提取int值:" << *p << endl; // 输出:100
}

// 2. 类型不匹配:返回nullptr
if (double* p = any_cast<double>(&a)) {
cout << *p << endl;
} else {
cout << "a的类型不是double" << endl; // 输出此句
}

// 3. 空对象:返回nullptr
if (string* p = any_cast<string>(&c)) {
cout << *p << endl;
} else {
cout << "c是空对象,无值可提取" << endl; // 输出此句
}

// 4. 提取复杂类型(如string),取指针避免拷贝
if (string* p = any_cast<string>(&b)) {
*p = "world"; // 直接修改any内部的string值
cout << "修改后的值:" << *p << endl; // 输出:world
}
  • 适用场景:所有场景,尤其是不确定std::any存储类型 / 是否有值时;
  • 优势:无异常、可直接修改内部值(非 const 指针)、避免值拷贝(指针访问)。
  1. 取值(any_cast<T>(any_obj))→ 抛异常(严格校验)

    核心逻辑:传入std::any对象,直接返回目标类型T的值;类型不匹配 / 空对象时抛出std::bad_any_cast异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 类型匹配:返回值(会拷贝)
int val = any_cast<int>(a);
cout << "提取的int值:" << val << endl; // 输出:100

// 2. 类型不匹配:抛异常(需捕获)
try {
double d = any_cast<double>(a);
cout << d << endl;
} catch (const bad_any_cast& e) {
cout << "取值失败:" << e.what() << endl; // 输出异常信息
}

// 3. 空对象:抛异常
try {
string s = any_cast<string>(c);
cout << s << endl;
} catch (const bad_any_cast& e) {
cout << "空对象取值失败:" << e.what() << endl; // 输出此句
}

// 4. 提取引用(避免拷贝,需加&)
string& s_ref = any_cast<string&>(b);
s_ref = "hello any";
cout << "修改后的值:" << any_cast<string>(b) << endl; // 输出:hello any
  • 适用场景:业务逻辑中 “类型不匹配属于异常情况”,需要明确捕获错误;
  • 注意:
    • 直接取值会拷贝数据(对大类型如vector不友好),建议用any_cast<T&>取引用;
    • 必须配合try-catch,否则程序会崩溃。

std::apply

std::apply是 C++17 引入的标准库函数,核心作用是**「把一个可调用对象(函数 /lambda/ 仿函数),应用到一个元组(std::tuple)的各个元素上」**—— 简单说就是 “解包元组,把元组的每个元素作为参数传给函数”,解决了 “元组元素无法直接作为函数参数” 的问题。

​ 也就是将tuple元组解包,并作为函数的传入参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <any>
#include <iostream>
using namespace std;
int add(int a, int b) {
return a + b;
}
int main() {
auto add_lambda = [](auto a, auto b, auto c) { return a + b + c; };
cout << apply(add, pair(2, 3)) << endl; //5
cout << apply(add_lambda, tuple(2, 3, 4)) << endl; //9
return 0;

// 混合类型的元组
tuple<string, int, double> t("张三", 18, 95.5);
// apply + lambda:解包元组元素作为lambda的参数
apply([](const string& name, int age, double score) {
cout << "姓名:" << name << ",年龄:" << age << ",分数:" << score << endl;
}, t);
// 输出:姓名:张三,年龄:18,分数:95.5
}

核心特性:

  1. 类型严格匹配

元组的元素类型 / 数量必须和可调用对象的参数完全匹配,否则编译报错:

1
2
tuple<int, int> t(1,2);
// apply(add, t); // 编译报错!add需要3个int,元组只有2个
  1. 支持所有可调用对象

std::apply的第一个参数可以是:

  • 普通函数 / 函数指针;
  • lambda 表达式;
  • 仿函数(重载operator()的类);
  • std::function包装的函数。

仿函数示例:

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

// 仿函数
struct Multiply {
int operator()(int a, int b) {
return a * b;
}
};

int main() {
tuple<int, int> t(3, 4);
int res = apply(Multiply(), t);
cout << "3*4=" << res << endl; // 输出:3*4=12
return 0;
}
  1. 完美转发参数

    std::apply会完美转发元组的元素(保留左值 / 右值特性),避免不必要的拷贝:

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

// 接收右值引用的函数
void print_vector(vector<int>&& vec) {
for (int num : vec) cout << num << " ";
cout << endl;
}

int main() {
tuple<vector<int>> t({1,2,3});
// apply会完美转发vector的右值特性
apply(print_vector, move(t)); // 输出:1 2 3
return 0;
}

std::make_from_tuple

std::make_from_tuple是 C++17 引入的标准库函数,核心作用是「用一个元组(std::tuple)的元素作为构造参数,创建指定类型的对象」—— 简单说就是 “解包元组,用元组元素给类 / 类型的构造函数传参,直接创建对象”,是std::apply的 “对象构造版”。

为什么需要 make_from_tuple?(核心痛点)

普通情况下,若想通过元组的元素构造对象,需要手动拆包元组、逐个传参,代码繁琐且不通用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <tuple>
using namespace std;

// 自定义类:需要3个参数构造
struct Person {
string name;
int age;
double score;
Person(string n, int a, double s) : name(n), age(a), score(s) {}
};

int main() {
tuple<string, int, double> t("张三", 18, 95.5);
// 手动拆包构造(繁琐,元组元素多了更麻烦)
Person p(get<0>(t), get<1>(t), get<2>(t));
return 0;
}

std::make_from_tuple就是为解决这个问题而生 —— 它会自动解析元组元素,直接传给构造函数,一行代码完成对象创建。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
class ClassTest {
public:
string _name;
size_t _age;
ClassTest(string name, size_t age) : _name(name), _age(age) {
cout << "name: " << _name << ", age: " << _age << endl;
}
};
int main() {
auto param = make_tuple("zhangsan", 19);
make_from_tuple<ClassTest>(move(param));
return 0;
}

std::string_view

​ C++中字符串有两种形式,char*std::stringstring类型封装了char字符串,让我们对字符串的操作方便了很多,但是会有些许性能的损失,而且由char*转为string类型,需要调用string拷贝构造函数,也就是说需要重新申请一片内存,但如果只是对源字符串做只读操作,这样的构造行为显然是不必要的。

​ 在C++17中,增加了std::string_view类型,是 轻量级字符串 “视图” 类型,核心作用是「只读、零拷贝地访问字符串数据」—— 它不持有字符串内存,仅保存指向字符串起始的指针和长度,是替代const std::string&的高效选择(尤其适合频繁传递 / 访问字符串的场景)。

​ 它通过char*字符串构造,但是并不会去申请内存重新创建一份该字符串对象,只是char*字符串的一个视图,优化了不必要的内存操作。相应地,对源字符串只有读权限,没有写权限。

核心语法

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
// 核心类型定义
class string_view {
public:
// 构造函数(常用)
constexpr string_view() noexcept; // 空视图
constexpr string_view(const char* str) noexcept; // 从C风格字符串(自动找\0)
constexpr string_view(const char* str, size_t len) noexcept; // 从char*+长度
constexpr string_view(const std::string& str) noexcept; // 从std::string

// 核心成员函数(只读)
constexpr const char* data() const noexcept; // 返回指向字符串的指针(不保证\0结尾)
constexpr size_t size() const noexcept; // 字符串长度(等价于length())
constexpr size_t length() const noexcept; // 同size()
constexpr bool empty() const noexcept; // 判断是否为空
constexpr char operator[](size_t pos) const; // 下标访问(无越界检查)
constexpr const char& at(size_t pos) const; // 下标访问(有越界检查,抛异常)

// 切片/裁剪(零拷贝)
constexpr string_view substr(size_t pos = 0, size_t count = npos) const;
constexpr void remove_prefix(size_t n) noexcept; // 移除前n个字符
constexpr void remove_suffix(size_t n) noexcept; // 移除后n个字符

// 查找(和std::string一致)
constexpr size_t find(char c, size_t pos = 0) const noexcept;
constexpr size_t find(const string_view& sv, size_t pos = 0) const noexcept;

// 静态常量
static constexpr size_t npos = -1; // 表示“未找到”或“全部”
};

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
void func1(string_view str_v) {
cout << str_v << endl;
return;
}
int main() {
const char* charStr ="hello world";
//string拷贝构造
string str{ charStr };
//string_view类型创建,可指定获取长度
string_view str_v(charStr, strlen(charStr));
cout << "str: " << str << endl;
cout << "str_v: " << str_v << endl;
func1(str_v);
return 0;
}

std::as_const

std::as_const是 C++17 引入的标准库工具函数,核心作用是「将左值对象 / 引用转换为对应的const引用,且不产生拷贝」—— 它是语法糖,替代手动写const_cast或临时const变量,让代码更简洁、语义更清晰。

定义在<utility>头文件中。

核心语法(关键接口 / 函数签名)

1
2
3
4
5
6
7
// 核心模板函数(简化版,实际为 noexcept 且 constexpr)
template <typename T>
constexpr std::add_const_t<T>& as_const(T& t) noexcept;

// 禁用右值重载(避免将临时对象转为const引用,防止悬空)
template <typename T>
void as_const(const T&&) = delete;
  • 模板参数T:输入对象的类型;
  • 参数t:非 const 的左值引用(右值会触发删除的重载,编译报错);
  • 返回值:const T&(对原对象的 const 引用,无拷贝)。

案例:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
int main() {
string str={ "C++ as const test" };
//is_const见下文
cout << is_const<decltype(str)>::value << endl;
const string str_const = as_const(str);
cout << is_const<decltype(str_const)>::value << endl;
return 0;
}

std::is_const

std::is_const是 C++11 引入的类型特性模板,核心作用是「在编译期判断一个类型是否为const修饰的类型」—— 它是编译期类型校验的基础工具,语法上分为 “基础模板版” 和 “简化变量版(C++17+)”,使用时需结合<type_traits>头文件。

核心定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <type_traits> // 必须包含此头文件

// 基础模板(C++11)
template <typename T>
struct is_const : public integral_constant<bool, false> {};

// 特化版本(匹配const类型)
template <typename T>
struct is_const<const T> : public integral_constant<bool, true> {};

// 简化变量模板(C++17及以上,推荐)
template <typename T>
inline constexpr bool is_const_v = is_const<T>::value;

语法特性:

  1. 仅判断 “顶层 const”,不处理 “底层 const”
  • 顶层 const:修饰变量 / 指针本身(如const intint* const)→ is_const返回true
  • 底层 const:修饰指针指向的内容 / 引用的底层类型(如const int*const int&)→ 直接判断返回false,需先移除指针 / 引用再判断。
  1. 引用无 const 属性

C++ 中引用本身不能被const修饰(const int&const是修饰引用的底层类型),因此直接判断引用类型的is_const永远返回false,必须通过std::remove_reference_t移除引用后再判断。

  1. 简化写法(is_const_v)的兼容性
  • is_const_v<T>是 C++17 引入的变量模板,语法更简洁;
  • C++11/C++14 需用is_const<T>::value(功能等价,仅写法繁琐)。
  1. 配合其他类型特性模板

    is_const常与以下模板配合,处理复杂类型的 const 判断:

模板 作用 示例(输入 const int&)
remove_reference_t 移除引用修饰 const int
remove_pointer_t 移除指针修饰 const int(输入 const int*)
remove_const_t 移除 const 修饰 int(输入 const int)
add_const_t 添加 const 修饰 const int(输入 int)

std::filesystem

std::filesystem是 C++17 引入的标准库模块,核心作用是「跨平台、类型安全地操作文件系统(文件 / 目录的创建、遍历、查询、删除等)」—— 它替代了传统的 C 风格文件操作(如fopen/mkdir)和平台相关 API(如 Windows 的FindFirstFile、Linux 的opendir),提供统一、易用的文件系统操作接口。

​ 一定是C++17标准及以上版本。项目属性->C/C++->语言->C++语言标准设置为:ISO C++17 标准(/std:c++17)

1
2
#include<filesystem>
using namespace std::filesystem

常用类:

  • path类:路径处理
  • directory_entry类:文件入口
  • directory_iterator类:获取文件系统目录中文件的迭代器容器
  • file_status类:用于获取和修改文件(或目录)的属性

核心类型定义

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 <filesystem> // 必须包含此头文件
namespace fs = std::filesystem; // 常用别名,简化代码

// 核心类型:表示路径(跨平台兼容)
class path {
public:
// 构造函数(支持字符串/字符串视图)
path() noexcept;
path(const char* s);
path(const std::string& s);
path(std::string_view s);

// 路径拼接(重载/=)
path& operator/=(const path& p);

// 路径分解
std::string filename() const; // 文件名(含后缀)
std::string stem() const; // 文件名(无后缀)
std::string extension() const; // 文件后缀(含.)
path parent_path() const; // 父目录路径

// 路径转换
std::string string() const; // 转为std::string
const char* c_str() const; // 转为C风格字符串
};

// 核心函数(文件/目录操作)
// 1. 路径判断
bool exists(const path& p); // 判断路径是否存在
bool is_directory(const path& p); // 判断是否是目录
bool is_regular_file(const path& p); // 判断是否是普通文件

// 2. 文件/目录操作
bool create_directory(const path& p); // 创建单个目录
bool create_directories(const path& p); // 递归创建目录(如a/b/c)
bool remove(const path& p); // 删除文件/空目录
uintmax_t remove_all(const path& p); // 递归删除目录及所有内容
void rename(const path& p, const path& new_p); // 重命名/移动文件/目录

// 3. 文件信息
file_status status(const path& p); // 获取文件状态
uintmax_t file_size(const path& p); // 获取文件大小(字节)
file_time_type last_write_time(const path& p); // 获取最后修改时间

// 4. 目录遍历
directory_iterator begin(directory_iterator iter); // 目录迭代器(遍历一级)
recursive_directory_iterator begin(recursive_directory_iterator iter); // 递归遍历

普通文件定义

​ 普通文件是文件系统中最基础的文件类型,指「存储实际数据(如文本、二进制、图片等)的文件,而非目录、符号链接、设备文件等特殊文件」——std::filesystem::is_regular_file()就是专门用来判断路径是否指向这类文件的核心函数。

  1. 本质特征

普通文件的核心是 “存储数据的实体”,具备以下特点:

  • 有具体的文件大小(可通过fs::file_size()获取);
  • 可通过fstream/ifstream/ofstream读写内容;
  • 不是目录、符号链接、管道、设备文件、套接字等特殊文件;
  • 常见类型:.txt/.cpp/.exe/.jpg/.zip等都是普通文件。
  1. 普通文件 vs 其他文件类型(对比表)

std::filesystem能识别的常见文件类型,普通文件是其中最核心的一类:

文件类型 判断函数 特征 示例
普通文件 is_regular_file() 存储数据,有文件大小,可读写内容 test.txt/a.exe
目录 is_directory() 用于存放文件 / 子目录,无 “文件大小” 概念 C:\Users//home
符号链接 is_symlink() 指向其他文件 / 目录的快捷方式 Linux 下的ln -s链接
字符设备文件 is_character_file() 与硬件设备交互的特殊文件(Linux) /dev/tty
块设备文件 is_block_file() 存储设备相关文件(Linux) /dev/sda1
管道文件 is_fifo() 进程间通信的管道文件 Linux 下的mkfifo文件
套接字文件 is_socket() 网络 / 进程通信的套接字文件(Linux) /tmp/socket.sock

path类

常用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::filesystem::exists(const path& pval)
//用于判断path是否存在

std::filesystem::copy(const path& from, const path& to)
//目录复制

std::filesystem::absolute(const path& pval, const path& base = current_path())
//获取相对于base的绝对路径

std::filesystem::create_directory(const path& pval)
//当目录不存在时创建目录

std::filesystem::create_directories(const path& pval)
//形如/a/b/c这样的,如果都不存在,创建目录结构

std::filesystem::file_size(const path& pval)
//返回目录的大小

常用语法模版

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
// 1. 路径定义与拼接
fs::path dir_path = "test_dir";
fs::path file_path = dir_path / "test.txt"; // 跨平台拼接(Windows用\,Linux用/)

// 2. 目录操作
fs::create_directories(dir_path); // 递归创建目录
if (fs::is_directory(dir_path)) { /* 目录存在逻辑 */ }

// 3. 文件操作
if (fs::exists(file_path) && fs::is_regular_file(file_path)) {
uintmax_t size = fs::file_size(file_path); // 获取文件大小
}

// 4. 目录遍历(一级)
for (const auto& entry : fs::directory_iterator(dir_path)) {
cout << entry.path() << endl; // 输出目录下的文件/子目录
}

// 5. 递归遍历目录
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
if (fs::is_regular_file(entry.path())) {
cout << "文件:" << entry.path() << endl;
}
}

// 6. 删除文件/目录
fs::remove(file_path); // 删除单个文件
fs::remove_all(dir_path); // 递归删除目录

案例

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
#include <ctime>
#include <iostream>
#include <filesystem>
using namespace std;
int main() {
namespace fs = std::filesystem;
auto testdir = fs::path("./testdir");
if (!fs::exists(testdir))//文件是否存在
{
cout << "file or directory is not exists!" << endl;
}
// none (默认)跳过符号链接,权限拒绝是错误。
// follow_directory_symlink 跟随而非跳过符号链接。
// skip_permission_denied 跳过若不跳过就会产生权限拒绝错误的目录。
fs::directory_options opt(fs::directory_options::none);
fs::directory_entry dir(testdir);
// 遍历当前目录
std::cout << "show:\t" << dir.path().filename() << endl;
for(fs::directory_entry const& entry : fs::directory_iterator(testdir, opt))
{
if (entry.is_regular_file())
{
cout << entry.path().filename() << "\t size: " << entry.file_size() << endl;
}
else if (entry.is_directory())
{
cout << entry.path().filename() << "\t dir" << endl;
}
}
cout << endl;
// 递归遍历所有的文件
cout << "show all:\t" << dir.path().filename() << endl;
for (fs::directory_entry const& entry : fs::recursive_directory_iterator(testdir, opt))
{
if (entry.is_regular_file())
{
cout << entry.path().filename() << "\t size: " << entry.file_size() << "\t parent: " << entry.path().parent_path() << endl;
}
else if (entry.is_directory())
{
cout << entry.path().filename()<< "\t dir" << endl;
}
}
return 0;
}