C++23 新特性概览之 标准库
介绍C++23标准库更新.
文章目录
- C++ 23 新特性概览之 标准库
- 简介
- 字符串格式化改进
- 标准库模块
- `basic_string(_view)::contains()`
- 禁止从 `nullptr` 构造 `string(_view)`
- `basic_string::resize_and_overwrite(count, op)`
- `std::optional` 的链式调用
- `Stacktrace` 库
- `Ranges` 库的变化
- `Views` 库的变化
- `expected`
- `std::move_only_function<>`
- `std::spanstream`
- `std::byteswap()`
- `std::to_underlying()`
- `std::flat_(multi)map` & `std::flat_(multi)set`
- `std::mdspan`
- `std::generator`
- 关联容器异构删除
- 参考
- 后记
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的运行, 仅仅支持编译
- 对于一些完全没有编译器支持的功能, 则只是简单的介绍概念, 无可运行的代码.
字符串格式化改进
C++23支持在std::print()
以及std::println()
中格式化字符串.
以下是一个简单的例子:
std::string name{ "C++23" };
// Old-style cout/format() pattern
std::cout << std::format("Hello {}!\n", name);
// C++23 print()
std::print("Hello {}!\n", name);
// C++23 println()
std::println("Hello {}!", name);
目前没有编译器支持. 如果需要使用可以使用fmt
库.
vector<pair<int, int>> v { {1, 2}, { 3, 4 } };
println("{}", v); // [(1, 2), (3, 4)]
set<pair<int, int>> s { {1, 2}, { 3, 4 } };
println("{}", s); // {(1, 2), (3, 4)}
map<int, int> m { {1, 2}, { 3, 4 } };
println("{}", m); // {1: 2, 3: 4}
vector<string> strings{ "Hello", "World!\t2023" };
println("{}", strings); // ["Hello", "World!\t2023"]
println("{::}", strings); // [Hello, World! 2023]
vector<vector<int>> vv{ {11, 22}, { 33, 44, 55 } };
println("{}", vv); // [[11, 22], [33, 44, 55]]
println("{:n}", vv); // [11, 22], [33, 44, 55]
println("{:n:n}", vv); // 11, 22, 33, 44, 55
println("{:n:n:*^4}", vv); // *11*, *22*, *33*, *44*, *55*
标准库模块
C++23引入了标准库模块(Module),这是一个新的模块化标准库的方式,它将标准库分解为多个模块,每个模块都有自己的接口和实现, 这样可以减少编译时间,提高构建速度。
import std
import std
导入的模块(Module)包括:
- C++ 标准库的所有模块, 比如
vector
,string
,cout
等 - C++ 中关于C语言的包装, 比如
fopen()
import std;
using namespace std;
int main() {
cout << "Hello from cout!" << endl;
println("Hello from println!");
printf("%s\n", "Hello from cstdlib!");
return 0;
}
将会输出:
Hello from cout!
Hello from std::println!
Hello from cstdlib!
import std.compat
目前没有编译器支持.
除了std
之外会加上全局命名空间的符号.
示例代码:
import std.compat;
int main() {
std::cout << "Hello from std::cout" << std::endl;
std::println("Hello from std::println!");
std::printf("Hello from std::printf\n");
::printf("Hello from ::printf\n");
return 0;
}
basic_string(_view)::contains()
检查字符串中是否含有待查找串.
Example:
#include <fmt/core.h>
#include <string>
int main() {
std::string haystack{"Hello World!"};
fmt::println("{}", haystack.contains("World")); // true
fmt::println("{}", haystack.contains('!')); // true
using namespace std::string_view_literals;
fmt::println("{}", haystack.contains("Hello"sv)); // true
}
禁止从 nullptr
构造 string(_view)
在C++20及之前的版本中, 以下代码是合法的:
// C++20 and before (undefined behavior)
std::string s { nullptr };
但是在运行时会有未定义行为.
C++23中禁止了从nullptr
构建字符串.
// C++23 (compile-time error)
std::string s { nullptr };
但是还是可以从空指针构建字符串:
const char* p = nullptr;
std::string s{ p };
assert(s.empty()); // runtime assertion
basic_string::resize_and_overwrite(count, op)
- 若
count <= size()
, 删除多余的元素 - 若
count > size()
, 添加count - size()
个默认初始化的元素- 触发
r = op(data(), count)
- 触发
erase(begin() + r, end())
- 触发
考虑一个将字符串pattern
重复count
次的函数:
std::string GeneratePattern(const std::string& pattern, size_t count) {
std::string result;
result.reserve(pattern.size() * count);
for (size_t i = 0; i < count; i++) {
result.append(pattern);
}
return result;
}
这样的实现不是最优的, 因为:
- 写入
count
个null
- 更新
size
并检查潜在的resize
count
次
std::string GeneratePattern(const std::string& pattern, size_t count) {
std::string result;
const auto step = pattern.size();
// GOOD: No initialization
result.resize_and_overwrite(step * count, [&](char* buf, size_t n) {
for (size_t i = 0; i < count; i++) {
// GOOD: No bookkeeping
memcpy(buf + i * step, pattern.data(), step);
}
return step * count;
});
return result;
}
std::optional
的链式调用
C++23中新增了一些支持链式调用的函数:
transform(F)
: 如果*this
有值, 则返回F
的结果, 否则返回空的optional
and_then(F)
: 如果*this
有值, 则返回F
的结果, 否则返回空的optional
or_else(F)
: 如果*this
有值, 则返回*this
, 否则返回F
的结果
#include <iostream>
#include <optional>
#include <string>
#include <vector>
using namespace std;
optional<int> Parse(const string& s) {
try {
return stoi(s);
} catch (...) {
return {};
}
}
int main() {
vector<string> inputs{"12", "nan"};
for (auto& s : inputs) {
auto result =
Parse(s)
.and_then([](int value) -> optional<int> { return value * 2; })
.transform([](int value) { return to_string(value); })
.or_else([] { return optional<string>{"No Integer Found"}; });
cout << *result << endl;
}
}
Stacktrace
库
定义在 <stacktrace>
头文件中.
允许获取和处理堆栈跟踪.
样例代码:
auto trace { std::stacktrace::current() };
std::cout << std::to_string(trace) << std::endl;
g++ 编译时需要加 -lstdc++_libbacktrace
选项. 如果gcc版本大于等于14, 选项改为-lstdc++exp
样例输出:
0# bar() at /workspaces/cmake-project-2024/src/cpp23/stacktrace.cpp:6
1# foo() at /workspaces/cmake-project-2024/src/cpp23/stacktrace.cpp:14
2# main at /workspaces/cmake-project-2024/src/cpp23/stacktrace.cpp:16
3# __libc_start_call_main at :0
4# __libc_start_main_impl at :0
5# _start at :0
6#
在异常中增加堆栈信息
很多语言都支持在出现异常的时候打印堆栈, 更容易定位出错位置, 方便调试. C++23中也支持这个功能.
样例代码:
#include <exception>
#include <iostream>
#include <stacktrace>
class Exception : public std::exception {
public:
explicit Exception(std::string message,
std::stacktrace st = std::stacktrace::current())
: message_{std::move(message)}, stack_{std::move(st)} {}
[[nodiscard]] const char* what() const noexcept override {
return message_.c_str();
}
[[nodiscard]] const std::stacktrace& trace() const noexcept { return stack_; }
private:
std::string message_;
std::stacktrace stack_;
};
void bar() { throw Exception{"exception at bar()."}; }
void foo() { bar(); }
int main() {
try {
foo();
} catch (const Exception& e) {
std::cout << "Exception caught: " << e.what() << std::endl;
std::cout << "Stacktrace: \n" << std::to_string(e.trace()) << std::endl;
}
}
进一步阅读:
Ranges
库的变化
ranges::starts_with()
/ ranges::ends_with()
检查一个range
的开头(或结尾)是否与另一个range
匹配.
样例代码:
vector v1{11, 22, 33, 44};
vector v2{11, 22};
fmt::println("{}", ranges::starts_with(v1, v2)); // true
fmt::println("{}", ranges::ends_with(v1, v2)); // false
ranges::shift_left()
/ ranges::shift_right()
将一个range
的元素向左或向右移动.
样例代码:
vector<string> v{"a", "b", "c", "d", "e"};
ranges::shift_left(v, 2);
fmt::println("{}", v); // "c", "d", "e", "", ""
ranges::shift_right(v, 1);
fmt::println("{}", v); // "", "c", "d", "e", ""
ranges::to()
将一个range
内的元素转储到一个容器.
样例代码:
auto ints = std::views::iota(1, 5)
| std::views::transform([](const auto& v) { return v * 2; });
auto vec{ std::ranges::to<std::vector>(ints) };
std::print("{}", vec); // [2, 4, 6, 8]
// 从vector 转到set
auto set1{ std::ranges::to<std::set>(vec) };
// 用管道符从set<int> 转到 set<double>
auto set2{ vec | std::ranges::to<std::set<double>>() };
// 使用`from_range`构造函数从vector<int> 转到set<double>
std::set<double> set3{ std::from_range, vec };
ranges::split()
使用views::split()
将一个字符串分割成多个子串.
string text{"s1 s2 s3 s4 s5"};
auto words{text | views::split(' ') | views::transform([](const auto& v) {
return string{from_range, v};
}) |
ranges::to<vector>()};
fmt::println("words: {:n:?}", words); // words: "s1", "s2", "s3", "s4", "s5"
ranges::find_last()
系列
查找一个range
中的最后一个元素.
ranges::find_last()
: 如果匹配一个指定元素ranges::find_last_if()
: 如果给定的谓词返回true
ranges::find_last_if_not()
: 如果给定的谓词返回false
返回一个子range
的迭代器, 一直到范围结束. 如没有找到则返回{last, last}
样例代码:
vector v{1, 2, 3, 4, 5};
fmt::println("{}", ranges::find_last(v, 3)); // [3, 4, 5]
fmt::println("{}", ranges::find_last_if(
v, [](int i) { return i % 2 == 0; })); // [4, 5]
fmt::println("{}", ranges::find_last_if_not(
v, [](int i) { return i % 2 == 0; })); // [5]
ranges::contains()
/ ranges::contains_subrange()
ranges::contains()
: 检查是否含有一个元素ranges::contains_subrange()
: 检查是否含有一个range
样例代码:
std::vector v1{ 11, 22, 33, 44 };
std::vector v2{ 33, 44 };
std::println("{}", std::ranges::contains(v1, 22)); // true
std::println("{}", std::ranges::contains_subrange(v1, v2)); // true
一些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()
样例代码:
vector v{1, 2, 3, 4, 5};
fmt::println("{}", ranges::fold_left(v, 0, std::plus{})); // 15
fmt::println("{}", std::ranges::fold_left_first(v, std::multiplies<>()).value()); // 120
fmt::println("{}", ranges::fold_right(v, 1, std::multiplies{})); // 120
fmt::println("{}", ranges::fold_right_last(v, std::plus{}).value()); // 15
fmt::println("{}", ranges::fold_left_with_iter(v, 0, std::plus{}).value); // 15
fmt::println("{}", ranges::fold_left_first_with_iter(v, std::multiplies{}).value.value()); // 120
Views
库的变化
views::zip
views::zip
是一个新的视图, 它可以将多个视图合并成一个视图.
样例代码:
vector v1{1, 2, 3};
vector v2{'a', 'b', 'c'};
auto r1{views::zip(v1, v2)};
fmt::println("{}", r1); // [(1, 'a'), (2, 'b'), (3, 'c')]
view::zip_transform
views::zip_transform
是一个新的视图, 它可以将多个视图的元素应用一个操作符, 生成一个新的视图.
样例代码:
vector v1{1, 2, 3};
vector v2{4, 5, 6};
auto r2{views::zip_transform(multiplies(), v1, v2)};
fmt::println("{}", r2); // [4, 10, 18]
views::adjacent
views::adjacent
是一个新的视图, 它可以将一个视图的元素组合成一个元组.
views::adjacent: A view with each element a tuple of references to N adjacent elements from the original view
vector v{1, 2, 3, 4};
auto r3{v | views::adjacent<2>};
fmt::println("{}", r3); // [(1, 2), (2, 3), (3, 4)]
views::adjacent_transform
将操作施于相邻的N
个元素, 并将结果存储在新的视图中.
样例代码:
vector v{1, 2, 3, 4};
auto r4{v | views::adjacent_transform<2>(multiplies())};
fmt::println("{}", r4); // [2, 6, 12]
views::pairwise
& views::pairwise_transform
这两个是工具函数:
views::pairwise
=views::adjacent<2>
views::pairwise_transform
=views::adjacent_transform<2>
样例代码:
vector v2{1, 2, 3, 4};
auto r5{v2 | views::pairwise};
fmt::println("{}", r5); // [(1, 2), (2, 3), (3, 4)]
vector v3{3, 4, 5};
auto r6{v3 | views::pairwise_transform(plus())};
fmt::println("{}", r6); // [7, 9]
views::slide
views::slide
是一个新的视图, 它可以将一个视图的元素组合成一个元组.
与views::adjacent
相似, 但是窗口大小是运行时参数.
样例代码:
vector v{1, 2, 3, 4, 5};
auto r7{v | views::slide(2)};
fmt::println("{}", r7); // [[1, 2], [2, 3], [3, 4], [4, 5]]
views::chunk
创建一个新的视图, 每N个元素组成一个元组.
样例代码:
vector v { 1, 2, 3, 4, 5 };
auto r8 { v | views::chunk(2) };
fmt::println("{}", r8); // [[1, 2], [3, 4], [5]]
views::chunk_by
创建一个新的视图, 但是每个元组的元素是通过一个谓词来决定的.
样例代码:
vector v{1, 2, 2, 3, 0, 4, 5, 2};
auto r9{v | views::chunk_by(ranges::less_equal{})};
fmt::println("{}", r9); // {(1,2,2,3),(0,4,5),(2)}
views::join_with
使用给定的分隔符连接一个视图的元素.
views::join_with(): Joins elements of a range using a given separator
样例代码:
vector<string> vs{"hi", "you", "!"};
auto str{vs | views::join_with('\n')};
fmt::println("{}", str); // ['h', 'i', '\n', 'y', 'o', 'u', '\n', '!']
views::stride
返回一个视图, 其中的元素是原视图的等间隔的子集.
样例代码:
vector v { 1, 2, 3, 4, 5 };
auto r10 { v | views::stride(2) };
fmt::println("{}", r10); // {1, 3, 5}
views::repeat
重复一个元素无数次, 或者指定次数.
auto r11 { views::repeat(2, 3) }; // {2, 2, 2}
auto r12 { views::repeat(2) }; // {2, 2, 2, ... }
views::cartesian_product
返回表示n个给定范围的笛卡尔积的元组的视图.
样例代码:
vector v = { 1, 2 };
auto r13 { views::cartesian_product(v, v) };
fmt::println("{}", r13); // [(1, 1), (1, 2), (2, 1), (2, 2)]
views::as_rvalue
一个表示底层range
的视图, 但是其元素是右值.
vector words{"Hello"s, "World"s, "2023"s};
vector<string> mv;
ranges::copy(words | views::as_rvalue, back_inserter(mv));
fmt::println("{}", mv); // ["Hello", "World", "2023"]
expected
定义在头文件 <expected>
中.
expected<T, E>
包含两个值:
- 一个类型为
T
的值, 期望的值 - 一个类型为
E
的值, 错误值
保证不会为空.
unexpected()
用于创建一个意外的值.
样例代码:
expected<int, string> a { 21 };
expected<int, string> b { unexpected("Some error"s) };
成员函数:
has_value()
: 如果含有值, 返回true
, 否则返回false
value()
: 返回引用到包含的值, 如果没有值, 抛出bad_expected_access
error()
: 返回指向错误的引用
链式调用
#include <expected>
#include <iostream>
#include <string>
using namespace std;
expected<int, string> divide(int a, int b) {
if (b == 0) {
return unexpected<string>("Division by zero");
} else {
return a / b;
}
}
int main() {
auto result =
divide(12, 2)
.and_then([](int result) {
return divide(result, 2); // Further divide the result by 2
})
.or_else([](const string& error) {
std::cout << "Error: " << error << std::endl;
return expected<int, std::string>(
0); // Return a default value in case of error
});
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cout << "Error: " << result.error() << std::endl;
}
return 0;
}
std::move_only_function<>
在C++23之前, 这个例子会失败:
int Work(std::function<int()> f) {
return f();
}
std::cout << Work([p = std::make_unique<int>(42)] { return *p; });
“Attempting to reference a deleted function”
The copy ctor of the std::function tries to copy the lambda, which is not possible due to the captured unique_ptr
C++23:
#include <fmt/core.h>
#include <functional>
#include <iostream>
using namespace std;
int Process(std::move_only_function<int()> f) { return f(); }
int main() {
cout << Process([] { return 1; }) << endl; // 1
cout << Process([p = std::make_unique<int>(2)] { return *p; }) << endl; // 2
}
std::spanstream
定义在头文件 <spanstream>
中. 允许在外部缓冲区上使用流操作.
样例代码:
char text[] = "11 22";
int a, b;
ispanstream in{span<char>{text}};
in >> a >> b;
fmt::println("a: {} b: {}", a, b);
char buffer[32]{};
ospanstream out{span<char>{buffer}};
out << 22 << 11;
fmt::println("{}", buffer);
输出
a: 11 b: 22
2211
std::byteswap()
定义在头文件<bit>
中. 是交换整数类型字节的标准方法.
样例代码:
std::uint32_t a{ 0x12345678u };
fmt::println("{:x}", a);
std::uint32_t b{ std::byteswap(a) };
fmt::println("{:x}", b);
输出
12345678
78563412
std::to_underlying()
定义在头文件<utility>
中. 作用是将一个枚举转换为其基础类型, 与static_cast<std::underlying_type_t<E>>(enum_value)
等效.
样例代码:
#include <cassert>
#include <cstdint>
#include <iostream>
#include <utility>
using namespace std;
enum Options : uint32_t { Opt1 = 1, Opt2 = 2, Opt3 = 3 };
int main() {
Options r{Options::Opt1};
auto a{static_cast<underlying_type_t<Options>>(r)};
auto b{to_underlying(r)};
assert(a == b);
cout << a << endl;
cout << b << endl;
}
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_map<int, std::string> myMap;
myMap[2023] = "CppCon"s;
std::flat_map<int, std::string,
std::less<int>,
std::deque<int>,
std::deque<std::string>> myMap;
std::flat_set
和 std::flat_multiset
定义在 <flat_set>
头文件中.
内部的键存储在一个连续的有序的容器中.
std::flat_set<int> mySet;
std::mdspan
目前为止没有编译器支持.
定义在 <mdspan>
头文件中.
std::mdspan
: 一个多维数组的视图, 是对std::span
(C++20)的多维扩展.
支持不同的布局策略.std::submdspan
: 一个对现有mdspan
的子集的视图(切片)
样例代码:
int* data { /* ... */ };
// View data as contiguous memory representing 2 rows of 2 ints each
auto mySpan { std::mdspan(data, 2, 2) };
// Access data from mdspan
for (size_t i { 0 }; i < mySpan.extents().extent(0); ++i) {
for (size_t j { 0 }; j < mySpan.extents().extent(1); ++j) {
mySpan[i, j] = i * 1000 + j;
}
}
std::generator
定义在 <generator>
头文件中. 定义了一个标准的协程生成器.
目前为止没有编译器支持.
样例代码:
std::generator<int> getSequenceGenerator(int startValue, int numberOfValues) {
for (int i { startValue }; i < startValue + numberOfValues; ++i) {
// Yield a value to the caller, and suspend the coroutine.
co_yield i;
}
}
int main() {
auto gen { getSequenceGenerator(10, 5) };
for (const auto& value : gen) {
std::print("{} (Press enter for next value)", value);
std::cin.ignore();
}
}
关联容器异构删除
关联容器已经存在异构查找, C++23中新增了异构删除和提取, 也就是增加了一个erase(K&&)
和 extract(K&&)
. 用来提升性能.
参考
后记
C++23中的新特性还有很多, 本文只是列举了一部分. 有些特性还没有得到编译器的支持, 有些特性还在讨论中. 本文只是一个概览, 详细的特性还需要查阅官方文档.
这篇文章也会进行更新补充, 欢迎关注.
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)