Skip to content

C++23 新特性 ​

C++23 是一个 minor 版本,但含金量不低:std::expected、std::print、std::generator、flat 容器、多维 operator[]、「推导 this」(deducing this)都直接改变日常写法。它还把 C++20 协程和范围这两座「只给了机制没给工具」的框架补上了实用的库工具。

编译器支持情况见 Compiler support for C++23。系列总览见 现代 C++ 新特性汇总。

后续版本:C++26 已于 2025 年中完成特性冻结与委员会审定(反射、契约、std::execution 等进入标准),正式文本预计 2026 年内发布。

语言特性 ​

新语言功能特性测试宏 ​

每个新语言特性都有对应的 __cpp_XXX 宏(语言侧),可精确探测编译器支持:

cpp
#if __cpp_explicit_this_parameter >= 202110L
// 推导 this 可用
#endif

#if __has_cpp_attribute(assume)      // 属性类的用 __has_cpp_attribute 探测
// ...
#endif

显式对象形参(推导 this) ​

成员函数的第一个参数可以显式写 this(this 参数),类型由编译器按调用方式推导——一个签名同时覆盖 const/非 const、左值/右值四种版本:

cpp
struct Box {
    int v{};

    // 传统写法需要两份几乎相同的代码:
    //   int&  value()       { return v; }
    //   const int& value() const { return v; }

    // 推导 this:一份代码覆盖全部版本
    template<typename Self>
    auto&& value(this Self&& self) {
        return std::forward_like<Self>(self.v);   // self 是 Box& / const Box& / Box&& ...
    }
};
Box b;
const Box cb;
b.value() = 1;        // OK:int&
int x = cb.value();   // OK:const int&

另一个重要应用:递归 lambda(不再需要 std::function 自我引用的技巧):

cpp
auto factorial = [](this auto self, int n) -> long long {
    return n <= 1 ? 1 : n * self(n - 1);    // self 就是 lambda 自己
};
factorial(10);   // 3628800

原理:显式对象形参让 this 成为普通函数参数,按模板推导规则得到 Self = Box& 或 const Box&,成员访问通过 self. 隐式完成。

if consteval / if not consteval ​

编译期/运行期走不同实现,取代容易用错的 std::is_constant_evaluated():

cpp
constexpr double fast_inv_sqrt(double x) {
    if consteval {                       // 编译期求值中:只允许纯计算
        double y = x;
        for (int i = 0; i < 80; ++i)
            y = y * (1.5 - 0.5 * x * y * y);    // 牛顿迭代
        return y;
    } else {                             // 运行期:直接用硬件平方根
        return 1.0 / std::sqrt(x);
    }
}

为什么需要专门语法:if constexpr (std::is_constant_evaluated()) 是错的——在 if constexpr 的条件里求值,永远处于常量求值上下文,条件恒为 true,运行期分支被整个剪掉。if consteval 才能正确区分两种环境。

多维下标运算符 ​

operator[] 可以接受多个参数,多维访问不再需要 m[i][j] 或 at(i, j):

cpp
struct Matrix {
    double m[3][3]{};

    double& operator[](std::size_t r, std::size_t c) {       // 多维 operator[]
        return m[r][c];
    }
};

Matrix mat;
mat[1, 2] = 3.14;        // 直接按行列访问

// 注意坑:对没有多维 operator[] 的容器,v[1, 3] 走的仍是逗号运算符
// std::vector<int> v{0,1,2,3};
// v[1, 3] 等价 v[3] ——(下标里逗号表达式的行为已随之标记为弃用)

配套的库设施是 std::mdspan(见标准库部分)。

static operator() ​

函数调用运算符可以是 static 的:无 this 指针、无状态:

cpp
struct Plus {
    static constexpr int operator()(int a, int b) {   // 没有 this
        return a + b;
    }
};

Plus p;
p(2, 3);                    // 对象语法仍可用

std::accumulate(v.begin(), v.end(), 0, Plus{});    // 当谓词/运算器直接用

意义:比非静态版本少传一个 this;无捕获 lambda 生成的正是这类闭包类型,显式写出后,与要求「可默认构造」的泛型代码(C++20 concepts 的 default_initializable)配合更顺畅。

static operator[] ​

同理,下标运算符也可以 static,用于与对象状态无关的索引场景(查全局表、纯函数映射):

cpp
struct SineTable {
    // 表是编译期全局数据,operator[] 不需要对象状态
    static constexpr double operator[](std::size_t i) {
        return table[i];    // 假设 table 是静态常量
    }
    static inline double table[360]{ /* ... */ };
};

