C++23 新特性总结
简介
本文简要介绍了一下 C++23 中标准库部分新增的特性. 为了代码简短, 本文中默认使用了using namespace std;
, 实际工作中不推荐这样做.
另外为了更美观的打印, 使用了fmt 库, fmt::
命名空间的函数均为其库函数.
关于环境
- 本文中一部分代码是在 Microsoft Visual Studio 2022 Preview 版本上运行的. 其中开启的编译选项为
/std:c++latest
: 支持当前最新的标准/experimental:module
: 支持 C++ Module
- 另一部分代码是在Compiler Explorer上运行的.
- Compiler Explorer 网站支持运行 GCC, Clang 的编译和运行, 不支持 MSVC 的运行, 仅仅支持编译
- 对于一些完全没有编译器支持的功能, 则只是简单的介绍概念, 无可运行的代码.
核心语言层面的改进
Deducing this
语法:
等价于:
该功能可以方便重载运算符
可以改写为:
可以参考我的这篇文章: C++23 新特性教程: 如何使用 Deducing this 实现更优代码.
其他参考资料:
- C++ Weekly - Ep 326 - C++23’s Deducing
this
- C++23’s Deducing this: what it is, why it is, how to use it
- Daily bit(e) of C++ | Explicit object parameter (a.k.a. deducing this)
if consteval
语法:
语义: 如果语句是在常量表达式上下文中执行的, 那么执行 A, 否则执行 B.
示例:
从上面的例子中可以看到, 相同的函数调用
g(1)
, 在编译时运行会得到2
, 在运行时会得到42
.
多维数组下标(Multidimensional Subscript Operator)
lambada 表达式增加调用属性(Attribute)
在 C++23 以前, lambda 表达式可以有属性. 如下:
在 C++23 中, lambda 的调用可以有属性:
进一步参考:
size_t
字面量后缀
已有的数字字面量后缀: U
, L
, UL
, LL
, ULL
.
新增的数字字面量后缀:
z
或Z
表示 有符号版本的std::size_t
uz
或UZ
表示std::size_t
auto(x)
: decay copy
当前的auto
关键字用于类型推导
C++23 中新增的auto(x)
,auto {x}
用来创建一个prvalue
的副本.
样例:
增加更多编译宏 #elifdef
, #elifndef
, #warning
已有的的预处理指令:
#ifdef ID
是#if defined(ID)
的缩写#ifndef ID
是#if !defined(ID)
的缩写
C++23 中新增的预处理指令:
#elifdef ID
是#elif defined(ID)
的缩写#elifndef ID
是#elif !defined(IDENTIFIER)
的缩写
#warning
指令用于生成编译器警告信息.
#warning "This is a warning message"
标记不可达代码(std::unreachable()
)
std::unreachable()
常被用于标记不可达代码. 如果代码执行到这里, 就会产生未定义的行为.
何谓"未定义行为"? 未定义行为是指程序在运行时可能产生任何结果, 包括崩溃, 输出错误, 或者其他不可预测的行为. 取决于编译器的实现. 更多相关内容可以参考: C++ 基础概念: 未定义行为(Undefined Behavior)
查看汇编结果
可以看到优化结果是直接返回了i
的值. 因为编译器认为default
分支是不可达的. 这种函数如果遇到了[1-3]之外的输入则会导致不可预测的结果.
运行结果
假设(assume
)
C++23 之前,不同编译器有不同的方式来标记假设. 例如:
__builtin_assume
(Clang)__assume
(MSVC and ICC)if (expr) {} else { __builtin_unreachable(); }
(GCC)
C++23 统一了这些标记, 引入了std::assume
函数.
这个标记用来告诉编译器一些前提条件, 以便优化代码.
未加修饰符号
编译生成
加assume
之后:
编译生成
可以看出整个异常处理代码被优化掉了.
进一步参考:
Unicode 已命名字符转义
C++23 之前, 字符转义只能用于 ASCII 字符.
C++23 引入了已命名字符转义
example
行拼接符('\'
)前的空白字符
在 C++23 以前, 如果行拼接符\
后有空白字符, 则会出现未定义的行为.
- Clang & GCC: 会忽略空白字符并警告, 输出
1
- MSVC: 不会忽略空白字符, 输出
2
C++23 中, 行拼接符后的空白字符会被忽略.
注意: 如果要复现请你主动给\
后面加一个空格, 直接拷贝当前示例代码不一定会包含空格(这个不受我控制).
标准库部分
字符串格式化改进
C++23 支持在std::print()
以及std::println()
中格式化字符串.
以下是一个简单的例子:
目前没有编译器支持. 如果需要使用可以使用fmt
库.
标准库模块(import std)
C++23 引入了标准库模块(Module), 这是一个新的模块化标准库的方式, 它将标准库分解为多个模块, 每个模块都有自己的接口和实现, 这样可以减少编译时间, 提高构建速度.
import std
import std
导入的模块(Module)包括:
- C++ 标准库的所有模块, 比如
vector
,string
,cout
等 - C++ 中关于 C 语言的包装, 比如
fopen()
将会输出:
import std.compat
目前没有编译器支持.
除了std
之外会加上全局命名空间的符号.
示例代码:
basic_string(_view)::contains()
检查字符串中是否含有待查找串.
Example:
禁止从 nullptr
构造 string(_view)
在 C++20 及之前的版本中, 以下代码是合法的:
但是在运行时会有未定义行为.
C++23 中禁止了从nullptr
构建字符串.
但是还是可以从空指针构建字符串:
basic_string
新增 resize_and_overwrite(count, op)
- 若
count <= size()
, 删除多余的元素 - 若
count > size()
, 添加count - size()
个默认初始化的元素- 触发
r = op(data(), count)
- 触发
erase(begin() + r, end())
- 触发
考虑一个将字符串pattern
重复count
次的函数:
这样的实现不是最优的, 因为:
- 写入
count
个null
- 更新
size
并检查潜在的resize
count
次
std::optional
的链式调用
C++23 中新增了一些支持链式调用的函数:
transform(F)
: 如果*this
有值, 则返回F
的结果, 否则返回空的optional
and_then(F)
: 如果*this
有值, 则返回F
的结果, 否则返回空的optional
or_else(F)
: 如果*this
有值, 则返回*this
, 否则返回F
的结果
Stacktrace
库
定义在 <stacktrace>
头文件中.
允许获取和处理堆栈跟踪.
样例代码:
g++ 编译时需要加 -lstdc++_libbacktrace
选项. 如果 gcc 版本大于等于 14, 选项改为-lstdc++exp
样例输出:
在异常中增加堆栈信息
很多语言都支持在出现异常的时候打印堆栈, 更容易定位出错位置, 方便调试. C++23 中也支持这个功能.
样例代码:
进一步阅读:
Ranges
库的变化
ranges::starts_with()
/ ranges::ends_with()
检查一个range
的开头(或结尾)是否与另一个range
匹配.
样例代码:
ranges::shift_left()
/ ranges::shift_right()
将一个range
的元素向左或向右移动.
样例代码:
ranges::to()
将一个range
内的元素转储到一个容器.
样例代码:
ranges::split()
使用views::split()
将一个字符串分割成多个子串.
ranges::find_last()
系列
查找一个range
中的最后一个元素.
ranges::find_last()
: 如果匹配一个指定元素ranges::find_last_if()
: 如果给定的谓词返回true
ranges::find_last_if_not()
: 如果给定的谓词返回false
返回一个子range
的迭代器, 一直到范围结束. 如没有找到则返回{last, last}
样例代码:
ranges::contains()
/ ranges::contains_subrange()
ranges::contains()
: 检查是否含有一个元素ranges::contains_subrange()
: 检查是否含有一个range
样例代码:
一些folding
算法
新增了如下算法:
ranges::fold_left()
ranges::fold_left_first()
ranges::fold_right()
ranges::fold_right_last()
ranges::fold_left_with_iter()
ranges::fold_left_first_with_iter()
样例代码:
Views
库的变化
views::zip
views::zip
是一个新的视图, 它可以将多个视图合并成一个视图.
样例代码:
view::zip_transform
views::zip_transform
是一个新的视图, 它可以将多个视图的元素应用一个操作符, 生成一个新的视图.
样例代码:
views::adjacent
views::adjacent
是一个新的视图, 它可以将一个视图的元素组合成一个元组.
views::adjacent: A view with each element a tuple of references to N adjacent elements from the original view
views::adjacent_transform
将操作施于相邻的N
个元素, 并将结果存储在新的视图中.
样例代码:
views::pairwise
& views::pairwise_transform
这两个是工具函数:
views::pairwise
=views::adjacent<2>
views::pairwise_transform
=views::adjacent_transform<2>
样例代码:
views::slide
views::slide
是一个新的视图, 它可以将一个视图的元素组合成一个元组.
与views::adjacent
相似, 但是窗口大小是运行时参数.
样例代码:
views::chunk
创建一个新的视图, 每 N 个元素组成一个元组.
样例代码:
views::chunk_by
创建一个新的视图, 但是每个元组的元素是通过一个谓词来决定的.
样例代码:
views::join_with
使用给定的分隔符连接一个视图的元素. views::join_with(): Joins elements of a range using a given separator 样例代码:
views::stride
返回一个视图, 其中的元素是原视图的等间隔的子集.
样例代码:
views::repeat
重复一个元素无数次, 或者指定次数.
views::cartesian_product
返回表示 n 个给定范围的笛卡尔积的元组的视图.
样例代码:
views::as_rvalue
一个表示底层range
的视图, 但是其元素是右值.
std::expected
定义在头文件 <expected>
中.
expected<T, E>
包含两个值:
- 一个类型为
T
的值, 期望的值 - 一个类型为
E
的值, 错误值 保证不会为空.unexpected()
用于创建一个意外的值.
样例代码:
成员函数:
has_value()
: 如果含有值, 返回true
, 否则返回false
value()
: 返回引用到包含的值, 如果没有值, 抛出bad_expected_access
error()
: 返回指向错误的引用
链式调用
更详细内容可以参考: 解读 C++23 std::expected 函数式写法
std::move_only_function<>
在 C++23 之前, 这个例子会失败:
C++23:
std::spanstream
定义在头文件 <spanstream>
中. 允许在外部缓冲区上使用流操作.
样例代码:
输出
std::byteswap()
定义在头文件<bit>
中. 是交换整数类型字节的标准方法.
样例代码:
输出
std::to_underlying()
定义在头文件<utility>
中. 作用是将一个枚举转换为其基础类型, 与static_cast<std::underlying_type_t<E>>(enum_value)
等效.
样例代码:
std::flat_(multi)map
& std::flat_(multi)set
目前为止没有编译器支持. 如果需要使用可以使用boost
库.
std::flat_map
和 std::flat_multimap
定义在 <flat_map>
头文件中.
提供类似于 std::map
的接口, 但是不支持重复的键, 并且键是有序的.
键的查询很快, 因为键是有序的.
内部存储是一个连续性的容器, 比如 std::vector
或者 std::deque
. key
和value
分别存储在不同的容器上.
std::flat_set
和 std::flat_multiset
定义在 <flat_set>
头文件中.
内部的键存储在一个连续的有序的容器中.
std::mdspan
目前为止没有编译器支持.
定义在 <mdspan>
头文件中.
std::mdspan
: 一个多维数组的视图, 是对std::span
(C++20)的多维扩展. 支持不同的布局策略.std::submdspan
: 一个对现有mdspan
的子集的视图(切片)
样例代码:
std::generator
定义在 <generator>
头文件中. 定义了一个标准的协程生成器.
样例代码:
关联容器异构删除
关联容器已经存在异构查找, C++23 中新增了异构删除和提取, 也就是增加了一个erase(K&&)
和 extract(K&&)
. 用来提升性能.
参考链接
后记
C++23 中的新特性还有很多, 本文只是列举了一部分. 有些特性还没有得到编译器的支持. 本文只是一个概览, 详细的特性还需要查阅官方文档.
Tags: