
C23 是一版「补齐短板」的标准它没有引入全新的心智模型而是把十几年来大家反复手写的模式收进标准库。这篇挑四个最实用的讲清楚std::expected值或错误、std::print类型安全输出、deducing this显式对象参数、monadic optional可空值链式处理。它们都需要 C23 编译器其中 deducing this 在 gcc 14 之后才完整支持正文里会单独标版本。1. 引子错误处理的两难写一个「解析用户输入成整数」的函数返回值放不下「出错了」这个信息。三种老办法各有代价返回int出错返回 0分不清「合法的 0」和「出错的 0」、抛异常错误是罕见路径时太重、返回std::optionalint能表示「没有值」但说不出为什么没有。std::expectedT, E就是冲着第三种的缺口来的它要么持有T值要么持有E错误两个通道都显式存在。std::expectedint, std::string ┌──────────────────────────────────┐ │ 有值has_value() true │──── value() - int │ or │ │ 有错has_value() false │──── error() - std::string └──────────────────────────────────┘ ↑ 两种状态占一个对象官方文档std::expected — cppreference下面用「除零」这个最经典的场景跑一遍除数合法返回商除数为零返回一句错误描述。// verify: stdc23#includeexpected#includeiostream#includestringstd::expectedint,std::stringdivide(inta,intb){if(b0)returnstd::unexpected{divide by zero};returna/b;}intmain(){autookdivide(10,2);autobaddivide(10,0);std::coutok.has_value()ok.has_value() valueok.value()\n;std::coutbad.has_value()bad.has_value() errorbad.error()\n;std::coutbad.value_or(-1)bad.value_or(-1)\n;}ok.has_value()1 value5 bad.has_value()0 errordivide by zero bad.value_or(-1)-1std::unexpected{divide by zero}是构造错误态的方式读值时用value()错误态会抛std::bad_expected_access或value_or(-1)错误态返回兜底值。它和optional的差别以及和各错误策略的取舍见下表。策略能表达「为什么错」错误是否强制处理高频可预期错误的代价适用返回错误码 / 哨兵值否或靠约定否容易漏判低C ABI、性能敏感的内层循环抛异常是异常类型是不 catch 就崩高栈展开真正的「非正常路径」std::optionalT否否低只需要「有没有」std::expectedT,E是否但错误和值都在类型里显式低库边界、可预期失败expected适合「失败是可预期的、调用方必须知道」的场景——它把「值 or 错误」写进类型签名编译器帮你记得处理。2. std::print类型安全、格式化的输出std::cout x x and y y \n这种写法把格式和变量混在一串里既啰嗦又难本地化。C23 引入std::print/std::println用法和std::format一致{}是占位符{:.3f}是格式化规格。// verify: stdc23#includeprintintmain(){std::print({} {} {}\n,2,3,23);std::println(pi {:.3f},3.1415926);std::println({},print 不刷缓冲比 endl 快);}2 3 5 pi 3.142 print 不刷缓冲比 endl 快官方文档std::print / std::println — cppreference注意std::print没有std::endl那种「顺手刷缓冲」的副作用。这里面的差异是个性能考点std::endl会强制 flush而print只负责写入刷新交给系统按需完成。格式字符串写错比如{}数量对不上参数在 C23 里编译期就能报错这是 iostream 做不到的。3. deducing this显式对象参数传统成员函数里this是隐式的你拿不到它的类型也没法在成员函数上做「按值 vs 按引用」的分派。deducing this显式对象参数explicit object parameter把this变成第一个显式参数写this auto const self或this Point self。// verify: stdc23// verify: compilergcc-head#includeiostreamstructPoint{intx{1},y{2};autonorm(thisautoconstself){returnself.x*self.xself.y*self.y;}intpick(thisPointself,booluseX){returnuseX?self.x:self.y;}};intmain(){Point p;std::coutnormp.norm()\n;p.pick(true)10;std::coutxp.x yp.y\n;}norm5 x10 y2官方文档Member functions — cppreference其中「Explicit object parameter」一节它最大的价值是免掉 CRTPcuriously recurring template pattern奇异递归模板模式。以前想在基类里拿到派生类类型得写templateclass Derived struct Base { ... };再struct D : BaseD {}现在一个this auto self就够了调用时self会自动推导成实际对象类型。写法拿到派生类类型的方式样板量传统成员函数拿不到this固定为当前类指针无CRTPstatic_castDerived(*this)多模板基类 继承声明deducing thisthis auto self自动推导少deducing this 需要 gcc 14 或 clang 18所以这个示例指定了compilergcc-head其余示例用默认的 clang-17 即可。4. monadic optional可空值的链式处理处理「可能没有值」时最常见的写法是一串if (opt) { ... } else { ... }。C23 给std::optional补上 monadic单子式操作and_then有值则继续、返回新 optional、transform有值则映射、原地包裹、or_else无值则走兜底。// verify: stdc23#includeiostream#includeoptional#includestringstd::optionalinttoInt(conststd::strings){try{returnstd::stoi(s);}catch(...){returnstd::nullopt;}}intmain(){autorstd::optionalstd::string{42}.and_then(toInt).transform([](intx){returnx1;}).or_else([]{returnstd::optionalint{0};});std::coutpipeline(\42\) *r\n;autor2std::optionalstd::string{abc}.and_then(toInt).transform([](intx){returnx1;}).or_else([]{returnstd::optionalint{0};});std::coutpipeline(\abc\) *r2\n;}pipeline(42) 43 pipeline(abc) 0官方文档std::optional::and_then — cppreference整个「字符串 → 整数 → 加一」的流水线写成一串空值在任意一步短路、最后用or_else收口不再需要嵌套if。expected也有同样的一套and_then/transform/or_else/transform_error所以学了 optional 的这套expected 直接复用。操作有值时无值时返回类型transform(f)f(v)包进 optional保持空optionaldecltype(f(v))and_then(f)f(v)f 自己返回 optional保持空optional...f 的返回or_else(f)保持原值f()f 返回 optional与原 optional 同类型5. 完整示例四个特性一起用把一个「读取配置 → 解析整数 → 打印结果」的流程串起来覆盖这篇全部要点。// verify: stdc23#includeexpected#includeoptional#includeprint#includestringstd::optionalintparse(conststd::strings){try{returnstd::stoi(s);}catch(...){returnstd::nullopt;}}intmain(){// optional 链解析失败兜底 8080autoportparse(9090).and_then([](intx){returnstd::optionalint{x};}).or_else([]{returnstd::optionalint{8080};});// expected 链范围校验非法走错误通道autocheck[](intp)-std::expectedint,std::string{if(p1||p65535){returnstd::expectedint,std::string{std::unexpected{port out of range}};}returnstd::expectedint,std::string{p};};autoresultstd::expectedint,std::string{*port}.and_then(check);autobadstd::expectedint,std::string{99999}.and_then(check);std::println(port {},result.value());std::println(bad has_error{} err{},bad.has_value(),bad.error());}port 9090 bad has_errorfalse errport out of range两段链各司其职optional 的and_then/or_else处理「有没有值」expected 的and_then处理「值合不合法」。注意 optional 的and_then要求返回 optional、expected 的and_then要求返回 expected两者不能混用这也是类型系统在帮你把「可空」和「可错」这两件事分开。6. 延伸阅读std::expected — cppreference值或错误的完整接口含 monadic 操作。std::print — cppreference格式字符串语法与std::format同源。Member functions (deducing this) — cppreference显式对象参数的语法与推导规则。std::optional::and_then — cppreferencemonadic optional 的入口同页能跳到transform/or_else。本知识库内的相关篇目《C20 升 C23 迁移清单开关、破坏性改动与收益》 —— 从 C20 升到 C23 是「低风险、有正收益」的一次迁移《前瞻std::generator、reflection 与 C 的未来方向》 —— C23 之后C26 正在路上《C 错误处理策略异常 vs 错误码 vs expected 的真实取舍》 —— 函数失败到底该抛异常、返回错误码顺带把工程与架构这条线也铺一下。7. 一句话总结std::expected补上「值 or 错误」的缺口std::print让输出类型安全又简洁deducing this 消灭 CRTP 样板需 gcc 14monadic optional 让可空值能像流水线一样链式处理——这四个是 C23 里最值得先上手、也最能立刻替换掉手写样板的一组特性。