← 返回博客列表

一、概述

C++23(ISO/IEC 14882:2024)是继 C++20 之后的又一次重要更新。如果说 C++20 是"四大天王"级别的革命性升级,那么 C++23 就是"填坑 + 完善 + 实用化"的版本——它补齐了 C++20 遗留的短板(如 std::format 没有 print),引入了期待已久的 std::expected 错误处理方案,并添加了大量让日常编码更愉快的便利特性。

C++23 核心看点(一句话总结)

最重要的std::print/println —— 终于告别 std::cout 的冗长语法

最实用的std::expected<T, E> —— 不用异常也能优雅处理错误

最惊喜的Deducing this —— CRTP 的终结者,Lambda 递归的救星

最期待的std::flat_map / std::flat_set —— 缓存友好的扁平容器

最养眼的std::mdspan —— 多维数组视图,科学计算的福音

最爽快的optional::and_then / transform —— 链式调用消灭嵌套 if

二、std::print / std::println —— 终于能打印了

C++20 引入了 std::format,带来了 Python f-string 风格的格式化能力。但令人抓狂的是——它只能格式化字符串,不能直接输出到终端!你必须写:

// C++20:又臭又长
#include <format>
#include <iostream>

std::cout << std::format("Hello, {}! You are {} years old.", name, age);

// 或者先 format 再 cout,多此一举
auto s = std::format("x={}, y={}", x, y);
std::cout << s << '\n';

C++23 终于补上了这个坑:std::print 和 std::println 直接格式化并输出,无需 #include <iostream>,无需手动加换行。

#include <print>  // 就这一个头文件

// 基本用法
std::println("Hello, {}!");                    // Hello, world!
std::println("Name: {}, Age: {}", name, age);  // Name: Alice, Age: 30

// print 不换行,println 换行
std::print("Loading... ");
std::print("{}%", progress);
std::println();  // 空参数 = 只输出换行

// 格式化说明符:和 std::format 完全一致
std::println("Pi = {:.2f}", 3.14159);     // Pi = 3.14
std::println("Hex: {:08x}", 255);         // Hex: 000000ff
std::println("{:>20}", "right aligned");  // 右对齐,宽度20

// 输出到文件
std::FILE* fp = std::fopen("log.txt", "w");
std::println(fp, "Log: {} = {}", key, value);
对比:为什么 println 比 cout 好?
· 性能:println 内部使用编译期格式字符串解析,比 cout 的 << 链式调用快得多
· 可读性:println("x={}, y={}", x, y) 比 cout << "x=" << x << ", y=" << y << '\n' 清晰十倍
· 类型安全:编译期检查格式字符串与参数类型匹配
· 国际化:格式字符串中的占位符顺序可以调整,cout 链做不到

三、std::expected<T, E> —— 错误处理新范式

C++ 的错误处理一直有两派之争:异常 vs 返回码。异常优雅但开销不可预测;返回码高效但容易忘记检查。C++23 引入的 std::expected<T, E> 借鉴了 Rust 的 Result<T, E>,提供了一种既安全又高效的方案。

核心思想:函数要么返回期望的值 T,要么返回错误 E。调用者必须处理两种情况,不能"假装没看到"。

#include <expected>
#include <string>

// 定义错误类型
enum class ParseError {
    Empty,
    InvalidChar,
    OutOfRange
};

// 返回 expected:成功返回 int,失败返回 ParseError
std::expected<int, ParseError> parse_int(const std::string& s) {
    if (s.empty()) return std::unexpected(ParseError::Empty);

    for (char c : s) {
        if (!std::isdigit(c)) return std::unexpected(ParseError::InvalidChar);
    }

    long val = std::stol(s);
    if (val < INT_MIN || val > INT_MAX)
        return std::unexpected(ParseError::OutOfRange);

    return (int)val;  // 隐式构造 expected<int, ...>
}