double s = SineTable{}[30];      // 无需构造有状态对象

auto(x):语言中的衰退复制 ​

auto(x) 显式制造一份「衰退拷贝」(decay copy):去引用、去顶层 const、数组退化成指针,得到值类型的临时副本:

cpp
std::string name = "task-data";

// 异步任务要一份自己的拷贝,写法一目了然:
std::thread t([msg = auto(name)] {          // 明确:按值拷贝一份,而非引用
    process(msg);
});

int x = 42;
int& r = x;
auto a = auto(r);       // int:不再是引用(auto a = r; 得到的也是 int,但意图不明显)
// auto(x) 的类型 = std::decay_t<decltype(x)>
// 花括号形式同样有效:auto{x}

与 T(x) 的区别:不需要知道 T 是什么,「把这个东西按值复制一份」有了统一写法;标准库内部(thread 参数、ranges)大量使用这个语义。

lambda 表达式上的属性 ​

属性可以出现在 lambda 形参列表之后,作用于调用运算符本身:

cpp
auto f = [](int x) [[nodiscard]] { return x * 2; };    // 丢弃返回值会告警
auto g = []() [[noreturn]] { throw std::runtime_error("fatal"); };
// 以前 [[nodiscard]] 无法套在 lambda 上,只能套在具名函数上

可选的扩展浮点类型 ​

<stdfloat> 提供定宽浮点类型(实现可选,硬件不支持可不提供):

cpp
#include <stdfloat>

std::float16_t   h;     // IEEE 半精度 16 位:GPU、深度学习推理
std::float32_t   f;     // 通常与 float 同表示(可能是别名)
std::float64_t   d;     // 通常与 double 同表示
std::float128_t  q;     // 四精度:高精度科学计算
std::bfloat16_t  bf;    // bfloat16:范围同 float、精度低,训练场景主流

动机:机器学习与 GPU 生态里 half/bfloat16 早已事实标准化,C++ 里却只能靠编译器扩展(__fp16、_Float16),现在统一进标准。

std::size_t 字面量的后缀 uz / z ​

uz/UZ 后缀直接得到 std::size_t 类型的字面量,消除循环里的有符号/无符号比较告警:

cpp
std::vector<int> v(100);

for (auto i = 0uz; i < v.size(); ++i)   // i 是 size_t,无符号比较告警消失
    v[i] = static_cast<int>(i);

// v[42uz] 这类下标写法也不再产生转换
// z 是有符号对应物(ptrdiff_t 类),uz 是无符号 size_t
auto n = 42z;
static_assert(std::is_signed_v<decltype(n)>);

#elifdef、#elifndef 与 #warning ​

预处理指令补全:

cpp
#if defined(NDEBUG)
    // release 路径
#elifndef NDEBUG
    // 没定义 NDEBUG 的调试路径
#endif

#ifndef FEATURE_X
#  warning "FEATURE_X not defined; falling back to defaults"
#endif

// 以前 #elif 只能写 defined(X),嵌套 #if 很啰嗦;#warning 提示构建配置问题

新属性 [[assume(表达式)]] ​

告诉编译器「此处该表达式恒为真」,不做求值、不生成代码,仅供优化:

cpp
int fast_div(int a, int n) {
    [[assume(n > 0)]];          // 不求值;编译器据此可以:
    return a / n;               //  省略除零/负数除法的防御分支、按正数优化
}
// 代价:若运行时假设不成立 -> 未定义行为,自己负责
// 与 assert 相反:assert 是"检查",assume 是"保证"

具名通用字符转义 ​

字符转义可以按码点名或带花括号的十六进制书写:

cpp
char alpha = '\N{GREEK SMALL LETTER ALPHA}';   // 具名转义(Unicode 标准名)
char zhong = '\u{4E2D}';                       // 花括号十六进制码点:'中'
// 好处:\u{...} 不再受"4 位还是 8 位"的写法歧义困扰,
//       \N{...} 自带文档含义,比裸码点可读

可移植源文件编码为 UTF-8 ​

C++23 起源文件的编码必须是 UTF-8(不再由实现自定义),跨平台编译含中文注释/字符串的代码不再因源码编码出问题(老 MSVC 默认 GBK 的年代结束)。

行拼合之前修剪空白 ​

反斜杠续行时,反斜杠后面的尾随空白/注释不再破坏拼合:

