C++23 是"每三年一版"节奏下的最新标准,已由 ISO 正式发布。它没有 C++20 那样的大爆炸,但带来的都是实打实的便利:错误处理有了正统的 std::expected,成员函数可以显式声明 this 参数,输出有了 std::print, ranges 库也补上了 zip 和 to。本文带你预览这些前沿特性,并说明哪些值得立刻用起来。

1. std::expected:函数式错误处理

C++20 的 std::optional 只回答"有没有值",回答不了"为什么失败"。std::expected<T, E> 类似 Rust 的 Result:要么持有成功值 T,要么持有错误 E,错误信息随值一起返回,不再依赖异常、哨兵值或出参。对"错误是常态"的代码(解析、校验、网络请求),它比异常更轻、比返回码更安全。

#include <expected>
#include <iostream>
#include <string>

enum class Error { BadFormat, OutOfRange };

// 解析字符串为整数:成功返回 int,失败返回 Error
std::expected<int, Error> parse_int(const std::string& s) {
    try {
        std::size_t pos = 0;
        int value = std::stoi(s, &pos);
        if (pos != s.size()) return std::unexpected(Error::BadFormat);
        return value;
    } catch (...) {
        return std::unexpected(Error::BadFormat);
    }
}

int main() {
    auto ok = parse_int("42");
    auto bad = parse_int("abc");

    std::cout << (ok ? *ok : -1) << std::endl;       // 42
    std::cout << (bad ? *bad : -1) << std::endl;     // -1

    if (!bad) {
        std::cout << "错误码: "
                  << static_cast<int>(bad.error()) << std::endl;
    }
    return 0;
}

访问接口和 optional 几乎一一对应:operator* 解引用、value() 无值时抛 bad_expected_access、value_or 给默认值;错误侧用 error() 读取。配合 and_then / transform / or_else 等成员函数,还能把多个可能失败的步骤链式串起来,写出一段没有 if 的流水线。expected 最迷人的就是这种组合能力:auto r = step1().and_then(step2).or_else(handle_error); 每一步失败都自动短路,错误处理变成一条可读的链。与异常相比,它把"错误路径"显式写进类型系统,调用者想忽略都难;与返回码相比,它强制你处理错误,不会"忘了检查"。代价是代码里要多写一些类型标注,但换来的是确定性。

2. deducing this:this 作为显式参数

过去成员函数的 const 重载要写两遍:void f() 和 void f() const,代码重复且容易漏。deducing this 允许把"对象自身"作为第一个参数显式写出,编译器自动推导 const/非 const、左值/右值版本,一份代码通吃所有情况。它还让递归 lambda 第一次变得优雅。

#include <iostream>

struct Counter {
    int count = 0;

    // 显式对象参数:自动适配 const/非 const 调用
    int get(this const Counter& self) { return self.count; }

    Counter& inc(this Counter& self) {
        ++self.count;
        return self;
    }
};

int main() {
    Counter c;
    c.inc().inc();            // 链式调用
    std::cout << c.get() << std::endl;   // 2

    const Counter cc = c;
    std::cout << cc.get() << std::endl;  // const 对象也能调用
    return 0;
}

另一个经典场景是递归 lambda:auto dfs = [](this auto&& self, int n) -> void { if (n > 0) self(n - 1); };。以前递归 lambda 要用 std::function 或手动包装,现在一句声明即可,性能还更好。deducing this 还让 CRTP(奇异递归模板)这种绕弯的写法有了更直接的替代:基类模板想访问派生类成员,以前要靠 static_cast<Derived&>(*this),现在直接写成员函数参数 this Derived& self,编译器自动完成推导。标准库也在跟进,很多原本需要 const 重载的接口,未来都会统一成一份声明。

3. std::print 与 std::println:告别 cout 流式

printf 不安全,cout 啰嗦。C++23 的 std::println 兼具两者优点:格式化占位符清晰、类型安全、自动换行,还支持 {} 占位符(类似 Python 的 f-string)。日常调试和日志输出,它都是更舒服的选择。

#include <print>

int main() {
    std::println("Hello, C++23!");
    std::println("圆周率约等于 {:.2f}", 3.14159);
    std::println("{} 年发布了 C++{}", 2023, 23);
    return 0;
}

