ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++17这波又带来了好东西

2026/8/30 4:59:55 拓冰建站 浏览量
C++17这波又带来了好东西 文章目录一、语言语法1. 结构化绑定2. if / switch 语句内初始化变量3. 折叠表达式 Fold‑Expression4. inline 内联变量5. constexpr if 编译期分支6. 类模板参数推导7. 嵌套命名空间简写8. 属性标记 [[nodiscard]]二、标准库高频更新1. std::optional2. std::variant3. std::any4. std::filesystem5. std::string_view6. std::as_const7. std::clamp一、语言语法1. 结构化绑定C11/14痛点获取 pair、tuple、结构体成员必须用.first/.second/get代码啰嗦可读性差。新能力一次性解包元组、pair、结构体、数组元素。刷算法题又爽了#includemap#includeiostreamintmain(){std::mapint,std::stringmp;mp[1]hello;// 遍历map神器for(auto[key,val]:mp){std::coutkey val;}return0;}场景遍历map返回多值函数解包结构体。auto [key,val]值拷贝修改不会影响原容器auto [key,val]引用可以修改原值const auto [key,val]只读引用禁止修改结构化绑定出来的变量不能直接放进 lambda 的捕获列表[]C17 标准禁止。结构化绑定产生的是一种别名并不是一个普通实体变量lambda 无法捕获它。#includemap#includeiostream#includefunctionalintmain(){std::mapint,std::stringmp{{1,hello}};for(auto[key,val]:mp){// ❌ 非法不能捕获 key / valautolambda[key,val](){std::coutkey val;};lambda();}return0;}方案1先拷贝一份普通变量再捕获最常用#includemap#includeiostream#includefunctionalintmain(){std::mapint,std::stringmp{{1,hello}};for(auto[key,val]:mp){// 先拷贝出来intkkey;std::string vval;// ✅ 捕获普通变量没问题autolambda[k,v](){std::coutk v;};lambda();}return0;}方案2捕获外层整个对象lambda 内部再解包如果你是想异步执行线程任务可以捕获整个 pair/map‑item不要捕获绑定别名#includemap#includeiostream#includefunctional#includethreadintmain(){std::mapint,std::stringmp{{1,hello}};for(autoitem:mp){// 捕获整个元素autolambda[item](){auto[k,v]item;// lambda 内部再结构化绑定std::coutk v;};std::threadt(lambda);t.join();}return0;}2. if / switch 语句内初始化变量旧写法变量定义在 if 外面扩大作用域容易污染上下文。// C17之前autoresget_result();if(res.is_ok()){}C17if(autoresget_result();res.is_ok()){// res 仅在 if‑else 范围内生效}switch(autovalget_id();val){case1:break;}3. 折叠表达式 Fold‑Expression解决可变参数包Args...难以遍历运算的痛点。#includeiostreamtemplatetypename...Argsautosum(Args...args){return(args...);}intmain(){std::coutsum(1,2,3,4)std::endl;return0;}输出结果10。(pack op ...)右折叠(... op pack)左折叠4. inline 内联变量痛点 C14头文件中定义全局变量/静态成员变量多文件包含就触发重定义链接错误只能放源文件。// 头文件直接写多文件包含不会报重定义inlineconststd::string g_namedemo;structConfig{inlinestaticintcount0;};工程最实用特性之一头文件常量、类静态成员变量首选方案。5. constexpr if 编译期分支编译期判断不满足的分支直接丢弃不会参与编译模板场景下。templatetypenameTvoidfunc(T t){ifconstexpr(std::is_integral_vT){// 整数类型执行这段}else{// 其他类型}}场景模板特化简化、类型分支判断、可变参数处理替代笨重的模板重载。非模板场景两个分支代码语法都必须合法条件必须是编译期可求出的常量表达式。6. 类模板参数推导不用显式写出模板参数编译器自动推导// C14std::pairint,std::stringp{1,abc};// C17自动推导类型std::pair p{1,abc};std::vector vec{1,2,3};只适用于类模板不能推导函数模板复杂自定义类有时需要手动提供推导指引。7. 嵌套命名空间简写// C14 冗长写法namespaceA{namespaceB{namespaceC{}}}// C17 简写namespaceA::B::C{}头文件高频使用减少缩进。8. 属性标记 [[nodiscard]]标记函数返回值不允许忽略如果调用方丢弃返回值编译器给出警告适合错误码、optional 返回值。[[nodiscard]]intopen_file();工程规范、错误防御高频使用。二、标准库高频更新1. std::optional头文件optionalstd::optionalT是一个包装容器内部存储一块T的内存 一个布尔标记标记当前是否有效值。它专门用来解决函数返回值有可能失败、没有结果的问题。在C17之前我们只能用这些丑陋方案标记失败返回-1、INT_MAX等魔法值缺点返回值本身就可以取到这个数字歧义返回指针失败返回nullptr缺点要管理堆内存、容易内存泄漏optional 本质值语义对象可拷贝、可移动不需要堆内存。#includeoptional#includeiostream// 查找id对应的分数查找失败则无结果std::optionalintquery_score(intid){if(id100){return95;// 有值}returnstd::nullopt;// 代表空没有结果}intmain(){autoresquery_score(99);// 方式1判断是否有值if(res.has_value()){std::coutres.value()\n;}// 方式2bool隐式转换简写判断if(res){std::cout*res\n;}// 方式3取不到值时给默认值不会抛异常intscoreres.value_or(60);return0;}API作用.has_value()bool判断是否包含有效值.value()获取值空的时候调用直接抛异常std::bad_optional_access*opt解引用获取值空的时候行为是未定义UB不会抛异常.value_or(default)有值返回原值无值返回默认值安全无异常.reset()清空optional变成空状态使用场景查询类接口数据库查找、配置查找查找可能失败函数参数可选某些字段可以不存在解析报文某个字段可能缺失注意点不要混用.value()和*空的时候.value()抛异常*直接UB隐患极大不要返回std::optionalstd::stringoptional不能存引用只能存值对象安全优先拿不到值优先使用value_or()返回optional的函数建议加上[[nodiscard]]防止调用方忽略失败状态2. std::variant头文件variantstd::variantT1,T2,T3...是类型安全的联合体同一时刻variant里面最多保存其中一种类型的值。底层是一块足够容纳最大类型的缓冲区 一个下标标记记录当前存放的是哪一种类型栈上分配无堆内存开销。C语言原生union的致命缺陷没有标记记录当前存的是什么类型、不能存放带非平凡构造析构的类型例如std::string#includevariant#includeiostream#includestringvoidprint_value(conststd::variantint,double,std::stringv){// 判断当前存储的类型if(std::holds_alternativeint(v)){std::coutint: std::getint(v)\n;}elseif(std::holds_alternativedouble(v)){std::coutdouble: std::getdouble(v)\n;}elseif(std::holds_alternativestd::string(v)){std::coutstring: std::getstd::string(v)\n;}}intmain(){std::variantint,double,std::stringdata;data10;print_value(data);data3.14;print_value(data);datahello variant;print_value(data);return0;}API作用std::holds_alternativeT(var)判断variant当前是不是T类型std::getT(var)取出T类型的值类型不匹配抛std::bad_variant_accessstd::get_ifT(var)安全版本。匹配成功返回指针失败返回nullptr不会抛异常std::visit(fun, var)访问器自动匹配当前类型执行回调最优雅的遍历方式使用场景消息协议、报文一个字段可能是多种不同类型状态机的数据载体解析配置字段类型不固定但是类型集合有限注意点variant不能存放引用std::variantint非法variant里面不能放重复类型variantint,int编译报错std::getT类型不对直接抛异常不想抛异常优先用std::get_if存放的所有类型必须能被构造禁止放入没有默认构造函数的类型如果需要表示variant里面什么都没有加上std::monostatestd::variantstd::monostate, int, std::string3. std::any头文件anystd::any是无约束的类型擦除容器可以存放任意一种单值类型不需要提前声明类型列表。内部实现堆上分配内存存储对象运行时记录类型信息。和variant的本质区别variant类型集合编译期就要全部写死栈内存几乎无运行时开销any类型可以运行时任意变化堆分配有运行时开销类型安全检查#includeany#includeiostream#includestringintmain(){std::any container;container100;// 取出值类型错误抛 std::bad_any_castintnumstd::any_castint(container);std::coutnum\n;containerstd::string(hello any);std::string strstd::any_caststd::string(container);std::coutstr\n;// 安全读取不抛异常if(int*pstd::any_castint(container)){std::cout*p\n;}// 判断是否为空if(!container.has_value()){}return0;}API作用.has_value()是否存有对象std::any_castT(any_obj)取出值类型不匹配抛异常std::any_castT(any_obj)指针版本失败返回nullptr安全无异常使用场景通用事件总线事件携带的数据类型完全无法提前预知脚本、反射框架通用缓存对比选型指南类型集合有限 → std::variant有可能为空 → std::optional类型完全未知、任意类型 → std::any注意点any有堆内存开销频繁创建销毁性能较差高性能、实时循环里面尽量少用any_cast类型不对会抛异常优先用指针版std::any_castT(x)做安全判断any不能存放引用4. std::filesystem头文件filesystemC17第一次把跨平台文件系统操作纳入标准库。在此之前Windows需要调用Win32 APILinux调用posix两套代码无法跨平台std::filesystem::path是路径对象自动处理不同操作系统路径分隔符windows\linux/#includefilesystem#includeiostreamnamespacefsstd::filesystem;intmain(){fs::path log_dir./log;// 判断目录是否存在if(!fs::exists(log_dir)){// 创建目录fs::create_directories(log_dir);}fs::path cfg_filelog_dir/config.json;// /运算符拼接路径跨平台if(fs::exists(cfg_file)){// 获取文件大小字节autosizefs::file_size(cfg_file);std::coutfile size:size\n;}// 遍历文件夹下所有文件for(autoentry:fs::directory_iterator(log_dir)){if(entry.is_regular_file()){std::coutentry.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版本需要手动链接库编译加-lstdcfsGCC10以后不需要filesystem的接口大部分会抛异常如果你不想抛异常所有函数都有带std::error_code重载版本std::error_code ec;fs::create_directories(log_dir,ec);if(ec){// 创建失败}路径对象fs::path和std::string可以互相转换不要自己手动拼接字符串路径用path1 / path25. std::string_view头文件string_viewstd::string_view字符串只读视图不拥有、不拷贝、不存储字符串内存内部只有两个成员const char*指针 长度只是借用一段已经存在的字符缓冲区零拷贝、性能极高C17之前函数接收字符串参数只能写const std::string s如果我们传入一个char*字符串字面量编译器会隐式构造临时std::string发生内存拷贝带来开销。string_view完美规避拷贝开销。#includestring_view#includestring#includeiostream// 只读接收字符串零拷贝开销voidprint(std::string_view s){std::couts\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 tempabc;returntemp;// temp函数结束销毁返回的视图悬空}6. std::as_const头文件utilitystd::as_const(x)返回变量x的const左值引用不会产生拷贝作用把一个非const对象临时当作只读对象使用。#includeutility#includevector#includeiostreamintmain(){std::vectorintvec{1,2,3};autoconst_refstd::as_const(vec);// const_ref.push_back(5); // 编译报错只读无法修改// 只读范围for循环for(autov:std::as_const(vec)){std::coutv\n;}return0;}使用场景遍历容器强制只读防止不小心修改原数据lambda捕获时生成const副本注意点as_const只生成引用不会拷贝对象无性能开销不能用于临时对象7. std::clamp头文件algorithmtemplateclassTconstexprTclamp(constTvalue,constTlow,constThigh);数值截断函数把一个值限制在[low, high]的闭区间内。C17之前你必须手动写max、min嵌套intvalstd::max(low,std::min(high,val));clamp一步替代#includealgorithm#includeiostreamintmain(){intastd::clamp(120,0,100);//100intbstd::clamp(-10,0,100);//0intcstd::clamp(50,0,100);//50std::couta b c\n;return0;}传入参数low 必须 high如果lowhigh属于未定义行为。