// ---- 使用示例 ----
int main() {
    auto result = parse_int("42");

    // 方式1:has_value() + value()
    if (result.has_value()) {
        std::println("Parsed: {}", result.value());
    } else {
        std::println("Error: {}", (int)result.error());
    }

    // 方式2:value_or() 提供默认值
    auto val = parse_int("abc").value_or(0);
    std::println("Value or default: {}", val);

    // 方式3:and_then() 链式调用(见下文 optional 详解)
    auto doubled = parse_int("21")
                      .and_then([](int v) -> std::expected<int, ParseError> {
                          return v * 2;
                      });
    std::println("Doubled: {}", doubled.value());
}

实战场景:文件读取 + JSON 解析 + 类型转换

// 链式调用,任一步失败则短路返回错误
std::expected<User, Error> load_user(const std::string& path) {
    return read_file(path)              // expected<string, Error>
        .and_then(parse_json)          // expected<Json, Error>
        .and_then(json_to_user)        // expected<User, Error>
        .transform([](User u) {           // 转换成功值
            u.loaded_at = std::time(nullptr);
            return u;
        });
}

// 使用:简洁且安全
if (auto user = load_user("config.json"); user) {
    std::println("User: {}", user->name);
} else {
    std::println("Failed: {}", user.error().message());
}
方案性能安全性可读性适用场景
异常 (try/catch)差(栈展开开销)一般(可能忘记 catch)好真正的异常情况(罕见错误)
返回码 (int/enum)好差(容易忽略)差简单函数
std::expected好(无异常开销)好(必须显式处理)好(链式调用)可预期的错误(解析失败、文件不存在等)

四、std::optional 单子操作 —— 链式调用消灭嵌套 if

C++17 引入了 std::optional,但用它处理链式操作时,代码会陷入"嵌套 if 地狱":

// C++17 的噩梦:嵌套 if 检查 optional
std::optional<Config> get_config();
std::optional<Database> get_db(const Config&);
std::optional<User> get_user(const Database&, int id);

auto cfg = get_config();
if (!cfg) return std::nullopt;

auto db = get_db(*cfg);
if (!db) return std::nullopt;

auto user = get_user(*db, 42);
if (!user) return std::nullopt;

// 终于拿到了 user...
return user;

C++23 为 optional 引入了三个单子操作(Monadic Operations):and_then、transform、or_else。一行搞定:

// C++23:链式调用,任一步返回 nullopt 则短路
auto user = get_config()
    .and_then([](const Config& c) { return get_db(c); })
    .and_then([](const Database& db) { return get_user(db, 42); });

// transform:对成功值做变换(类似 map)
auto name = get_config()
    .and_then([](const Config& c) { return get_db(c); })
    .and_then([](const Database& db) { return get_user(db, 42); })
    .transform([](const User& u) { return u.name; });  // optional<string>

// or_else:失败时提供替代方案
auto safe_user = get_config()
    .and_then(get_db)
    .and_then([](auto& db) { return get_user(db, 42); })
    .or_else([] { return std::optional{User::guest()}; });  // 失败就用默认用户

三个操作的对比

and_then 接收一个返回 optional<U> 的函数,用于链式串联多个可能失败的操作

transform 接收一个返回普通值 U 的函数,用于变换成功值(类似 map)

or_else 接收一个返回 optional<T> 的函数,用于兜底处理失败情况

注意:std::expected 也有完全相同的三个操作(and_then / transform / or_else),用法一致。这意味着 optional 和 expected 可以用同样的链式风格编程,心智负担极低。

五、std::flat_map / std::flat_set —— 扁平容器

传统的 std::map 和 std::set 底层是红黑树,每个节点单独分配内存,缓存不友好。C++23 引入了扁平版本,底层用连续数组存储,牺牲插入/删除时间换取大幅提升的遍历和查找性能。

#include <flat_map>
#include <flat_set>
#include <print>

std::flat_map<std::string, int> scores;

