ARTICLE DETAIL

建站实战干货

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

C++17 核心新特性总览:一张速查表 + 8 个上手示例

2026/10/7 2:41:45 拓冰建站 浏览量
C++17 核心新特性总览:一张速查表 + 8 个上手示例 C17 不是「又多了一堆语法糖」的版本它是让日常业务代码真正变短、变安全的一版std::optional替掉了-1这种哨兵值if (auto it m.find(k); it ! m.end())把迭代器关进了作用域std::filesystem让路径操作不再写平台相关的字符串拼接。这篇是一张速查地图。每个特性给一句话作用、一段最小示例、一句「什么时候会用到」并标明本知识库里哪一篇会展开细讲。1. 引子C14 代码里的三个重复动作// C14 写法三个「噪音」 std::mapstd::string, int ports {{http, 80}}; auto it ports.find(http); // ① it 的作用域泄露到外面 if (it ! ports.end()) { std::string name it-first; // ② 为了可读性再拷一份 key int port it-second; // ③ 再拷一份 value use(name, port); }C17 里这三块噪音同时消失// C17 写法 if (const auto it ports.find(http); it ! ports.end()) { // ① 作用域关进 if const auto [name, port] *it; // ②③ 结构化绑定零拷贝 use(name, port); }下面按「改动幅度」从大到小过一遍。2. 总览大表特性一句话作用典型代码本库细讲结构化绑定一次拆开 pair / tuple / 结构体 / 数组const auto [k, v] *it;《pair、tuple 与结构化绑定》if / switch 初始化语句把变量作用域关进分支里if (auto it m.find(k); it ! m.end())这里 §3if constexpr编译期分支另一支被丢弃if constexpr (std::is_integral_vT)《if constexpr 编译期分支》constexpr lambdalambda 也能编译期求值constexpr auto f [](int x){ return x * x; };《constexpr / constinit / consteval》std::string_view非拥有字符串视图零拷贝void f(std::string_view sv)《std::string_view 零拷贝字符串视图》std::optional显式表达「可能没有值」std::optionalint find(const Key)这里 §4std::variant类型安全的联合体类型集合编译期固定std::variantint, std::string v;这里 §4std::any运行时装任意可拷贝类型std::any a 42;《C std::any 完全指南》std::filesystem跨平台路径与目录操作fs::path p{/usr/include};这里 §5折叠表达式一行展开整个参数包return (values ... 0);《可变参数模板与折叠表达式》inline 变量头文件里定义全局变量不再 ODR 冲突inline constexpr int kMax 3;《inline 的真面目从函数内联到 C17 变量》类模板参数推导 CTAD类模板也能省模板实参std::pair p{1, 2.5};《CTAD 类模板参数推导与推导指引》[[nodiscard]]忽略返回值给编译警告[[nodiscard]] bool save();这里 §6[[maybe_unused]]明确「就是不用它」压掉警告int f(int code, [[maybe_unused]] int lv);这里 §6[[fallthrough]]声明「故意贯穿」不是漏写 breakcase 1: f(); [[fallthrough]];这里 §6嵌套命名空间少写一层缩进和括号namespace app::net::detail { }这里 §7std::apply把 tuple 摊开成函数实参std::apply(add3, args);这里 §8std::invoke统一调用成员指针 / 普通函数 / 仿函数std::invoke(Point::x, pt);这里 §8并行算法execution 头文件标准算法加个策略就能并行std::sort(std::execution::par, b, e);这里 §9std::byte「字节」终于有独立类型std::byte b{0xFF};这里 §6std::clamp / gcd / lcm常用小工具进了标准库std::clamp(x, lo, hi)/std::gcd(a, b)这里 §6保证的复制消除复制消除从「允许」升级为「必须」Tracer t make();《RVO 与 NRVO 返回值优化》变量模板_v简写trait 查询少写一层::valuestd::is_integral_vT《type_traits 查询与变换速查》3. 结构化绑定与 if 初始化语句消掉最多的噪音一行话结构化绑定把「一个对象」拆成「一组有名字的引用」if初始化语句把变量的可见范围缩到分支内部。std::mapstd::string, int m; for (const auto [key, value] : m) ... map 节点里真实存在的对象 ┌──────────────────────┬──────────────┐ │ first │ second │ │ const std::string │ int │ ← 数据只存在这一份 └──────────┬───────────┴──────┬───────┘ │ 绑定只是别名不是拷贝│ ▼ ▼ key value key 是 first 的 const 引用 → 不能改 value 是 second 的 const 引用 → 加了 const 也改不了 写成 auto [key, value] 就能改 value改的是 map 里那一份注意最后一行结构化绑定默认不是拷贝auto [k, v]会拷auto [k, v]/const auto [k, v]才是别名。这是最容易踩的一个点。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include cstdio #include map #include string int main() { const std::mapstd::string, int ports{ {http, 80}, {https, 443}, {ssh, 22}, }; // 结构化绑定一次拆开 pair不用再写 it-first / it-second for (const auto [name, port] : ports) { printf( %-6s - %d\n, name.c_str(), port); } // if 初始化语句it 的作用域被关在 if 里外面看不到它 if (const auto it ports.find(https); it ! ports.end()) { printf(https 的端口是 %d\n, it-second); } // 这里再写 it 会编译失败it 的生命周期已经结束 if (const auto it ports.find(ftp); it ports.end()) { printf(ftp 不在表里\n); } }http - 80 https - 443 ssh - 22 https 的端口是 443 ftp 不在表里官方文档结构化绑定、if 语句含初始化语句4. optional / variant把「可能没有」和「可能是之一」写进类型一句话std::optionalT表示「一个T或者什么都没有」std::variantA, B表示「一个A或者一个B编译期就定好集合」。它们最大的价值不是少写几行而是让函数签名自己说明意图返回int时你不知道-1是错误还是合法值返回std::optionalint时调用方一眼就明白。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include cstdio #include optional #include stdexcept #include string #include variant // optional不再用 -1 / 空字符串这类哨兵值表示「失败」 std::optionalint parse_port(const std::string text) { try { const int value std::stoi(text); if (value 1 || value 65535) return std::nullopt; // 有值但超出范围 return value; } catch (const std::invalid_argument) { return std::nullopt; // 根本转不成数字 } } struct PrintVisitor { void operator()(int x) const { printf( variant 装的是 int%d\n, x); } void operator()(const std::string s) const { printf( variant 装的是 string%s\n, s.c_str()); } }; int main() { if (const auto port parse_port(8080)) { printf(解析成功%d\n, *port); // 有值时才解引用 } printf(parse_port(\abc\) 有值吗 %s\n, parse_port(abc).has_value() ? 有 : 没有); printf(value_or 兜底%d\n, parse_port(abc).value_or(80)); const std::variantint, std::string v1 8080; const std::variantint, std::string v2 std::string(not a number); std::visit(PrintVisitor{}, v1); // visit按当前实际类型分派 std::visit(PrintVisitor{}, v2); printf(v1 是 string 吗 %s\n, std::holds_alternativestd::string(v1) ? 是 : 否); }解析成功8080 parse_port(abc) 有值吗 没有 value_or 兜底80 variant 装的是 int8080 variant 装的是 stringnot a number v1 是 string 吗 否官方文档std::optional、std::variant选型顺序值得记一下类型集合编译期已知 →variant只是「可能没有」→optional类型到运行时才知道 → 才轮到any。需求用哪个存储取值方式一个值可能不存在std::optionalT栈上*opt/value_or有限个类型里取一个std::variantA,B栈上按最大成员对齐std::visit/std::get类型运行时才确定std::any通常堆上std::any_castT类型错就抛异常5. std::filesystem路径操作终于不用手工拼字符串一句话filesystem提供跨平台的路径解析、拼接、目录遍历、文件属性查询。路径拼接用/运算符不用自己判断分隔符。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include cstdio #include filesystem namespace fs std::filesystem; // 命名空间别名不是 using namespace int main() { const fs::path p{/usr/include/stdio.h}; printf(filename() %s\n, p.filename().string().c_str()); printf(stem() %s\n, p.stem().string().c_str()); printf(extension() %s\n, p.extension().string().c_str()); printf(parent_path %s\n, p.parent_path().string().c_str()); printf(is_absolute %s\n, p.is_absolute() ? true : false); // 拼路径用 / 运算符比手写 / name 安全分隔符由实现决定 const fs::path joined p.parent_path() / stdlib.h; printf(拼接后 %s\n, joined.string().c_str()); }filename() stdio.h stem() stdio extension() .h parent_path /usr/include is_absolute true 拼接后 /usr/include/stdlib.h官方文档std::filesystem、std::filesystem::path坑fs::path::string()在 Windows 上按本地编码返回窄字符串中文路径会出问题要处理中文路径请用u8string()。6. 属性、std::byte 与三个数学小工具一句话[[nodiscard]]让「忘了用返回值」变成编译期警告[[maybe_unused]]/[[fallthrough]]用来精准关掉某一条警告std::byte让字节和整数在类型上分开。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include algorithm #include cstddef #include cstdio #include numeric // inline 变量C17头文件里定义全局常量被多个翻译单元包含也不会 ODR 冲突 inline constexpr int kMaxRetries 3; // [[nodiscard]]谁忘了用返回值编译期就警告 [[nodiscard]] bool save_to_disk(int retries) { return retries kMaxRetries; } // [[maybe_unused]]明确告诉编译器「这个参数就是暂时不用」别报警告 int handle(int code, [[maybe_unused]] int verbose_level) { switch (code) { case 1: printf( code 1 的前置处理\n); [[fallthrough]]; // 声明这是故意的不是漏写 break case 2: printf( code 1 和 2 共用这段\n); break; default: printf( 其他 code\n); break; } return code; } int main() { printf(kMaxRetries %d\n, kMaxRetries); printf(save_to_disk(2) %s\n, save_to_disk(2) ? true : false); handle(1, 0); handle(3, 0); printf(clamp(15, 0, 10) %d\n, std::clamp(15, 0, 10)); printf(gcd(24, 18) %d\n, std::gcd(24, 18)); printf(lcm(4, 6) %d\n, std::lcm(4, 6)); const std::byte flags{0xFF}; printf(std::byte 0xFF 转成整数 %d\n, static_castint(flags)); }kMaxRetries 3 save_to_disk(2) true code 1 的前置处理 code 1 和 2 共用这段 其他 code clamp(15, 0, 10) 10 gcd(24, 18) 6 lcm(4, 6) 12 std::byte 0xFF 转成整数 255官方文档[nodiscard]、[maybe_unused]、[fallthrough]、std::byte[[nodiscard]]是这三个里最值得无脑加的一个凡是返回bool表示成功/失败、或者返回新对象的函数标上它未来某次改动漏了检查返回值编译器会替你发现。std::byte的价值也很实在它不是整数不能直接参与算术从类型上挡住了「把字节当数字算」这类错误。7. if constexpr编译期剪枝一句话if constexpr的条件在编译期求值不成立的那一支会被丢弃连实例化都不会发生——所以那一支里可以写「对当前类型根本不合法」的代码。template typename T std::string describe(T v) { if constexpr (std::is_integral_vT) { return 整数; } else if constexpr (std::is_floating_point_vT) { return 浮点; } else { return 其他; } } 实例化 T int 实例化 T const char* ┌──────────────────────────┐ ┌──────────────────────────┐ │ 保留「整数」分支 │ │ 丢弃「整数」「浮点」分支 │ │ 另两支整段丢弃 │ │ 只保留 else 分支 │ │ 里面的 to_string 不实例化│ │ std::string(v) 才合法 │ └──────────────────────────┘ └──────────────────────────┘ 对比普通 if两支都会编译只是运行时不走 —— 类型不对就直接报错// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include cstdio #include string #include type_traits template typename T std::string describe(T value) { if constexpr (std::is_integral_vT) { // _v 简写也是 C17 新增 return 整数 std::to_string(value); } else if constexpr (std::is_floating_point_vT) { return 浮点数 std::to_string(value); } else { return 其他类型 std::string(value); // 只对能转 string 的类型有效 } } int main() { printf(%s\n, describe(42).c_str()); printf(%s\n, describe(3.5).c_str()); printf(%s\n, describe(hello).c_str()); // constexpr lambdaC17lambda 也能在编译期求值 constexpr auto square [](int x) { return x * x; }; static_assert(square(7) 49, 编译期就算完了); printf(编译期算出的 square(7) %d\n, square(7)); }整数 42 浮点数 3.500000 其他类型hello 编译期算出的 square(7) 49官方文档if 语句if constexpr、constexpr lambda8. 折叠表达式、CTAD、apply / invoke一句话折叠表达式把参数包一行展开CTAD让类模板不用写模板实参std::apply把 tuple 摊成实参std::invoke用同一种写法调用所有「可调用体」。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include cstdio #include functional #include string #include tuple #include utility // 折叠表达式左折叠(values ... 0) 在空包时取初值 0 template typename... Ts auto sum_all(Ts... values) { return (values ... 0); } int add3(int a, int b, int c) { return a b c; } struct Point { int x 0; int y 0; int sum() const { return x y; } }; int main() { printf(sum_all() %d\n, sum_all()); printf(sum_all(1,2,3,4) %d\n, sum_all(1, 2, 3, 4)); // CTAD类模板参数推导不用再写 std::pairint, double const std::pair p{1, 2.5}; // → std::pairint, double const std::tuple t{1, std::string(two), 3.0}; printf(CTAD pair.second %.1f\n, p.second); printf(CTAD tuple 大小 %zu\n, std::tuple_size_vdecltype(t)); const std::tupleint, int, int args{10, 20, 30}; printf(apply(add3,args) %d\n, std::apply(add3, args)); // std::invoke成员变量指针 / 成员函数指针 / 普通函数写法统一 const Point pt{3, 4}; printf(invoke 读成员 %d\n, std::invoke(Point::x, pt)); printf(invoke 调成员函数 %d\n, std::invoke(Point::sum, pt)); }sum_all() 0 sum_all(1,2,3,4) 10 CTAD pair.second 2.5 CTAD tuple 大小 3 apply(add3,args) 60 invoke 读成员 3 invoke 调成员函数 7官方文档折叠表达式、类模板参数推导、std::apply、std::invoke9. 保证的复制消除与并行算法C17 把「返回纯右值时的复制消除」从允许变成了必须guaranteed copy elision。用带日志的类型可以直接看到构造只发生一次拷贝构造和移动构造一次都没调用。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include cstdio struct Tracer { int id 7; Tracer() { printf( [Tracer] 默认构造\n); } Tracer(const Tracer) { printf( [Tracer] 拷贝构造\n); } Tracer(Tracer) noexcept { printf( [Tracer] 移动构造\n); } ~Tracer() { printf( [Tracer] 析构\n); } }; Tracer make_tracer() { return Tracer{}; } // 返回纯右值 → C17 保证不拷贝不移动 int main() { printf(调用 make_tracer() 并初始化 t\n); { const Tracer t make_tracer(); // 直接在 t 的存储上构造 printf( t.id %d\n, t.id); } printf(t 的作用域结束\n); }调用 make_tracer() 并初始化 t [Tracer] 默认构造 t.id 7 [Tracer] 析构 t 的作用域结束官方文档复制消除copy elision并行算法是 C17 里最容易被忽略的一项给标准算法加一个执行策略即可。execution头文件里定义了三个策略——seq串行、par并行、par_unseq并行 向量化。// demo.cpp — 编译: g -stdc17 -O2 demo.cpp -o demo -ltbb 并行算法需要链接 TBB #include algorithm #include cstdio #include execution #include vector int main() { std::vectorint v{5, 3, 1, 4, 2}; std::sort(std::execution::par, v.begin(), v.end()); // par 并行执行策略 printf(并行排序结果); for (const int x : v) printf( %d, x); printf(\n); printf(最小值 %d\n, *std::min_element(std::execution::par, v.begin(), v.end())); }并行排序结果 1 2 3 4 5 最小值 1官方文档执行策略std::execution::par10. 两个不用编译也能记住的小改动嵌套命名空间C17 起可以一次写完不用层层缩进// C17 起等价于三层嵌套 namespace namespace app::net::detail { constexpr int kBufferSize 4096; } // 只写一对花括号十六进制浮点字面量与_v变量模板// 十六进制浮点字面量尾数用 16 进制、指数用 2 进制表示精确的浮点值 constexpr double kThreshold 0x1.8p3; // 1.5 × 2^3 12.0 // 变量模板 _v 后缀少写一层 ::value static_assert(std::is_integral_vint); // 等价于 std::is_integralint::value static_assert(!std::is_floating_point_vint);官方文档命名空间定义、浮点字面量11. 为什么 C17 值得升按日常影响排序排序特性对日常代码的实际影响上手成本1结构化绑定遍历 map / 返回多值 / 解 tuple 全靠它代码立刻短一截极低2std::optional干掉-1、、nullptr这类哨兵值和「返回 bool 出参」极低3std::string_view字符串形参不再触发临时std::string构造低但要懂悬垂4if/switch初始化语句迭代器、锁、句柄的作用域能收干净极低5if constexpr模板代码不用再玩 SFINAE 重载那套中6std::filesystem路径拼接、遍历目录、查属性不再写平台分支低7std::variant需要「有限类型里选一个」时比继承 虚函数轻量得多中8折叠表达式可变参模板从「两三个重载 递归」变成一行中9属性nodiscard等把约定交给编译器检查长期收益大于短期便利极低10并行算法 /std::byte/clamp等局部优化点用到了再查就行低一句话的经验先吃排序 1–4 这四个代码质量的提升最明显、风险最低if constexpr和折叠表达式留到真正要写模板时再上。12. 延伸阅读cppreference — C17 标准库新特性清单官方口径的完整列表比任何二手总结都准cppreference — 结构化绑定auto/auto/const auto三种写法的绑定规则都在这cppreference — 类模板参数推导CTAD 的推导过程和自定义推导指引cppreference — 复制消除C17 前后「保证 / 允许」的差别讲得最清楚ISO C — isocpp.org标准版本与各编译器支持度升级前先确认工具链本知识库内的相关篇目《C20 核心新特性总览四大特性 gcc 13 实测清单》 —— C20 是继 C11 之后最大的一次改动。《if / switch 初始化语句C17 把变量作用域关进笼子》 —— C17 给 if 和 switch 加了 init-statement《指定初始化器C20 给聚合初始化补上的最后一块》 —— C20 的指定初始化器designated initializers让 Point p{.x 13. 一句话总结C17 的价值集中在四件事上少写结构化绑定、初始化语句、折叠表达式、少错optional/variant的类型表达、属性检查、少拷string_view、保证的复制消除、少写平台分支filesystem先把前四个排序靠前的用熟收益最大且风险最低。