C++23 新特性

作者:billy

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

前言

C++23 是 C++20 之后的又一个重要标准版本。如果说 C++20 是一次"大爆炸"(模块、协程、概念、范围),那么 C++23 则是对 C++20 的"完善与补全" ------它修复了 C++20 留下的诸多不便,同时带来了一批非常实用、能立刻提升日常编码幸福感的新特性,比如 std::printstd::expectedstd::flat_mapstd::mdspan、显式对象参数(Deducing this)等。

本文代码基于 C++23 标准。不同编译器支持进度不同:

  • GCC 14+ / Clang 18+-std=c++23-std=c++2b
  • MSVC (VS 2022 17.6+)/std:c++latest

文中部分标准库特性(如 std::flat_mapstd::generator)需要较新版本编译器才能完整体验。

下面这张思维导图概览了 C++23 的主要新特性(按"语言"和"标准库"两大类组织):

复制代码
C++23 新特性总览
|
+-- 语言特性 (Language)
|   +-- if consteval                    替代 is_constant_evaluated()
|   +-- 显式对象参数 (Deducing this)    this 作为第一个显式参数
|   +-- 多维下标 operator[]             a[i, j, k]
|   +-- static operator() / operator[]  静态调用/下标
|   +-- [[assume(x)]] 属性              编译期优化提示
|   +-- #elifdef / #elifndef            条件编译增强
|   +-- auto(x) 衰减拷贝                简洁的 decay-copy
|   +-- size_t 字面量后缀               100uz / 100z
|
+-- 标准库 (Library)
    +-- std::print / std::println       类型安全的格式化输出
    +-- std::expected                   期望值/错误(替代异常)
    +-- std::flat_map / flat_set        扁平容器(连续存储)
    +-- std::mdspan                     多维非拥有数组视图
    +-- std::generator                  协程生成器
    +-- Ranges 新视图                   enumerate/zip/chunk/slide...
    +-- std::optional 单子操作          and_then / transform / or_else
    +-- std::move_only_function         可移动闭包
    +-- <stacktrace>                    堆栈跟踪
    +-- <stdfloat>                      float16_t / float32_t ...
    +-- 小工具                          to_underlying / unreachable / byteswap

一. 语言特性

1. if consteval(编译期判断)

C++20 中我们用 std::is_constant_evaluated() 来判断当前是否处于常量求值上下文。C++23 引入 if consteval / if !consteval,语法更清晰,且能在常量上下文中保证分支是真正的"常量"。

if consteval 用于判断当前代码是否在编译期(常量表达式)中执行,从而让同一个函数能同时服务于编译期和运行期,各走各的分支。

cpp 复制代码
#include <cmath>
#include <cstdio>

// 同一个函数,编译期用泰勒展开,运行期用库函数
constexpr double my_sqrt(double x) {
    if consteval {
        // 编译期:不能用 std::sqrt(非 constexpr),用简化算法
        double guess = x / 2.0;
        for (int i = 0; i < 20; ++i)
            guess = (guess + x / guess) / 2.0;  // 牛顿迭代
        return guess;
    } else {
        // 运行期:直接调用高效的库函数
        return std::sqrt(x);
    }
}

int main() {
    constexpr double c = my_sqrt(2.0);   // 编译期计算
    double r = my_sqrt(2.0);             // 运行期计算
    std::printf("constexpr: %.6f\nruntime : %.6f\n", c, r);
}

if constevalstd::is_constant_evaluated() 的对比:

特性 std::is_constant_evaluated() if consteval
引入版本 C++20 C++23
语法 函数调用 + if 语句关键字
常量上下文保证 弱(分支内仍是运行期语义) 强(分支内是真正的常量上下文)
可读性 一般 更直观

2. 显式对象参数(Deducing this,推导 this)

这是 C++23 最重要的语言特性之一 。它允许把 this 指针作为函数的第一个显式参数写出,编译器会根据调用方式自动推导其值类别(const / 非 const / 左值 / 右值)。

Deducing this 的核心价值:用一个函数模板同时替代过去要写 2~4 份的 const/非 const、左值/右值重载,大幅消除样板代码;还能实现 CRTP 的简化、链式调用的完美转发。

cpp 复制代码
#include <iostream>
#include <string>
#include <utility>

struct StringBuilder {
    std::string data;

    // 过去要为 const/非const/左值/右值 写 4 个 append 重载,
    // 现在只需要一个!This 的值类别会自动推导。
    template <class Self>
    auto&& append(this Self&& self, std::string_view s) {
        self.data += s;
        return std::forward<Self>(self);   // 完美转发,支持链式调用
    }

    void print(this auto const& self) {    // 也可以用 auto
        std::cout << self.data << '\n';
    }
};

int main() {
    StringBuilder b;
    b.append("Hello, ").append("C++23 ").append("Deducing this!").print();
}

下面这张图说明 Deducing this 如何"以一抵四":

复制代码
   传统写法(4 个重载)                 Deducing this(1 个模板)
+-------------------------------+     +----------------------------------+
| void append(const& s);        |     | template <class Self>            |
| void append(& s);             | ==> | auto&& append(this Self&&, ...); |
| void append(const&& s) &;     |     |   // 编译器按调用形式推导 Self   |
| void append(&& s) &&;         |     +----------------------------------+
+-------------------------------+

Deducing this 还能简化 CRTP:

cpp 复制代码
// 过去:CRTP 需要把派生类作为模板参数,还要 static_cast
template <class Derived>
struct Base {
    void interface() { static_cast<Derived*>(this)->impl(); }
};
struct Foo : Base<Foo> { void impl() { /*...*/ } };

// C++23:用 Deducing this 直接转发,无需模板参数
struct Base {
    template <class Self>
    void interface(this Self&& self) { std::forward<Self>(self).impl(); }
};
struct Foo : Base { void impl() { /*...*/ } };  // 不再需要 Base<Foo>

3. 多维下标运算符 operator[]

C++20 及之前,operator[] 只能接受一个参数,要实现矩阵 m(i, j) 只能重载 operator()。C++23 允许 operator[] 接收多个参数 ,终于可以写出 m[i, j] 这样的语法了。

多维下标运算符让矩阵、张量、多维数组等类型的访问语法更自然:a[i, j, k] 直接对应数学/科学计算中的多维索引。

cpp 复制代码
#include <array>
#include <cstddef>
#include <iostream>
#include <vector>

class Matrix {
    std::vector<double> data_;
    std::size_t cols_;
public:
    Matrix(std::size_t rows, std::size_t cols)
        : data_(rows * cols, 0.0), cols_(cols) {}

    // C++23:多维下标,一行搞定
    double& operator[](std::size_t r, std::size_t c) {
        return data_[r * cols_ + c];
    }
    const double& operator[](std::size_t r, std::size_t c) const {
        return data_[r * cols_ + c];
    }
};

int main() {
    Matrix m(3, 3);
    m[1, 2] = 3.14;          // C++23:自然的多维索引
    m[0, 0] = m[1, 2] + 1;
    std::cout << "m[1,2] = " << m[1, 2] << '\n';
    std::cout << "m[0,0] = " << m[0, 0] << '\n';
}

下标运算符的演进对比:

复制代码
C++20 及之前                    C++23
-------------------------------------------------
m(1, 2)   // 只能用 operator()   m[1, 2]    // operator[] 多参数
m.at(1,2) // 自定义成员函数      m[1, 2, 3] // 任意维度

4. static operator()static operator[]

C++23 允许把 operator()operator[] 声明为 static。这意味着调用它们时不需要 this 对象------适合那些不依赖对象状态的函数对象/下标访问,既省内存也更快。

静态调用运算符让"无状态的函数对象"调用时无需绑定对象实例,既消除了多余的 this 开销,又能像普通函数一样使用。

cpp 复制代码
#include <iostream>

struct Adder {
    // static:不依赖任何对象状态,直接 Adder{}(a, b) 或 Adder::operator()(a,b)
    static int operator()(int a, int b) { return a + b; }
};

struct Indexer {
    static int operator[](int i) { return i * i; }  // 平方查表
};

int main() {
    std::cout << Adder{}(3, 4) << '\n';          // 7
    std::cout << Adder::operator()(3, 4) << '\n'; // 7,无需实例
    std::cout << Indexer::operator[](5) << '\n';  // 25
}

5. [[assume(x)]] 属性(编译期假设)

[[assume(x)]] 告诉编译器"表达式 x 在此处一定为真",编译器据此进行优化。它比 if (x) {} 更彻底------编译器不会生成任何检查代码,完全信任你的假设。

[[assume(x)]] 是给编译器的"信任承诺":声明某条件恒为真,编译器据此做更激进的优化(如消除分支、向量化),但要由开发者保证假设成立,否则是未定义行为。

cpp 复制代码
#include <cstdint>

int compute(int x) {
    [[assume(x > 0)]];       // 承诺 x 恒为正:编译器可省去符号检查/分支
    if (x > 0) return x * 2; // 此分支大概率被优化掉
    return -1;
}

// 典型应用:循环边界,帮助向量化
void scale(int* a, int n) {
    [[assume(n > 0)]];
    for (int i = 0; i < n; ++i)
        a[i] *= 2;
}
复制代码
           普通分支检查              [[assume()]] 优化后
      +--------------------+     +--------------------+
      | if (x > 0) {...}   | ==> |  直接执行 {...}    |  <- 无条件判断指令
      |   生成 cmp+jcc     |     |  (编译器信任)      |
      +--------------------+     +--------------------+

6. #elifdef / #elifndef(条件编译增强)

C++23 给预处理指令增加了 #elifdef(else-if-defined)和 #elifndef(else-if-not-defined),省去层层嵌套的 #elif defined()

cpp 复制代码
// 过去的写法:层层嵌套,啰嗦
#if defined(WIN32)
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#elif defined(__APPLE__)
    #include <AvailabilityMacros.h>
#endif

// C++23:简洁直观
#ifdef WIN32
    #include <windows.h>
#elifdef __linux__
    #include <unistd.h>
#elifndef __APPLE__
    #error "未知平台"
#else
    #include <AvailabilityMacros.h>
#endif

7. auto(x)auto{x}(衰减拷贝)

C++23 让 auto(x)auto{x} 成为一种"显式衰减拷贝(decay-copy)"的语法。auto(x) 会按值拷贝并做数组->指针、函数->函数指针的衰减,非常适合在多线程中"捕获一个值的拷贝"或消除引用悬垂。

auto(x) 是一个简洁的"按值拷贝并衰减"表达式,常用于:多线程传参时强制拷贝、防止引用悬垂、把数组/函数衰减成指针。

cpp 复制代码
#include <iostream>
#include <thread>

void take_ptr(int* p, int n) {
    for (int i = 0; i < n; ++i) std::cout << p[i] << ' ';
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};

    // auto(arr) 把数组衰减成指针并拷贝一份(这里拷贝指针值)
    auto p = auto(arr);            // p 是 int*,指向数组
    take_ptr(p, 5);

    // 多线程:确保按值捕获,避免引用悬垂
    int local = 42;
    std::thread([x = auto(local)] { std::cout << "\nthread sees: " << x; }).join();
}

8. size_t 字面量后缀

C++23 增加了 z / uz 后缀,让字面量直接成为 std::size_t 类型,消除 intsize_t 比较时的有符号/无符号警告。

cpp 复制代码
#include <cstddef>
#include <vector>

int main() {
    std::vector<int> v(10);

    auto a = 100uz;     // std::size_t
    auto b = 100z;      // signed size_t (有符号)
    auto c = 100ULL;    // unsigned long long(C++11,不一定等于 size_t)

    for (auto i = 0uz; i < v.size(); ++i) {  // i 与 v.size() 类型一致,无警告
        v[i] = static_cast<int>(i);
    }
}

二. 标准库新特性

1. std::print / std::println(类型安全的格式化输出)

C++20 的 <format> 已经提供了类似 Python format 的能力,C++23 在此基础上新增了 <print> 头文件,提供 std::printstd::println直接输出到 stdout ,比 printf 类型安全,比 std::cout << 简洁得多。

std::print 结合 C++20 的 <format> 格式串,既类型安全又简洁,终于让 C++ 拥有了媲美 Python 的格式化输出体验。

cpp 复制代码
#include <print>
#include <string>

int main() {
    std::string name = "billy";
    int age = 18;
    double pi = 3.14159;

    // 格式串语法:{[arg-id][:[fill][align][sign][#][0][width][.prec][type]}
    std::println("Hello, {}! You are {} years old.", name, age);
    std::println("pi = {:.2f}", pi);            // 保留 2 位小数:3.14
    std::println("hex = {:#x}", 255);           // 十六进制:0xff
    std::println("[{:>10}]", "right");          // 右对齐,宽度 10
    std::println("[{:^10}]", "center");         // 居中
    std::println("[{:*<10}]", "left");          // 左对齐,* 填充
    std::println("{1} before {0}", "A", "B");   // 按索引:B before A
}

输出风格对比:

方式 写法 特点
printf printf("x=%d, y=%.2f\n", x, y); 不类型安全,易错
cout std::cout << "x=" << x << ", y=" << y << '\n'; 啰嗦
std::print std::println("x={}, y={:.2f}", x, y); 简洁 + 类型安全(推荐)

2. std::expected(期望值/错误处理)

std::expected<T, E> 是 C++23 的"错误处理利器":它要么包含一个正确的值 T,要么包含一个错误 E。它是对异常机制的有力补充------在不适合抛异常(如嵌入式、游戏引擎、性能敏感场景)时,用返回值优雅地传递错误。

std::expected 用"值或错误"的二选一模型,让函数可以把正常结果和错误都通过返回值传递,既避免异常的开销,又比错误码/指针更安全、更可组合。

cpp 复制代码
#include <expected>
#include <iostream>
#include <string>
#include <cmath>

// 返回成功的平方根,或错误信息
std::expected<double, std::string> safe_sqrt(double x) {
    if (x < 0)
        return std::unexpected("负数没有实数平方根");
    return std::sqrt(x);
}

int main() {
    auto r1 = safe_sqrt(16.0);
    if (r1.has_value())
        std::cout << "sqrt(16) = " << *r1 << '\n';      // 4

    auto r2 = safe_sqrt(-1.0);
    if (!r2)
        std::cout << "错误: " << r2.error() << '\n';   // 负数没有实数平方根

    // value_or:有值用值,否则用默认
    std::cout << "fallback = " << safe_sqrt(-4).value_or(0.0) << '\n'; // 0
}

std::expected 与异常、错误码的对比:

方式 优点 缺点
异常 throw 代码干净,自动传播 运行时开销,不适合实时/嵌入
错误码 int 零开销 易被忽略,无错误信息载体
expected 零开销 + 类型安全 + 可组合 需要显式检查

std::expected 还支持像 std::optional 一样的单子(monadic)操作and_then / or_else / transform),可以链式组合,避免层层 if

cpp 复制代码
auto result = safe_sqrt(-9.0)
    .or_else([](auto) { return std::expected<double,std::string>(0.0); })  // 失败给默认
    .transform([](double v) { return v + 1; });                            // 成功则变换
std::cout << "chained = " << *result << '\n';   // 1

3. std::flat_map / std::flat_set(扁平容器)

C++23 引入了 std::flat_mapstd::flat_set。它们与 std::map/std::set 的接口完全一致,但底层用连续数组存储 (默认是 std::vector),因此缓存友好、随机访问快、内存紧凑。

flat_map/flat_set 用连续内存实现有序关联容器:牺牲插入/删除速度(O(n)),换取更小的内存占用和更好的缓存局部性,特别适合"读多写少"或数据量小的场景。

cpp 复制代码
#include <flat_map>
#include <iostream>
#include <string>

int main() {
    std::flat_map<std::string, int> scores{
        {"billy", 95}, {"alice", 88}, {"carol", 91}
    };

    scores["dave"] = 76;          // 插入
    scores["alice"] = 90;         // 修改

    for (const auto& [name, score] : scores)   // 自动按 key 排序遍历
        std::println("{}: {}", name, score);
}

std::mapstd::flat_map 的对比:

复制代码
   std::map (红黑树,节点分散)         std::flat_map (连续数组)
                                          +--+--+--+--+
   [root]----> key,val   <--- 节点        |k1|k2|k3|k4|  连续内存
    /    \    key,val                     +--+--+--+--+
 [l]     [r]  key,val                     缓存命中率高,遍历飞快
   |       |  内存碎片大,缓存不友好       插入需搬移(O(n))