格式化语法与 std::format 完全一致:冒号后面跟格式说明,{:.2f} 表示保留两位小数的浮点数,{} 按参数顺序填充。编译期还会校验占位符与参数是否匹配,写错直接编译失败,而不是运行时才崩。std::print 输出到 stdout,std::println 自动换行,还有带 FILE* 的 std::print(FILE*, ...) 重载可以写文件。它底层基于 std::format,格式化规则与 Python 的 format 相近,学一次到处用。注意:print 系列需要编译器在编译期解析格式字符串,GCC 14 之前的版本会报错,升级工具链是使用它的前提。

4. ranges 补完:zip、to 与 optional 的 monadic 操作

C++23 给 ranges 库补上了期待已久的拼装件:std::views::zip 并行遍历多个容器,std::ranges::to 把视图直接收集成容器;std::optional 也新增 transform / and_then / or_else,链式处理"可能为空"的值,风格与 expected 对齐。

#include <optional>
#include <print>
#include <ranges>
#include <string>
#include <vector>

int main() {
    // zip:并行遍历两个容器,结构化绑定拆开
    std::vector<int> ids = {1, 2, 3};
    std::vector<std::string> names = {"Alice", "Bob", "Carol"};
    for (auto [id, name] : std::views::zip(ids, names)) {
        std::println("{}: {}", id, name);
    }

    // ranges::to:视图直接收集成 vector
    auto doubled = std::views::iota(1, 6)
        | std::views::transform([](int x) { return x * 2; })
        | std::ranges::to<std::vector<int>>();

    // optional 链式变换
    std::optional<int> o = 21;
    auto msg = o.transform([](int v) { return v * 2; })
                .transform([](int v) { return "答案是 " + std::to_string(v); });
    std::println("{}", msg.value_or("无值"));
    return 0;
}

这几样组合起来,"取数据、变换、收集、处理可能为空"整条链都能写成表达式,中间不出现一个临时变量,可读性和可维护性都远超手写循环。

5. 如何在编译器中启用 C++23

# 编译并运行(std::print 需要 GCC 14+)
g++ -std=c++23 main.cpp -o main
./main

# Clang 建议 17+,配合 libc++
clang++ -std=c++23 -stdlib=libc++ main.cpp -o main

C++23 特性在各大编译器中的落地进度不一,编译前先对照下表确认:

特性作用GCCClang(libc++)
std::expected携带错误信息的返回值13+16+
deducing this显式 this 参数14+17+
std::print / println类型安全格式化输出14+18+
views::zip / ranges::toranges 拼装件13+17+
optional monadic链式处理可选值13+17+

表中没有列出的特性还有 std::stacktrace(程序崩溃时打印调用栈)、std::mdspan(多维数组视图)、static operator[] 等,它们各有用途,但对大多数项目优先级不高,知道存在即可,等真正需要时再查文档。最后提醒一点:C++23 的许多特性单独看只是"语法糖",但组合起来效果惊人——expected 加 optional 的 monadic 接口、print 加 format 的编译期检查、zip 加结构化绑定,每一条都能直接减少一类 bug。拥抱新标准最好的方式,就是在一个小项目里完整地用一遍。

6. 升级到 C++23 值不值

如果你是个人项目或新项目,值得直接上:C++23 对 C++20 完全向后兼容,新代码用 std::print 和 std::expected 写起来明显更舒服,编译期错误也更友好。但如果是团队生产环境,建议先看两个因素:一是编译器/CI 镜像是否已升级到 GCC 14+ 或 Clang 17+,二是团队是否愿意消化新语法带来的代码风格变化。再给一个判断标准:如果你们的 CI 已经在跑 C++20,那么升级 C++23 只是把 -std=c++20 改成 -std=c++23 再修少量编译错误的事;如果连 C++20 都没上,建议先补 C++20 的 concepts 和 ranges,再谈 C++23。步子太大会让团队消化不良,循序渐进反而更快。

务实的做法是分两步走:第一步,把 std::print、views::zip 这类零风险特性立刻用起来,它们不改变任何既有设计;第二步,把 expected 引入到"错误是常态"的新接口里(解析器、校验器、客户端调用),等团队熟悉后再逐步替换旧的错误码约定。记住:前沿特性不必全用,能立刻提升代码质量的那几个,才是你的第一桶金。

💡 学习建议:用 std::println 重写一个你熟悉的输出密集小程序,再把你项目里"返回 -1 表示失败"的函数改成 std::expected,体会错误信息随值传递的清爽。建议在本地装一个 GCC 14+,把本文所有示例亲手编译运行一遍。