// API 和 std::map 完全一致
scores["Alice"] = 95;
scores["Bob"] = 87;
scores.emplace("Charlie", 92);

// 遍历(内存连续,缓存命中率高,快得多)
for (const auto& [name, score] : scores) {
    std::println("{}: {}", name, score);
}

// 查找:O(log n) 二分查找,但常数因子远小于红黑树
auto it = scores.find("Alice");
if (it != scores.end()) {
    std::println("Found: {}", it->second);
}

// flat_set 同理
std::flat_set<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
std::println("Contains 5: {}", nums.contains(5));
特性std::map (红黑树)std::flat_map (有序数组)
底层结构红黑树(节点离散分配)两个有序 vector(key 数组 + value 数组)
查找O(log n)O(log n)(二分查找,但缓存友好)
遍历O(n),缓存不友好O(n),缓存友好,快 5-10 倍
插入/删除O(log n)O(n)(需要移动元素)
内存开销每节点 3 指针 + 颜色紧凑连续存储
适用场景频繁插入删除读多写少、数据量不大、需要遍历
实战建议:配置表、字典查找、枚举映射等"一次性构建、频繁查询"的场景,flat_map 比 map 快数倍。但如果需要频繁中间插入/删除,仍应使用 std::map。

六、Deducing this —— 显式对象参数

C++23 最具创新性的语言特性之一。允许在成员函数的第一个参数位置写 this,编译器会自动推导对象的 cv 限定符、值类别(lvalue/rvalue)。

6.1 消灭 CRTP

// ---- C++20 之前:CRTP(奇异递归模板模式)----
template<typename Derived>
struct Base {
    void interface() {
        static_cast<Derived*>(this)->impl();  // 丑陋的强制转换
    }
};

struct Concrete : Base<Concrete> {
    void impl() { std::println("Concrete::impl"); }
};

// ---- C++23:Deducing this,干净利落 ----
struct Base {
    template<typename Self>
    void interface(this Self& self) {  // Self 自动推导为 Concrete
        self.impl();  // 直接调用,无需转换
    }
};

struct Concrete : Base {
    void impl() { std::println("Concrete::impl"); }
};

6.2 一个函数替代四个重载

// ---- C++20:要写 4 个重载来覆盖所有 cv/ref 组合 ----
struct Widget {
    void process() &             { println("lvalue"); }
    void process() const &    { println("const lvalue"); }
    void process() &&            { println("rvalue"); }
    void process() const &&   { println("const rvalue"); }
};

// ---- C++23:一个模板函数搞定 ----
struct Widget {
    template<typename Self>
    void process(this Self&& self) {
        if constexpr (std::is_const_v<std::remove_reference_t<Self>>) {
            println("const ");
        }
        if constexpr (std::is_lvalue_reference_v<Self>) {
            println("lvalue");
        } else {
            println("rvalue");
        }
    }
};

6.3 Lambda 递归

// C++20:Lambda 不能递归,必须用 std::function 或 Y组合子
// C++23:传 this 参数,Lambda 可以直接递归!

auto factorial = [](this auto& self, int n) -> int {
    return n <= 1 ? 1 : n * self(n - 1);
};

std::println("5! = {}", factorial(5));  // 120

// 更复杂的例子:递归遍历 JSON 树
auto print_json = [](this auto& self, const Json& j, int indent = 0) -> void {
    if (j.is_object()) {
        for (const auto& [k, v] : j.items()) {
            std::println("{:>{}}{}:", "", indent, k);
            self(v, indent + 2);  // 递归调用!
        }
    } else if (j.is_array()) {
        for (const auto& elem : j) {
            self(elem, indent + 2);
        }
    } else {
        std::println("{:>{}}{}", "", indent, j.dump());
    }
};

七、std::mdspan —— 多维跨度

C++20 的 std::span 是一维数组视图的利器,但不能处理多维数据。C++23 引入 std::mdspan(多维 span),终于可以优雅地操作矩阵、张量了。

#include <mdspan>
#include <print>

// 一维数组存储的 3x4 矩阵
int data[] = {
    1,  2,  3,  4,
    5,  6,  7,  8,
    9, 10, 11, 12
};

// 创建 3x4 的二维视图(不拷贝数据!)
std::mdspan mat(data, 3, 4);

// 用多维下标访问(C++23 多维 a[i, j] 语法)
std::println("mat[1, 2] = {}", mat[1, 2]);  // 第2行第3列 = 7

// 遍历二维矩阵
for (size_t i = 0; i < mat.extent(0); i++) {
    for (size_t j = 0; j < mat.extent(1); j++) {
        std::print("{:4d}", mat[i, j]);
    }
    std::println();
}

// 三维张量也行
float tensor_data[24];
std::mdspan tensor(tensor_data, 2, 3, 4);  // 2x3x4 三维张量
tensor[1, 2, 3] = 3.14f;

// 支持非连续布局(行优先 / 列优先 / 自定义步长)
using layout_right = std::layout_right;  // 行优先(C 风格,默认)
using layout_left  = std::layout_left;   // 列优先(Fortran 风格)
应用场景:图像处理(HxWxC)、科学计算(矩阵运算)、深度学习(张量操作)。mdspan 零拷贝、零分配,是替代裸指针和手算下标的完美方案。

八、std::generator —— 协程生成器

C++20 引入了协程,但标准库没有提供现成的生成器类型,你得自己写或者用第三方库(如 cppcoro)。C++23 终于提供了 std::generator。

#include <generator>
#include <print>

// 生成斐波那契数列
std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

// 使用
int main() {
    // 取前 10 个斐波那契数
    int count = 0;
    for (int val : fibonacci()) {
        std::print("{} ", val);
        if (++count >= 10) break;
    }
    std::println();
    // 输出: 0 1 1 2 3 5 8 13 21 34
}

// 生成器可以嵌套引用(递归 yield)
std::generator<int> flatten_tree(const Node* node) {
    if (!node) co_return;

    co_yield node->value;
    co_yield std::ranges::elements_of(flatten_tree(node->left));
    co_yield std::ranges::elements_of(flatten_tree(node->right));
}

九、std::ranges::to —— 容器转换

C++20 的 Ranges 管道很优雅,但最后想把结果存到容器里时,只能写循环或用 std::vector<...>(begin, end)。C++23 的 ranges::to 一行搞定。

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

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // C++20:管道操作后,手动构造容器
    std::vector<int> evens;
    std::ranges::copy(nums | std::views::filter([](int n) { return n % 2 == 0; }),
                        std::back_inserter(evens));

    // C++23:ranges::to 一行搞定
    auto evens2 = nums | std::views::filter([](int n) { return n % 2 == 0; })
                     | std::ranges::to<std::vector>();

    // 转成其他容器类型
    auto even_set = nums | std::views::filter([](int n) { return n % 2 == 0; })
                       | std::ranges::to<std::set>();

    // 链式 transform + to
    std::vector<std::string> names = {"alice", "bob", "charlie"};
    auto upper = names | std::views::transform([](const std::string& s) {
                           std::string result;
                           for (char c : s) result += std::toupper(c);
                           return result;
                       })
                     | std::ranges::to<std::vector>();
    // upper = {"ALICE", "BOB", "CHARLIE"}
}

十、if consteval —— 编译期判断

C++20 引入了 std::is_constant_evaluated(),但写起来啰嗦且容易误用。C++23 提供了更直观的 if consteval 语法。

// C++20:std::is_constant_evaluated()
constexpr double sqrt_approx(double x) {
    if (std::is_constant_evaluated()) {
        // 编译期:用简单的牛顿迭代法
        double guess = x / 2;
        for (int i = 0; i < 20; i++) {
            guess = (guess + x / guess) / 2;
        }
        return guess;
    } else {
        // 运行期:调用标准库的 sqrt
        return std::sqrt(x);
    }
}

// C++23:if consteval 更清晰
constexpr double sqrt_approx(double x) {
    if consteval {
        // 编译期路径
        double guess = x / 2;
        for (int i = 0; i < 20; i++) {
            guess = (guess + x / guess) / 2;
        }
        return guess;
    } else {
        // 运行期路径
        return std::sqrt(x);
    }
}

constexpr double compile_time_val = sqrt_approx(2.0);  // 编译期计算
double runtime_val = sqrt_approx(2.0);            // 运行期调用 std::sqrt

十一、多维下标运算符 a[1, 2]

C++23 允许 operator[] 接收多个参数,终于可以写 matrix[1, 2] 而不是 matrix(1, 2) 或 matrix[1][2]。

struct Matrix {
    std::vector<double> data;
    int rows, cols;

    double& operator[](size_t i, size_t j) {       // C++23 多维下标
        return data[i * cols + j];
    }
    const double& operator[](size_t i, size_t j) const {
        return data[i * cols + j];
    }
};

Matrix m(3, 4);
m[1, 2] = 3.14;       // 多维下标,清晰简洁
std::println("{}", m[1, 2]);

十二、static operator() / static operator[]

C++23 允许将 operator() 和 operator[] 声明为 static,适用于不需要访问 this 的场景,减少开销。

struct Adder {
    static int operator()(int a, int b) {  // 不需要 this 指针
        return a + b;
    }
};

Adder add;
std::println("{}", add(3, 4));  // 7

// 好处:编译器不需要传递 this 指针,生成的代码更高效
// 特别适合用作 STL 算法的谓词
std::vector<int> nums = {3, 1, 4, 1, 5};
std::sort(nums.begin(), nums.end(), Adder{});  // 无状态比较器

十三、std::move_only_function

std::function 要求目标可拷贝,这在包装只移动类型(如 unique_ptr、协程句柄)时很麻烦。C++23 引入 move_only_function 解决这个问题。

#include <functional>
#include <memory>

// std::function 拷贝语义,无法持有只移动对象
// std::move_only_function 移动语义,可以!

std::move_only_function<int()> task;

// 可以捕获 unique_ptr
auto ptr = std::make_unique<int>(42);
task = [p = std::move(ptr)]() { return *p; };

std::println("{}", task());  // 42

// 适合异步回调、协程回调等场景
auto callback = [promise = std::move(some_promise)]() mutable {
    promise.set_value(100);
};

十四、std::stacktrace —— 堆栈跟踪

终于!C++ 标准库内置了堆栈跟踪功能,不再需要依赖 Boost.Stacktrace 或平台 API。

#include <stacktrace>
#include <print>

void deep_function() {
    // 获取当前调用栈
    auto trace = std::stacktrace::current();

    // 打印堆栈
    std::println("{}", trace);

    // 也可以遍历每一帧
    for (const auto& entry : trace) {
        std::println("  {} at {}:{}",
            entry.description(),
            entry.source_file(),
            entry.source_line());
    }
}

void middle_function() { deep_function(); }
int  main()             { middle_function(); }

/*
输出:
 0# deep_function() at app.cpp:5
 1# middle_function() at app.cpp:15
 2# main() at app.cpp:16
...
*/

十五、其他实用特性合集

15.1 size_t 字面量后缀 uz / UZ

// C++23 前:0 是 int,可能导致隐式转换警告
for (size_t i = 0; i < vec.size(); i++) { ... }

// C++23:uz 后缀直接得到 size_t
for (size_t i = 0uz; i < vec.size(); i++) { ... }
auto n = 100uz;  // 类型为 size_t

15.2 #warning 预处理指令

// C++23 终于标准化了 #warning
#warning "This header is deprecated, use new_api.hpp instead"

// 以及 #elifdef / #elifndef
#ifdef DEBUG
#elifdef RELEASE
    // release 模式
#elifndef TESTING
    // 非 testing 模式