特性 std::map std::flat_map
底层结构 红黑树(节点) 连续数组(vector)
查找 O(log n),缓存不友好 O(log n) 二分,缓存友好
插入/删除 O(log n) O(n)(需搬移)
内存 碎片多,每节点开销大 紧凑,适合小数据/读多写少

4. std::mdspan(多维非拥有数组视图)

C++20 有 std::span(一维视图),C++23 进一步引入 std::mdspan------一个多维的、非拥有的数组视图。它非常适合科学计算、图像处理、机器学习中处理多维数据(矩阵、张量),零拷贝地"重新解释"一块连续内存的形状。

mdspan 是一个轻量的多维数组视图(不拥有数据),可以把一块连续内存按任意维度"重塑",零拷贝、零开销,是科学计算与图像处理的利器。

cpp 复制代码
#include <mdspan>
#include <iostream>
#include <vector>

int main() {
    // 一块连续内存:12 个元素
    std::vector<int> data(12);
    for (int i = 0; i < 12; ++i) data[i] = i;

    // 把它看成 3 行 4 列的二维矩阵(零拷贝)
    std::mdspan<int, std::dextents<std::size_t, 2>> mat(data.data(), 3, 4);

    std::println("mat[2, 3] = {}", mat[2, 3]);   // 第2行第3列 = 11
    std::println("rank = {}, extent(1) = {}", mat.rank(), mat.extent(1));

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

spanmdspan 的关系:

复制代码
   std::span<int>          :  一维视图,一段连续内存
   +--------------------+
   | 0 1 2 3 4 ... 11   |  <-- 一条线
   +--------------------+

   std::mdspan<int, 3x4>  :  二维视图,同一块内存重塑成矩阵
   +----+----+----+----+
   | 0  | 1  | 2  | 3  |
   +----+----+----+----+
   | 4  | 5  | 6  | 7  |   <-- 同一块内存,不同"形状"
   +----+----+----+----+
   | 8  | 9  | 10 | 11 |
   +----+----+----+----+
   (数据不拷贝,只换视角)

5. std::generator(协程生成器)

在 C++20 文章里我们手写了一个 Generator 协程类型(几十行样板)。C++23 直接在标准库提供了 std::generator------一个同步的、可迭代的协程生成器,从此写"惰性序列"变得轻而易举。

std::generator 让你像写普通函数一样用 co_yield 生成一个惰性序列,然后在 for 循环中迭代它,无需手写复杂的 promise_type 样板。

cpp 复制代码
#include <generator>
#include <iostream>

// 生成器协程:惰性产出斐波那契数列前 n 项
std::generator<int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;            // 产出一个值,暂停
        int t = a; a = b; b = t + b;
    }
}

int main() {
    for (int x : fibonacci(10))   // 直接用范围 for 迭代!
        std::print("{} ", x);
    std::println();
}

C++20(手写 Generator)vs C++23(std::generator):

复制代码
C++20:  自己实现 promise_type / handle / resume() ......  约 40 行样板代码
                |
                v
C++23:  std::generator<int> fib() { co_yield ...; }  一行搞定 + range-for

