语言特性 折叠表达式 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 );
参数包累加案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> using namespace std;template <typename ... Args>auto sum (Args... args) { return (... + args); } int main () { cout << sum (1 , 2 , 3 , 4 ) << endl; cout << sum (1.5 , 2.5 , 3 ) << endl; return 0 ; }
对比 C++11 的递归写法(需要递归终止函数):
1 2 3 4 5 6 7 int sum () { return 0 ; } template <typename T, typename ... Args>T sum (T first, Args... rest) { return first + sum (rest...); }
常用场景
通用打印函数(最常用)
用<<运算符折叠,一行实现任意参数的打印:
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 << ... << args) << endl; } int main () { print (10 , "hello" , 3.14 , 'A' ); return 0 ; }
参数包逻辑判断(&&/||)
判断所有参数是否满足条件(比如全为正数):
1 2 3 4 5 6 7 8 9 10 11 template <typename ... Args>bool all_positive (Args... args) { return (... && (args > 0 )); } int main () { cout << boolalpha; cout << all_positive (1 , 2 , 3 ) << endl; cout << all_positive (1 , -2 , 3 ) << endl; return 0 ; }
带初始值的二元折叠
比如计算参数包的乘积,初始值为 1(避免空参数包报错):
1 2 3 4 5 6 7 8 9 10 11 template <typename ... Args>auto product (Args... args) { return (1 * ... * args); } int main () { cout << product (2 , 3 , 4 ) << endl; cout << product () << endl; 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 }; 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 >(); return 0 ; }
编译期constexpr if语句 constexpr if是 C++17 引入的编译期分支选择工具 ,核心是 “编译器在编译时就决定保留哪个分支、丢弃哪个分支”,而非运行时判断。
核心定义 & 本质
案例:
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 constexpr (ok == true ) { cout << "ok" << endl; } else { cout << "not ok" << endl; } } int main () { func2 <true >(); func2 <false >(); 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 inline int value = 100 ;class AAA {inline static int value2 = 200 ;};
为什么需要 inline 变量?(解决的核心问题)
C++ 中,全局变量如果定义在头文件中,当多个.cpp文件包含该头文件时,会触发 “重复定义” 编译错误(ODR 规则:一个变量只能有一个定义)。
问题示例(无 inline 的坑):
1 2 3 4 5 6 7 8 int global_num = 10 ; #include "header.h" #include "header.h"
C++17 前的解决方法是:头文件中声明extern,在一个.cpp文件中定义,例如类的静态成员变量需要 “类内声明、类外定义” —— 但步骤繁琐;inline变量则能直接在头文件中定义,且多文件包含不报错。
inline 变量的核心特性
唯一性 :整个程序中,inline变量只有一个实例(即使多个文件包含定义),所有文件操作的是同一个变量;
链接属性 :inline变量默认是外部链接(全局可见),若想限制在当前文件,需加static(static inline int num = 10;);
初始化要求 :inline变量必须在定义时初始化(如inline int num = 10;,不能只写inline int num;);
和 inline 函数的类比:
inline函数:允许多文件定义,编译器只保留一个实例;
inline变量:逻辑完全一致,只是对象从 “函数” 变成 “变量”。
结构化绑定 std::tuple std::tuple是 C++11 引入的「多元组」容器,能把任意数量、任意类型的元素打包成一个单一对象 —— 可以理解为 “增强版的std::pair”(pair只能存 2 个元素,tuple支持任意个),是处理多类型、多值聚合的核心工具。
tuple 的核心特点
异构性 :元素类型可以完全不同(如同时存int、string、double);
固定大小 :创建后元素数量不可变(和数组类似);
值语义 :元素直接存储在tuple内部,而非引用(除非显式存储引用类型);
可嵌套 :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; 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 unordered_map<string, int > stu1{ {"zhangsan" , 18 }, {"wangwu" , 19 } }; auto iter = stu1.f ind("wangwu" );if (iter != stu1. end ()) { cout << iter->second << endl; } if (auto iter = stu1.f ind("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 namespace A { namespace B { namespace C { void func1 () {} } } } namespace A::B::C { void func1 () {} }
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 开始引入的、给代码(变量 / 函数 / 类 / 命名空间等)贴 “标签” 的工具—— 这些标签不会改变代码的逻辑功能,而是告诉编译器 / 开发者 “这个代码有什么特性、需要怎么处理” ,比如 “这个函数已废弃别用了”“这个返回值必须接收”,是一种 “代码注释的升级版”(注释只给人看,属性编译器能识别)
常用的属性主要有:
[[deprecated]]:标记 “废弃 / 过时”,提醒别用了
[[nodiscard]]:标记 “返回值必须接收”,避免浪费。
当用于描述函数的返回值时,如果调用函数的地方没有获取返回值时,编译器给出警告
[[maybe_unused]]:标记 “可能未使用”,消除无用警告
常用属性形式:
简单属性 :只有名字,不带参数
1 [[noreturn]] void exit () ;
有命名空间的属性 :用命名空间::属性名,避免名字冲突
1 [[gnu::unused]] int temp;
有实参的属性 :属性名后面带括号传参,提供更多信息
1 [[deprecated ("请用new_func()替代" )]] void old_func () ;
既有命名空间又有实参 :上面两种的组合
1 [[gnu::deprecated ("use new_func" )]] void old_func () ;
属性机制的核心特点
不改变代码逻辑 :属性只是 “提示”,不是 “指令”—— 比如[[deprecated]]的函数依然能运行,[[nodiscard]]不接收返回值也不会报错,只是给警告;
编译器识别 :注释(//)只有人能看,属性是编译器能理解的 “机器注释”,能触发编译器的特定行为(如警告);
可选性 :属性不是必须写的,只是帮你写出更规范、更少坑的代码;
版本要求 :核心属性(上面 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是作为格式控制的类定义在<charconv>头文件中
1 2 3 4 5 6 enum class chars_format { scientific = , fixed = , hex = , 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; }; std::from_chars_result from_chars (const char * first, const char * last, & 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; auto [ptr, ec] = from_chars (str, str + strlen (str), num); if (ec == errc{}) { cout << "转换结果:" << num << endl; } 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; }; 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 ; auto [ptr, ec] = to_chars (buf.data (), buf.data () + buf.size (), num); if (ec == errc{}) { *ptr = '\0' ; cout << "转换结果:" << buf.data () << endl; } 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; cout << holds_alternative <double >(var) << endl; cout << holds_alternative <string>(var) << endl; 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; optional<int > o2 = nullopt ; optional<int > o3 = 10 ; optional<int > o4 = o3; 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; 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; any a; cout << a.has_value () << endl; cout << a.type ().name () << endl; any b = 1 ; auto c = make_any <float >(5.0f ); any d (6.0 ) ; cout << b.has_value () << endl; cout << b.type ().name () << endl; cout << c.has_value () << endl; cout << c.type ().name () << endl; cout << d.has_value () << endl; cout << d.type ().name () << endl; a = 2 ; auto e = c.emplace <float >(4.0f ); b.reset (); cout << b.has_value () << endl; cout << b.type ().name () << endl; try { auto f = any_cast <int >(a); cout << f << endl; } catch (const bad_any_cast& e) { cout << e.what () << endl; } try { auto g = any_cast <float >(a); cout << g << endl; } catch (const bad_any_cast& e) { cout << e.what () << endl; } return 0 ; }
std::any_cast std::any_cast是 C++17 为std::any提供的类型转换工具,核心作用是「安全地从std::any对象中提取 / 转换指定类型的值」—— 它是访问std::any内部数据的唯一合法方式 ,本质是 “类型检查 + 值提取” 的组合操作
取指针(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 if (int * p = any_cast <int >(&a)) { cout << "提取int值:" << *p << endl; } if (double * p = any_cast <double >(&a)) { cout << *p << endl; } else { cout << "a的类型不是double" << endl; } if (string* p = any_cast <string>(&c)) { cout << *p << endl; } else { cout << "c是空对象,无值可提取" << endl; } if (string* p = any_cast <string>(&b)) { *p = "world" ; cout << "修改后的值:" << *p << endl; }
适用场景 :所有场景,尤其是不确定std::any存储类型 / 是否有值时;
优势 :无异常、可直接修改内部值(非 const 指针)、避免值拷贝(指针访问)。
取值(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 int val = any_cast <int >(a);cout << "提取的int值:" << val << endl; try { double d = any_cast <double >(a); cout << d << endl; } catch (const bad_any_cast& e) { cout << "取值失败:" << e.what () << endl; } try { string s = any_cast <string>(c); cout << s << endl; } catch (const bad_any_cast& e) { cout << "空对象取值失败:" << e.what () << endl; } string& s_ref = any_cast <string&>(b); s_ref = "hello any" ; cout << "修改后的值:" << any_cast <string>(b) << endl;
适用场景 :业务逻辑中 “类型不匹配属于异常情况”,需要明确捕获错误;
注意:
直接取值会拷贝数据(对大类型如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; cout << apply (add_lambda, tuple (2 , 3 , 4 )) << endl; return 0 ; tuple<string, int , double > t ("张三" , 18 , 95.5 ) ; apply ([](const string& name, int age, double score) { cout << "姓名:" << name << ",年龄:" << age << ",分数:" << score << endl; }, t); }
核心特性:
类型严格匹配
元组的元素类型 / 数量必须和可调用对象的参数完全匹配,否则编译报错:
1 2 tuple<int , int > t (1 ,2 ) ;
支持所有可调用对象
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; return 0 ; }
完美转发参数
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 (print_vector, move (t)); 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;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::string,string类型封装了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 ; constexpr string_view (const char * str, size_t len) noexcept ; constexpr string_view (const std::string& str) noexcept ; constexpr const char * data () const noexcept ; constexpr size_t size () const noexcept ; constexpr size_t length () const noexcept ; 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 ; constexpr void remove_suffix (size_t n) noexcept ; 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 str{ charStr }; 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 template <typename T>constexpr std::add_const_t <T>& as_const (T& t) noexcept ;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" }; 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> template <typename T>struct is_const : public integral_constant<bool , false > {};template <typename T>struct is_const <const T> : public integral_constant<bool , true > {};template <typename T>inline constexpr bool is_const_v = is_const<T>::value;
语法特性:
仅判断 “顶层 const”,不处理 “底层 const”
顶层 const :修饰变量 / 指针本身(如const int、int* const)→ is_const返回true;
底层 const :修饰指针指向的内容 / 引用的底层类型(如const int*、const int&)→ 直接判断返回false,需先移除指针 / 引用再判断。
引用无 const 属性
C++ 中引用本身不能被const修饰(const int&的const是修饰引用的底层类型 ),因此直接判断引用类型的is_const永远返回false,必须通过std::remove_reference_t移除引用后再判断。
简化写法(is_const_v)的兼容性
is_const_v<T>是 C++17 引入的变量模板,语法更简洁;
C++11/C++14 需用is_const<T>::value(功能等价,仅写法繁琐)。
配合其他类型特性模板
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 ; const char * c_str () const ; }; bool exists (const path& p) ; bool is_directory (const path& p) ; bool is_regular_file (const path& p) ; bool create_directory (const path& p) ; bool create_directories (const path& p) ; bool remove (const path& p) ; uintmax_t remove_all (const path& p) ; void rename (const path& p, const path& new_p) ; file_status status (const path& p) ; uintmax_t file_size (const path& p) ; file_time_type last_write_time (const path& p) ; directory_iterator begin (directory_iterator iter) ; recursive_directory_iterator begin (recursive_directory_iterator iter) ;
普通文件定义 普通文件是文件系统中最基础的文件类型,指「存储实际数据(如文本、二进制、图片等)的文件,而非目录、符号链接、设备文件等特殊文件」——std::filesystem::is_regular_file()就是专门用来判断路径是否指向这类文件的核心函数。
本质特征
普通文件的核心是 “存储数据的实体”,具备以下特点:
有具体的文件大小(可通过fs::file_size()获取);
可通过fstream/ifstream/ofstream读写内容;
不是目录、符号链接、管道、设备文件、套接字等特殊文件;
常见类型:.txt/.cpp/.exe/.jpg/.zip等都是普通文件。
普通文件 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) std::filesystem::copy (const path& from, const path& to) std::filesystem::absolute (const path& pval, const path& base = current_path ()) std::filesystem::create_directory (const path& pval) std::filesystem::create_directories (const path& pval) 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 fs::path dir_path = "test_dir" ; fs::path file_path = dir_path / "test.txt" ; fs::create_directories (dir_path); if (fs::is_directory (dir_path)) { }if (fs::exists (file_path) && fs::is_regular_file (file_path)) { uintmax_t size = fs::file_size (file_path); } for (const auto & entry : fs::directory_iterator (dir_path)) { cout << entry.path () << endl; } for (const auto & entry : fs::recursive_directory_iterator (dir_path)) { if (fs::is_regular_file (entry.path ())) { cout << "文件:" << entry.path () << endl; } } 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; } 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 ; }