#endif

15.3 auto(x) / auto{x} 衰减拷贝

// C++23:auto(x) 对 x 做衰减拷贝
// 类似 C 语言的 "to decay or not to decay" 问题解决方案

std::vector<std::string> names = {"Alice", "Bob"};

// 避免悬空引用:auto(x) 安全拷贝
for (auto& name : names) {
    // 如果 names 可能被修改,用 auto(name) 拷贝一份安全的副本
    auto safe_copy = auto(name);  // decay-copy
    std::println("{}", safe_copy);
}

15.4 std::byteswap

#include <bit>

// 字节序转换,网络编程常用
uint32_t host_val = 0x12345678;
uint32_t net_val = std::byteswap(host_val);
std::println("{:08x}", net_val);  // 78563412

// 条件字节序转换
if constexpr (std::endian::native == std::endian::little) {
    val = std::byteswap(val);  // 小端转大端
}

15.5 range-based for 循环中的初始化语句

// C++23:range-based for 可以带初始化语句
for (auto& vec = get_data(); auto& item : vec) {
    std::println("{}", item);
}
// vec 的生命周期限于 for 循环,避免悬空引用

15.6 std::span 的 construction 改进

// C++23:span 可以从 array 构造,更灵活
std::span<const int> s = {1, 2, 3};  // 从初始化列表构造

15.7 constexpr 增强

// 更多 STL 函数变成 constexpr
constexpr bool is_sorted(std::vector<int> v) {
    std::sort(v.begin(), v.end());  // C++23: sort 是 constexpr!
    return std::is_sorted(v.begin(), v.end());
}

十六、总结与编译器支持

C++23 核心特性速查表

特性类别一句话描述实用度
std::print/println标准库终于可以直接格式化打印了★★★★★
std::expected标准库不用异常也能优雅处理错误★★★★★
optional 单子操作标准库and_then/transform 链式调用★★★★★
Deducing this语言消灭 CRTP,Lambda 可递归★★★★☆
std::flat_map/set标准库缓存友好的扁平容器★★★★☆
std::mdspan标准库多维数组视图★★★★☆
std::generator标准库协程生成器终于来了★★★★☆
ranges::to标准库Ranges 结果转容器★★★★☆
if consteval语言编译期判断更清晰★★★☆☆
多维下标 a[i,j]语言矩阵操作更自然★★★☆☆
static operator()语言无状态调用更高效★★★☆☆
move_only_function标准库可移动的 function★★★☆☆
std::stacktrace标准库标准堆栈跟踪★★★☆☆
uz 字面量语言size_t 后缀★★☆☆☆
#warning预处理编译期警告★★☆☆☆
编译器支持情况(截至 2026 年初)
· GCC 14+:支持大部分 C++23 特性(print、expected、optional 单子操作、flat_map、deducing this 等)
· Clang 18+:支持大部分语言特性,部分库特性需 libc++ 最新版
· MSVC 19.40+ (VS 2022 17.10+):支持大部分特性
· 编译时加 -std=c++23(GCC/Clang)或 /std:c++latest(MSVC)
· 建议:生产环境使用 GCC 14+ 或 Clang 18+ 以获得最佳 C++23 支持
学习建议
1. 立刻用起来:std::print/println、optional 单子操作、ranges::to 几乎零学习成本,立刻提升代码质量
2. 逐步引入:std::expected 替代部分异常场景,flat_map 替代读多写少的 map
3. 深入学习:Deducing this 是范式级改变,值得花时间掌握
4. 关注生态:C++23 的 std::generator 和 mdspan 会推动协程和科学计算生态发展

C++23 虽然没有 C++20 那么革命性,但它是 C++20 的完美补充——填补了 std::format 没有 print 的遗憾、提供了 expected 解决错误处理之争、用 deducing this 简化了 CRTP。这些改进让现代 C++ 的开发体验更接近 Python 和 Rust,同时保持了零开销抽象的核心优势。