6. Ranges 新增视图(enumerate / zip / chunk / slide / adjacent / stride / join_with

C++23 给 Ranges 库补充了一大批高频实用视图,几乎覆盖了日常数据处理的全部需求。这些视图都是惰性求值、可组合的。

C++23 的 Ranges 新视图让数据流的转换变得极其优雅:enumerate 带索引、zip 配对、chunk/slide 分块滑动、adjacent 取相邻元素、stride 跨步、join_with 拼接------全程惰性、零中间容器。

cpp 复制代码
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> langs = {"C", "C++", "Rust", "Go"};

    // 1) views::enumerate ------ 带索引遍历(告别 size_t i = 0 写法)
    std::println("enumerate:");
    for (auto [idx, name] : std::views::enumerate(langs))
        std::println("  [{}] {}", idx, name);

    // 2) views::zip ------ 把多个范围"拉链式"配对
    std::vector scores = {95, 88, 91, 76};
    std::println("zip:");
    for (auto [name, score] : std::views::zip(langs, scores))
        std::println("  {} : {}", name, score);

    // 3) views::chunk ------ 固定大小分块
    std::vector<int> nums{1,2,3,4,5,6,7};
    std::println("chunk(3):");
    for (auto c : nums | std::views::chunk(3)) {
        for (int x : c) std::print("{} ", x);
        std::println();
    }

    // 4) views::slide ------ 滑动窗口
    std::println("slide(3) on 1..7:");
    for (auto w : nums | std::views::slide(3)) {
        for (int x : w) std::print("{} ", x);
        std::println();
    }

    // 5) views::adjacent ------ 取相邻 N 个(类似 slide,窗口大小编译期固定)
    std::println("adjacent<2>:");
    for (auto [a, b] : nums | std::views::adjacent<2>)
        std::print("({},{}) ", a, b);
    std::println();

    // 6) views::stride ------ 按步长采样
    std::println("stride(2):");
    for (int x : nums | std::views::stride(2))
        std::print("{} ", x);
    std::println();

    // 7) views::join_with ------ 把嵌套范围用分隔符拼接
    std::vector<std::string> words{"Hello", "C++", "23"};
    std::println("join_with('-'):");
    for (char ch : words | std::views::join_with('-'))
        std::print("{}", ch);
    std::println();
}

各视图功能一图速览:

复制代码
原始:     [A][B][C][D]            原始:  [1,2,3,4,5,6,7]

enumerate:[(0,A)(1,B)(2,C)(3,D)]  chunk(3):  [[1,2,3][4,5,6][7]]
zip(A,B): [(A,1)(B,2)(C,3)]       slide(3):  [[1,2,3][2,3,4][3,4,5]...]
adjacent2:[(A,B)(B,C)(C,D)]       stride(2): [1,3,5,7]