cpp
constexpr double total = 1.0 + 2.0 \
    + 3.0;
// 以前:行尾反斜杠后若有空格(部分编译器)续行失效;现在一律先修剪再拼合

标准库特性 ​

新的库功能特性测试宏 ​

库侧特性宏集中在 <version>,__cpp_lib_XXX 形式:

cpp
#include <version>
#ifdef __cpp_lib_expected
#  include <expected>      // 有 std::expected 才用
#endif
#ifdef __cpp_lib_print
    std::println("hello");
#endif

新的范围折叠算法 ​

std::ranges::fold_left 一族把折叠操作纳入范围算法:

cpp
#include <algorithm>

int sum  = std::ranges::fold_left(v, 0, std::plus{});         // ((0+v0)+v1)+v2
int prod = std::ranges::fold_left(v, 1, std::multiplies{});
std::string cat = std::ranges::fold_left(words, std::string{}, std::plus{});

// 家族成员:
//   fold_right:从右往左
//   fold_left_first / fold_right_last:用首/末元素当初值,空范围返回 optional
//   fold_left_with_result:折叠同时带回最终迭代器位置

对比手写循环:折叠意图显式,配合投影可写 fold_left(people, 0, std::plus{}, &Person::age)。

字符串格式化改进 ​

std::format 体系三处升级(其中 print 是日常影响最大的):

cpp
// 1. std::print / std::println:直接输出,告别 iostream 拼接
std::print("{} items, total {:.2f}\n", n, total);
std::println("done");                       // 自动换行
std::print(stderr, "error: {}", code);      // 指定文件流

// 2. 指针的调试格式:任何指针都能打印(0x 前缀地址),调试日志方便
int* p = &x;
std::println("p = {}", p);                  // p = 0x7ffd8c3b1a4c(无需转 uintptr_t)

// 3. 范围格式化:容器直接可打印
std::vector<int> v{1, 2, 3};
std::println("{}", v);                      // [1, 2, 3]
std::map<std::string, int> m{{"a", 1}};
std::println("{}", m);                      // {"a": 1}(格式细节实现略有差异)

「平铺」容器适配器 ​

flat_map / flat_set 等:接口与 map/set 一致,底层是排序的连续数组:

cpp
#include <flat_map>

std::flat_map<int, std::string> m;   // 底层默认是两个排序 vector
m[3] = "c";
m[1] = "a";
m[2] = "b";      // 查找:O(log n) 二分 + 连续内存,常数远小于红黑树

for (auto it = m.lower_bound(1); it != m.upper_bound(2); ++it)
    std::println("{}:{}", it->first, it->second);   // 有序遍历,行为同 map
对比项 map flat_map
查找 O(log n),指针跳转 O(log n) 二分,缓存友好(实际更快)
插入/删除 O(log n) O(n) 挪动数组
内存 每节点 3 指针+颜色开销 紧凑连续
迭代器稳定性 插入不失效 插入可能全失效

结论:读多写少、批量构建后只查的配置表/查找表,flat_map 几乎总是更快。

std::mdspan ​

多维数组的非拥有视图(「多维版 span」),配合多维 operator[]:

cpp
#include <mdspan>

std::vector<int> buf(12);

std::mdspan mat(buf.data(), 3, 4);    // 3 行 4 列的视图,不拷贝数据
mat[2, 3] = 42;                       // 多维下标,C++23 新语法
mat[0, 0] = 1;

// 定长多维:std::mdspan<int, std::extents<std::size_t, 3, 4>> 固定 3x4
// 还可选布局(layout_right 行主 / layout_left 列主)——接 BLAS、图像、深度学习张量

价值:把「一维 buffer + 手算下标」的裸代码变成类型安全的多维视图,零拷贝、零分配。

std::generator ​

协程的官方易用封装:std::generator<T> 就是一个可以 co_yield 的惰性序列:

cpp
#include <generator>

std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        a = std::exchange(b, a + b);
    }
}

for (int x : fibonacci() | std::views::take(10))
    std::print("{} ", x);     // 0 1 1 2 3 5 8 13 21 34
// 生成器按需产出一个消费一个,无限序列也不爆内存
// C++20 里手写的 60 行 Generator 模板,从此一行 using 换 std::generator

string 与 string_view 的 contains ​

cpp
std::string s = "hello.cpp";
std::string_view sv = s;

s.contains(".cpp");         // true:子串判断
sv.contains("hello");       // true
s.contains('c');            // 单字符重载
// 以前要写 s.find(x) != std::string::npos,可读性差且容易写反

