作者:billy
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
前言
C++23 是 C++20 之后的又一个重要标准版本。如果说 C++20 是一次"大爆炸"(模块、协程、概念、范围),那么 C++23 则是对 C++20 的"完善与补全" ------它修复了 C++20 留下的诸多不便,同时带来了一批非常实用、能立刻提升日常编码幸福感的新特性,比如 std::print、std::expected、std::flat_map、std::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_map、std::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 consteval 与 std::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 类型,消除 int 与 size_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::print 和 std::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_map 和 std::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::map 与 std::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();
}
}
span 与 mdspan 的关系:
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_function 是 std::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::stacktrace 和 std::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_t、std::float32_t、std::float64_t、std::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::print、std::expected、std::flat_map、std::mdspan、std::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
学习建议:
- 先从
std::print、std::expected、std::to_underlying等小特性用起,这些几乎可以立刻融入日常代码,收益高、风险低。 - 再逐步掌握 Ranges 新视图、
std::generator、Deducing this,这些会显著改变你的代码组织方式。 std::mdspan、std::flat_map等适合在特定领域(科学计算、性能敏感)深入实践。
参考资料与进一步阅读:
如果觉得本文对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!下一篇我们继续探索 C++ 的更多精彩内容。