7. std::optional 的单子操作(and_then / transform / or_else

C++23 给 std::optional 增加了三个链式操作,让我们告别层层嵌套的 if (opt.has_value()) 判断,像流水线一样组合"可能失败"的操作。

optional 的单子操作让你以链式调用的方式串联多个"可能返回空"的步骤:有值就继续处理,无值就自动短路,代码扁平、可读、无嵌套。

cpp 复制代码
#include <iostream>
#include <optional>
#include <string>

std::optional<int> parse_int(const std::string& s) {
    try { return std::stoi(s); }
    catch (...) { return std::nullopt; }
}

std::optional<double> safe_sqrt(int x) {
    return x >= 0 ? std::optional<double>(std::sqrt(x)) : std::nullopt;
}

int main() {
    std::string input = "16";

    // 传统写法:层层 if 嵌套
    auto i = parse_int(input);
    if (i) {
        auto s = safe_sqrt(*i);
        if (s) std::println("传统: {}", *s);
    }

    // C++23 链式:扁平、优雅
    auto result = parse_int(input)
        .and_then(safe_sqrt)                 // 有值则开方
        .transform([](double v){ return v*2; }) // 有值则翻倍
        .or_else([]{ return std::optional<double>(0.0); }); // 空则给默认

    std::println("链式: {}", result.value());  // 8
}

单子操作的数据流向:

复制代码
parse_int("16")  ==>  optional(16)
   | and_then(safe_sqrt)        有值? 开方 -> optional(4)
   | transform(*2)              有值? 翻倍 -> optional(8)
   | or_else(默认 0.0)          空?   填默认 -> optional(8)
   v
optional(8)       (任一环节为空,后续 transform 自动短路)

8. std::move_only_function(仅可移动的函数包装器)

std::function 必须可拷贝,因此无法包装仅可移动的对象 (如含 unique_ptr 的 lambda、packaged_task)。C++23 的 std::move_only_function 解除了这一限制------它只可移动,能包装任意可调用对象,包括仅移动类型。

std::move_only_functionstd::function 的"仅移动"版本,可以包装持有 unique_ptr、协程等不可拷贝资源的可调用对象,填补了 std::function 的空白。

cpp 复制代码
#include <functional>
#include <memory>
#include <iostream>

int main() {
    auto p = std::make_unique<int>(42);

    // std::function 拷贝 lambda,但 unique_ptr 不可拷贝 -> 编译失败!
    // std::function<int()> f = [up = std::move(p)] { return *up; };

    // std::move_only_function 只移动,完美支持!
    std::move_only_function<int()> f = [up = std::move(p)] { return *up; };
    std::println("value = {}", f());

    auto f2 = std::move(f);   // 只能移动,不能拷贝
    std::println("moved = {}", f2());
}
包装器 可拷贝 可移动 能包装仅移动对象
std::function
std::move_only_function

9. <stacktrace>(堆栈跟踪)

C++ 终于有了标准库的堆栈跟踪 !当程序崩溃或抛异常时,可以打印调用栈,定位问题位置。<stacktrace> 提供了 std::stacktracestd::stacktrace_entry

<stacktrace> 让 C++ 原生支持获取和打印调用栈信息,调试崩溃和异常时无需再依赖平台相关的 API,跨平台、标准化。

cpp 复制代码
#include <stacktrace>
#include <iostream>
#include <stdexcept>

void deep_call() {
    // 获取当前调用栈并打印
    std::cerr << "=== 当前调用栈 ===\n";
    for (const auto& entry : std::stacktrace::current()) {
        std::cerr << "  " << entry << '\n';   // 文件名、行号、函数名
    }
    throw std::runtime_error("出错了!");
}

void middle() { deep_call(); }
void top()    { middle(); }

int main() {
    try {
        top();
    } catch (const std::exception& e) {
        std::cerr << "捕获异常: " << e.what() << '\n';
        // 打印捕获点的栈
        std::cerr << std::stacktrace::current();
    }
}

编译提示:GCC 需要链接 -lstdc++_exp,Clang/libc++ 需要 -lc++experimental

10. <stdfloat>(标准浮点类型别名)

C++23 的 <stdfloat> 提供了固定宽度的浮点类型别名 ,如 std::float16_tstd::float32_tstd::float64_tstd::bfloat16_t 等。这保证了"用几个字节"的精确语义,对机器学习、图形、跨平台数值计算非常重要。

cpp 复制代码
#include <stdfloat>
#include <iostream>
#include <cmath>

int main() {
    std::float32_t  a = 3.14f32;   // 精确 32 位浮点
    std::float64_t  b = 2.718281828f64;  // 精确 64 位浮点
    std::bfloat16_t bf = 1.0bf16;  // 脑浮点(AI 常用)

    std::println("float32: {:.6}", static_cast<double>(a));
    std::println("float64: {:.10}", b);
}

支持情况依赖硬件与编译器(如 float16_t 需要 ARM/较新 x86 支持)。

11. 实用小工具(to_underlying / unreachable / byteswap

C++23 还有一批"小而美"的工具函数,解决日常开发的高频痛点:

std::to_underlying ------ 枚举转底层类型

cpp 复制代码
#include <utility>
#include <iostream>

enum class Color : int { Red = 1, Green = 2, Blue = 4 };

int main() {
    Color c = Color::Green;
    // 过去:static_cast<int>(c) ------ 啰嗦且易错
    int v = std::to_underlying(c);   // C++23:优雅、安全
    std::println("value = {}", v);   // 2
}

std::unreachable ------ 标记不可达代码

std::unreachable 明确告诉编译器"这行代码永远不会执行",帮助优化;若运行时真的到达,则是未定义行为(通常用于 switch 已覆盖所有枚举值的 default 分支)。

cpp 复制代码
#include <utility>
#include <stdexcept>

int color_weight(Color c) {
    switch (c) {
        case Color::Red:   return 10;
        case Color::Green: return 20;
        case Color::Blue:  return 30;
    }
    std::unreachable();   // 上面已覆盖所有情况,理论上不会到达
}

std::byteswap ------ 字节序翻转

处理网络字节序(大端)与主机字节序(小端)转换时非常方便:

cpp 复制代码
#include <bit>
#include <cstdint>
#include <iostream>

int main() {
    std::uint16_t host = 0x0102;            // 小端机器上内存: 02 01
    std::uint16_t net  = std::byteswap(host); // 翻转 -> 02 01 -> 0x0201
    std::println("host = {:#06x}, after byteswap = {:#06x}", host, net);

    // 检测当前是否小端(C++20 已有 std::endian)
    if constexpr (std::endian::native == std::endian::little)
        std::println("本机为小端序");
}

字节翻转示意:

复制代码
   uint16_t 0x0102          byteswap 后
   +----+----+              +----+----+
   | 02 | 01 |  (小端)  ->  | 01 | 02 |  (大端)
   +----+----+              +----+----+
    低字节 在前               高字节 在前

三. 其他值得关注的特性速览

除了上面详细讲解的,C++23 还有许多值得一提的改进,这里列出供参考:

特性 说明
constexpr 扩展 更多的标准库函数(如部分 <cmath>)变为 constexpr,可在编译期使用
std::span 推导增强 可从数组/容器更方便地推导 span 的维度
std::basic_string::resize_and_overwrite 高效重设字符串大小并自定义填充,避免多余初始化
std::start_lifetime_as 显式启动对象生命周期,优化类型双关(punning)
std::is_implicit_lifetime 判断类型是否为隐式生命周期类型
std::reference_constructs_from_temporary 类型特征:引用是否绑定到临时对象(检测悬垂引用)
std::out_ptr / std::inout_ptr 与 C 风格输出指针 API(如 int**)无缝对接
范围 for 的别名 允许 for (auto [i, x] : ...) 等带初始化(配合 enumerate)
#warning 预处理指令 编译时输出警告信息(标准化)
std::format 范围支持 格式化容器时自动展开
字符集改进 支持 \u{...} 命名字符等

四. 总结

C++23 虽然没有 C++20 那样"震撼"的四大特性(模块/协程/概念/范围),但它把 C++20 的地基夯实、把常用工具补齐,是一份非常"实用主义"的标准:

  • 语言层面if consteval、Deducing this、多维 operator[][[assume]] 等,让代码更简洁、更高效、更易优化。
  • 标准库层面std::printstd::expectedstd::flat_mapstd::mdspanstd::generator、Ranges 新视图等,直接提升了日常编码的幸福感。

下面这张流程图概括了如何在不同场景下选择 C++23 的新特性:

复制代码
需要"值或错误"返回?     ==> std::expected
需要格式化输出?         ==> std::print / std::println
处理二维/多维数据?      ==> std::mdspan / 多维 operator[]
有序映射,读多写少?      ==> std::flat_map
惰性生成序列?           ==> std::generator
链式处理"可能失败"步骤? ==> optional.and_then / transform
消除 const/左值/右值重载? ==> Deducing this
编译期 vs 运行期分支?    ==> if consteval
调试崩溃定位?           ==> <stacktrace>
枚举转整数?             ==> std::to_underlying

学习建议:

  1. 先从 std::printstd::expectedstd::to_underlying 等小特性用起,这些几乎可以立刻融入日常代码,收益高、风险低。
  2. 再逐步掌握 Ranges 新视图、std::generator、Deducing this,这些会显著改变你的代码组织方式。
  3. std::mdspanstd::flat_map 等适合在特定领域(科学计算、性能敏感)深入实践。

参考资料与进一步阅读:


如果觉得本文对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!下一篇我们继续探索 C++ 的更多精彩内容。

相关推荐
BerryS3N3 天前
C++23新特性在CLion中的实战体验:从语法糖到生产力提升
前端·c++23
石山代码10 天前
C++23 新特性在 CLion 中的实战体验
开发语言·c++·c++23
华科大胡子12 天前
C++23 新特性在 CLion 中的实战体验:从语法到工具链的完整指南
c++23
2603_9552797013 天前
在 CMakeLists.txt 中设置 C++23 标准
java·linux·c++23
2603_9552797015 天前
C++23新特性在CLion中的实战体验
前端·javascript·c++23
lqqjuly2 个月前
C++ 完整知识体系—从基础语法到现代 C++23 的系统性总结
c++·c++23
ComputerInBook2 个月前
C++ 23 相比 C++ 20 新增之特征
开发语言·算法·c++23
混迹中的咸鱼2 个月前
C++ 23种设计模式
设计模式·c++23
c++之路3 个月前
C++23概述
java·c++·c++23