禁止从 nullptr 构造 string_view ​

std::string_view 的 nullptr 构造被删除,从源头堵住未定义行为:

cpp
std::string_view sv = nullptr;    // C++23 起编译错误
// 以前:构造出 data()==nullptr 的 view,一旦读取就是 UB
// 修复思路:想要空 view 用默认构造或 "",想检查空指针用指针判空

resize_and_overwrite ​

先改大小再由你接管写入,跳过无意义的零填充:

cpp
std::string s;
s.resize_and_overwrite(64, [](char* p, std::size_t n) {
    // p 指向可用缓冲,n 是请求的容量:直接写入,返回实际保留长度
    auto res = std::format_to_n(p, n - 1, "{} items", 42);
    return static_cast<std::size_t>(res.size);
});
// 传统 resize(64) 会先填 64 个 '\0' 再被覆盖——纯浪费;序列化/读文件场景收益明显

std::optional 的单子式操作 ​

or_else / and_then / transform 把「层层判空」变成管道:

cpp
std::optional<std::string> label =
    find_user(id)                        // optional<User>
        .and_then(user_email)            // -> optional<string>,找不到用户则保持空
        .transform([](auto s) { return s + "@corp"; })
        .or_else([] { return std::optional{"unknown@corp"}; });

规则:and_then 的回调返回 optional(链式展平),transform 的回调返回普通值(自动包一层),or_else 在空时触发。std::expected(见下)同样有这一套。

栈踪迹(stacktrace)库 ​

<stacktrace> 拿调用栈,崩溃/诊断日志不再依赖平台 API:

cpp
#include <stacktrace>

void on_error() {
    std::cout << std::stacktrace::current() << '\n';   // 当前调用栈
    // 可只取最近几帧:std::stacktrace::current(1, 5)
}

// 也可以存起来慢慢用
auto trace = std::stacktrace::current();
if (trace) {                    // 至少有一帧
    for (const auto& frame : trace)
        std::cout << frame << '\n';    // 函数名、文件、行号
}

注意:需要调试信息(-g)才有函数名/行号;libstdc++ 需要额外链接(GCC 14+ 为 -lstdc++exp,更早版本为 -lstdc++_libbacktrace)。

新的范围算法 ​

cpp
bool has = std::ranges::contains(v, 42);            // 是否包含
auto it  = std::ranges::find_last(v, 7);            // 最后一个匹配
std::ranges::iota(v, 0);                            // 填充 0,1,2,...(算法版)
// std::ranges::generate_random(v, engine, dist);   // 向范围直接填充随机数
// 还有 fold 一族(单列一节)、find_last_if、shift 等

新的范围适配器(视图) ​

cpp
// enumerate:带下标遍历(不再需要手动维护下标变量)
for (auto [i, x] : v | std::views::enumerate)
    std::println("[{}] {}", i, x);

// zip:并行遍历多个序列
for (auto [name, score] : std::views::zip(names, scores))
    std::println("{}: {}", name, score);

// chunk / slide:分块与滑窗
for (auto&& c : v | std::views::chunk(3)) { /* 每 3 个一组 */ }
for (auto&& w : v | std::views::slide(2)) { /* 相邻对 (v[i], v[i+1]) */ }

// 常用的还有:
//   stride:跳步采样        cartesian_product:笛卡尔积
//   join_with:展开并插入分隔  chunk_by:按谓词分组
//   repeat:重复序列        as_rvalue:把元素按右值送入管道

对范围库的修改与对视图的修改 ​

  • ranges::to:把惰性管道物化回容器(写 pipeline 的最后一环):

    cpp
    auto out = v | std::views::filter(even)
                 | std::views::transform(square)
                 | std::ranges::to<std::vector>();       // 物化成 vector
    auto s   = words | std::views::join_with(' ')
                 | std::ranges::to<std::string>();       // 也能拼字符串
  • 视图可以拥有元素(从右值容器构造时接管数据,不再悬垂),管道中传右值容器是安全的

  • 适配器支持用 | 直接接在「容器生成表达式」后,成套组合性增强(views 侧的配套微调)

std::unreachable ​

标记「编译器认为到不了、真到了就是 UB」的代码位置:

cpp
#include <utility>

