文章目录
- 一、语言语法
- 1. 结构化绑定
- 2. if / switch 语句内初始化变量
- 3. 折叠表达式 Fold‑Expression
- 4. inline 内联变量
- 5. constexpr if 编译期分支
- 6. 类模板参数推导
- 7. 嵌套命名空间简写
- 8. 属性标记 [[nodiscard]]
- 二、标准库高频更新
- 1. std::optional
- 2. std::variant
- 3. std::any
- 4. std::filesystem
- 5. std::string_view
- 6. std::as_const
- 7. std::clamp
一、语言语法
1. 结构化绑定
C++11/14痛点:获取 pair、tuple、结构体成员,必须用.first/.second/get<>,代码啰嗦,可读性差。
新能力:一次性解包元组、pair、结构体、数组元素。(刷算法题又爽了)
#include<map>#include<iostream>intmain(){std::map<int,std::string>mp;mp[1]="hello";// 遍历map神器for(auto&[key,val]:mp){std::cout<<key<<" "<<val;}return0;}场景:遍历map,返回多值函数,解包结构体。
auto [key,val]:值拷贝,修改不会影响原容器;auto& [key,val]:引用,可以修改原值;const auto& [key,val]:只读引用,禁止修改;
结构化绑定出来的变量,不能直接放进 lambda 的捕获列表[],C++17 标准禁止。
结构化绑定产生的是一种别名,并不是一个普通实体变量,lambda 无法捕获它。
#include<map>#include<iostream>#include<functional>intmain(){std::map<int,std::string>mp{{1,"hello"}};for(auto&[key,val]:mp){// ❌ 非法!不能捕获 key / valautolambda=[key,val](){std::cout<<key<<" "<<val;};lambda();}return0;}方案1:先拷贝一份普通变量,再捕获(最常用)
#include<map>#include<iostream>#include<functional>intmain(){std::map<int,std::string>mp{{1,"hello"}};for(auto&[key,val]:mp){// 先拷贝出来intk=key;std::string v=val;// ✅ 捕获普通变量没问题autolambda=[k,v](){std::cout<<k<<" "<<v;};lambda();}return0;}方案2:捕获外层整个对象,lambda 内部再解包
如果你是想异步执行(线程任务),可以捕获整个 pair/map‑item,不要捕获绑定别名:
#include<map>#include<iostream>#include<functional>#include<thread>intmain(){std::map<int,std::string>mp{{1,"hello"}};for(auto&item:mp){// 捕获整个元素autolambda=[item](){auto[k,v]=item;// lambda 内部再结构化绑定std::cout<<k<<" "<<v;};std::threadt(lambda);t.join();}return0;}2. if / switch 语句内初始化变量
旧写法:变量定义在 if 外面,扩大作用域,容易污染上下文。
// C++17之前autores=get_result();if(res.is_ok()){}C++17:
if(autores=get_result();res.is_ok()){// res 仅在 if‑else 范围内生效}switch(autoval=get_id();val){case1:break;}3. 折叠表达式 Fold‑Expression
解决可变参数包Args...难以遍历运算的痛点。
#include<iostream>template<typename...Args>autosum(Args...args){return(args+...);}intmain(){std::cout<<sum(1,2,3,4)<<std::endl;return0;}输出结果:10。
(pack op ...)右折叠(... op pack)左折叠
4. inline 内联变量
痛点 C++14:头文件中定义全局变量/静态成员变量,多文件包含就触发重定义链接错误,只能放源文件。
// 头文件直接写,多文件包含不会报重定义inlineconststd::string g_name="demo";structConfig{inlinestaticintcount=0;};工程最实用特性之一!头文件常量、类静态成员变量首选方案。
5. constexpr if 编译期分支
编译期判断,不满足的分支直接丢弃,不会参与编译(模板场景下)。
template<typenameT>voidfunc(T t){ifconstexpr(std::is_integral_v<T>){// 整数类型执行这段}else{// 其他类型}}场景:模板特化简化、类型分支判断、可变参数处理;替代笨重的模板重载。
- 非模板场景:两个分支代码语法都必须合法;
- 条件必须是编译期可求出的常量表达式。
6. 类模板参数推导
不用显式写出模板参数,编译器自动推导
// C++14std::pair<int,std::string>p{1,"abc"};// C++17,自动推导类型std::pair p{1,"abc"};std::vector vec{1,2,3};只适用于类模板,不能推导函数模板;复杂自定义类有时需要手动提供推导指引。
7. 嵌套命名空间简写
// C++14 冗长写法namespaceA{namespaceB{namespaceC{}}}// C++17 简写namespaceA::B::C{}头文件高频使用,减少缩进。
8. 属性标记 [[nodiscard]]
标记函数返回值不允许忽略,如果调用方丢弃返回值编译器给出警告,适合错误码、optional 返回值。
[[nodiscard]]intopen_file();工程规范、错误防御高频使用。
二、标准库高频更新
1. std::optional
头文件:<optional>
std::optional<T>是一个包装容器,内部存储一块T的内存 + 一个布尔标记,标记当前是否有效值。
它专门用来解决函数返回值有可能失败、没有结果的问题。
在C++17之前我们只能用这些丑陋方案标记失败:
- 返回
-1、INT_MAX等魔法值;缺点:返回值本身就可以取到这个数字,歧义; - 返回指针,失败返回
nullptr;缺点:要管理堆内存、容易内存泄漏;
optional 本质:值语义对象,可拷贝、可移动,不需要堆内存。
#include<optional>#include<iostream>// 查找id对应的分数,查找失败则无结果std::optional<int>query_score(intid){if(id==100){return95;// 有值}returnstd::nullopt;// 代表空,没有结果}intmain(){autores=query_score(99);// 方式1:判断是否有值if(res.has_value()){std::cout<<res.value()<<"\n";}// 方式2:bool隐式转换简写判断if(res){std::cout<<*res<<"\n";}// 方式3:取不到值时给默认值,不会抛异常intscore=res.value_or(60);return0;}| API | 作用 |
|---|---|
.has_value() | bool,判断是否包含有效值 |
.value() | 获取值;空的时候调用直接抛异常std::bad_optional_access |
*opt | 解引用获取值;空的时候行为是未定义UB,不会抛异常! |
.value_or(default) | 有值返回原值,无值返回默认值,安全,无异常 |
.reset() | 清空optional,变成空状态 |
使用场景:
- 查询类接口(数据库查找、配置查找)查找可能失败;
- 函数参数可选(某些字段可以不存在);
- 解析报文,某个字段可能缺失;
注意点:
- 不要混用
.value()和*:空的时候.value()抛异常,*直接UB,隐患极大 - 不要返回
std::optional<std::string&>,optional不能存引用,只能存值对象; - 安全优先,拿不到值优先使用
value_or(); - 返回optional的函数,建议加上
[[nodiscard]],防止调用方忽略失败状态
2. std::variant
头文件:<variant>
std::variant<T1,T2,T3...>是类型安全的联合体,同一时刻,variant里面最多保存其中一种类型的值。
底层是一块足够容纳最大类型的缓冲区 + 一个下标标记,记录当前存放的是哪一种类型;栈上分配,无堆内存开销。
C语言原生union的致命缺陷:
- 没有标记记录当前存的是什么类型;、
- 不能存放带非平凡构造析构的类型,例如
std::string;
#include<variant>#include<iostream>#include<string>voidprint_value(conststd::variant<int,double,std::string>&v){// 判断当前存储的类型if(std::holds_alternative<int>(v)){std::cout<<"int: "<<std::get<int>(v)<<"\n";}elseif(std::holds_alternative<double>(v)){std::cout<<"double: "<<std::get<double>(v)<<"\n";}elseif(std::holds_alternative<std::string>(v)){std::cout<<"string: "<<std::get<std::string>(v)<<"\n";}}intmain(){std::variant<int,double,std::string>data;data=10;print_value(data);data=3.14;print_value(data);data="hello variant";print_value(data);return0;}| API | 作用 |
|---|---|
std::holds_alternative<T>(var) | 判断variant当前是不是T类型 |
std::get<T>(var) | 取出T类型的值;类型不匹配抛std::bad_variant_access |
std::get_if<T>(&var) | 安全版本。匹配成功返回指针,失败返回nullptr,不会抛异常 |
std::visit(fun, var) | 访问器,自动匹配当前类型执行回调,最优雅的遍历方式 |
使用场景:
- 消息协议、报文,一个字段可能是多种不同类型;
- 状态机的数据载体;
- 解析配置,字段类型不固定,但是类型集合有限;
注意点:
- variant不能存放引用
std::variant<int&>非法; - variant里面不能放重复类型,
variant<int,int>编译报错; std::get<T>类型不对直接抛异常;不想抛异常优先用std::get_if;- 存放的所有类型必须能被构造;禁止放入没有默认构造函数的类型;
- 如果需要表示variant里面什么都没有,加上
std::monostate:std::variant<std::monostate, int, std::string>
3. std::any
头文件:<any>
std::any是无约束的类型擦除容器,可以存放任意一种单值类型,不需要提前声明类型列表。
内部实现:堆上分配内存存储对象,运行时记录类型信息。
和
variant的本质区别:
- variant:类型集合编译期就要全部写死;
栈内存;几乎无运行时开销;- any:类型可以运行时任意变化;
堆分配;有运行时开销,类型安全检查;
#include<any>#include<iostream>#include<string>intmain(){std::any container;container=100;// 取出值,类型错误抛 std::bad_any_castintnum=std::any_cast<int>(container);std::cout<<num<<"\n";container=std::string("hello any");std::string str=std::any_cast<std::string>(container);std::cout<<str<<"\n";// 安全读取,不抛异常if(int*p=std::any_cast<int>(&container)){std::cout<<*p<<"\n";}// 判断是否为空if(!container.has_value()){}return0;}| API | 作用 |
|---|---|
.has_value() | 是否存有对象 |
std::any_cast<T>(any_obj) | 取出值;类型不匹配抛异常 |
std::any_cast<T>(&any_obj) | 指针版本,失败返回nullptr,安全无异常 |
使用场景:
- 通用事件总线,事件携带的数据类型完全无法提前预知
- 脚本、反射框架,通用缓存
对比选型指南:
- 类型集合有限 → std::variant
- 有可能为空 → std::optional
- 类型完全未知、任意类型 → std::any
注意点:
- any有堆内存开销,频繁创建销毁性能较差;高性能、实时循环里面尽量少用;
- any_cast类型不对会抛异常;优先用指针版
std::any_cast<T>(&x)做安全判断; - any不能存放引用;
4. std::filesystem
头文件:<filesystem>
- C++17第一次把跨平台文件系统操作纳入标准库。在此之前Windows需要调用Win32 API,Linux调用posix,两套代码,无法跨平台;
std::filesystem::path是路径对象,自动处理不同操作系统路径分隔符(windows\,linux/);
#include<filesystem>#include<iostream>namespacefs=std::filesystem;intmain(){fs::path log_dir="./log";// 判断目录是否存在if(!fs::exists(log_dir)){// 创建目录fs::create_directories(log_dir);}fs::path cfg_file=log_dir/"config.json";// /运算符拼接路径,跨平台!if(fs::exists(cfg_file)){// 获取文件大小,字节autosize=fs::file_size(cfg_file);std::cout<<"file size:"<<size<<"\n";}// 遍历文件夹下所有文件for(auto&entry:fs::directory_iterator(log_dir)){if(entry.is_regular_file()){std::cout<<entry.path()<<"\n";}}return0;}fs::exists(path)判断文件/目录是否存在fs::create_directories(path)递归创建多级目录(推荐);create_directory只能创建单级fs::file_size(path)获取文件字节大小fs::remove(path)删除文件fs::rename(old,new)文件重命名fs::directory_iterator单层遍历文件夹fs::recursive_directory_iterator递归遍历所有子文件夹
注意:
- GCC8~9版本需要手动链接库:编译加
-lstdc++fs;GCC10以后不需要; - filesystem的接口大部分会抛异常;如果你不想抛异常,所有函数都有带
std::error_code重载版本;
std::error_code ec;fs::create_directories(log_dir,ec);if(ec){// 创建失败}- 路径对象
fs::path和std::string可以互相转换;不要自己手动拼接字符串路径,用path1 / path2;
5. std::string_view
头文件:<string_view>
std::string_view:字符串只读视图;- 不拥有、不拷贝、不存储字符串内存;
- 内部只有两个成员:
const char*指针 + 长度; - 只是借用一段已经存在的字符缓冲区,零拷贝、性能极高;
C++17之前函数接收字符串参数只能写const std::string& s,如果我们传入一个char*字符串字面量,编译器会隐式构造临时std::string,发生内存拷贝,带来开销。string_view完美规避拷贝开销。
#include<string_view>#include<string>#include<iostream>// 只读接收字符串,零拷贝开销voidprint(std::string_view s){std::cout<<s<<"\n";// s[0] = 'a'; // ❌只读,不能修改}intmain(){std::string str{"hello world"};print(str);// 字符串字面量,无拷贝print("hello literal");// 截取子串,不会生成新string,零开销!std::string_viewsub(str.data(),5);print(sub);return0;}.data()返回底层const char*指针.size()/.length()获取字符长度.substr(pos,len)截取子视图(零拷贝)
选型建议:
- 函数入参,只读字符串,优先
std::string_view; - 绝对不要将string_view作为函数返回值!极易悬垂引用。
高危坑点:
string_view生命周期不能超过原始字符串缓冲区。原始字符串销毁后,string_view变成野指针,悬垂引用。
// ❌危险反面例子std::string_viewbad_func(){std::string temp="abc";returntemp;// temp函数结束销毁,返回的视图悬空!}6. std::as_const
头文件:<utility>
std::as_const(x)返回变量x的const左值引用,不会产生拷贝;- 作用:把一个非const对象,临时当作只读对象使用。
#include<utility>#include<vector>#include<iostream>intmain(){std::vector<int>vec{1,2,3};auto&const_ref=std::as_const(vec);// const_ref.push_back(5); // 编译报错,只读,无法修改// 只读范围for循环for(autov:std::as_const(vec)){std::cout<<v<<"\n";}return0;}使用场景:
- 遍历容器,强制只读,防止不小心修改原数据
- lambda捕获时生成const副本
注意点:
- as_const只生成引用,不会拷贝对象;无性能开销
- 不能用于临时对象
7. std::clamp
头文件:<algorithm>
template<classT>constexprTclamp(constT&value,constT&low,constT&high);数值截断函数,把一个值限制在[low, high]的闭区间内。
C++17之前,你必须手动写max、min嵌套:
intval=std::max(low,std::min(high,val));clamp一步替代:
#include<algorithm>#include<iostream>intmain(){inta=std::clamp(120,0,100);//100intb=std::clamp(-10,0,100);//0intc=std::clamp(50,0,100);//50std::cout<<a<<" "<<b<<" "<<c<<"\n";return0;}传入参数low 必须 <= high;如果low>high,属于未定义行为。