int to_code(Color c) {
    switch (c) {
    case Color::red:   return 1;
    case Color::green: return 2;
    case Color::blue:  return 3;
    }
    std::unreachable();    // 到这里 = UB;好处:
    // 1. 编译器知道 switch 已穷尽,省掉"落到 default"的分支
    // 2. 比注释 /* unreachable */ 有真实语义,也比 assert(false) 少生成代码
    // 3. UB 检测工具(UBSan)在误入时会立刻报告
}

std::expected ​

expected<T, E>:「要么是值 T,要么是错误 E」的返回类型,给异常受限环境一个正经的错误传递通道:

cpp
#include <expected>

enum class ParseError { empty, not_number };

std::expected<int, ParseError> parse(std::string_view sv) noexcept {
    if (sv.empty()) return std::unexpected{ParseError::empty};
    // ... 解析
    return 42;
}

auto r = parse("42");
if (r)                     // 有值
    use(*r);
else
    handle(r.error());     // r.error() 是 ParseError

// 与 optional 同款单子操作,链式编排不层层 if:
auto doubled = parse("21")
    .transform([](int x) { return x * 2; })        // 值存在才映射
    .value_or(0);                                   // 出错给 0
// 对比:用 optional 丢掉了"错在哪"的信息;用异常破坏 noexcept 与实时性
// expected 是两者之间的第三条路

std::move_only_function ​

std::function 的可移动版:只要求可调用物可移动(std::function 要求可拷贝),所以能装 lambda 捕获的 unique_ptr:

cpp
#include <functional>

std::move_only_function<int()> task =
    [cfg = std::make_unique<Config>()] {
        return cfg->run();
    };

// task 不可拷贝,但可以移动——正好是线程池任务的形态:
std::vector<std::move_only_function<void()>> queue;
queue.push_back([buf = std::move(owned_buffer)] { process(buf); });
// 出队执行时:auto t = std::move(queue.front()); t();

它还支持在类型里标注调用约定(move_only_function<int() const noexcept>),比 std::function 更精确。

std::spanstream ​

以 span<char> 为缓冲的流:零分配的 ostream/istream:

cpp
#include <spanstream>

char buf[64];

// 输出:写进自己的缓冲,不碰堆
std::ospanstream os{std::span<char>{buf}};
os << "value = " << 42 << " (0x" << std::hex << 42 << ")";
auto written = os.span().size();          // 实际写入字节数

// 输入:把内存当流解析
std::ispanstream is{std::span<const char>{buf, written}};
std::string tag; int n;
is >> tag >> n;                            // "value" 42
// 嵌入式/日志/协议场景:要"流的格式化能力"又不能分配内存

std::byteswap ​

字节序翻转,constexpr 可用:

cpp
#include <bit>

constexpr auto host = std::uint32_t{0x11223344};
constexpr auto be   = std::byteswap(host);      // 0x44332211

// 网络字节序、跨平台文件格式解析:
std::uint32_t to_network(std::uint32_t v) {
    if constexpr (std::endian::native == std::endian::little)
        return std::byteswap(v);          // 小端机器翻一下
    return v;
}
// 注意:byteswap 只是翻转,不感知"网络序";配合 std::endian(C++20)判断

std::to_underlying ​

枚举转底层整数,不再手写 static_cast<int>:

cpp
enum class Status : std::uint8_t { ok, retry, drop };

auto raw = std::to_underlying(Status::retry);   // 1,类型是底层类型 uint8_t
// 以前:static_cast<std::underlying_type_t<Status>>(s) 一长串
// 序列化、打日志、喂 C API 时干净多了

关联容器的异质擦除 ​

map/set 的 erase 支持异构 key(与 C++14 的异构查找、C++20 的异构 insert 构成闭环):

cpp
std::map<std::string, int, std::less<>> m;

m.erase(std::string_view{"stale"});   // 不构造临时 std::string
// 以前:m.erase(std::string("stale")) 必须先造一个 string
// 同样要求透明比较器(less<>)

小结 ​

C++23 的主线是「补齐易用性」:std::print 与格式化增强直接改善输出体验,expected 给错误处理提供了异常与 optional 之外的第三条路,generator/ranges::to/flat 容器把 C++20 框架的最后一公里走完,推导 this 消灭了成对的 const/非 const 成员函数样板。工具链方面,GCC 13+/Clang 17+ 已覆盖大部分库特性,按特性测试宏渐进采用即可。

系列其他篇目:现代 C++ 新特性汇总 · C++11 新特性 · C++14 新特性 · C++17 新特性 · C++20 新特性

最近更新

基于 VitePress 构建