C++20 新特性总结
简要总结
C++20 引入了四项非常大的更新, 分别是:
- 概念(Concepts). 用来简化模板编程, 强化表达能力. 并且使得出错原因更容易查找.
- 模块(Modules). 这是代码组织方面非常大的更新. 提供了新的方式来组织代码, 并且可以减少编译时间.
- 范围库(Ranges and Views). 轻量级的, 非拥有的范围库, 允许对数据进行各种操作.
- 协程(Coroutine). 多线程编程方面的一次重大更新.
本文将会对 C++20 的新特性做一个简要总结, 方便读者快速了解.
1. 三路比较运算符 <=>
C++20 引入了一个新的运算符 <=>
, 称为 “三路比较运算符” 或 “spaceship 运算符” . 这个运算符用于同时执行小于, 等于和大于的比较操作, 并返回一个特殊类型的值, 该类型表示两个对象之间的关系. 具体来说, 它会返回一个 std::strong_ordering
, std::weak_ordering
, 或者 std::partial_ordering
类型的对象, 分别对应强有序, 弱有序以及部分有序的情况.
- 当左侧小于右侧时, 返回一个小于零的值(通常是
std::strong_ordering::less
). - 当两侧相等时, 返回零(
std::strong_ordering::equal
). - 当左侧大于右侧时, 返回一个大于零的值(
std::strong_ordering::greater
).
示例代码
#include <compare>
#include <iostream>
struct Point {
int x, y;
auto operator<=>(const Point&) const = default;
};
int main() {
Point a{1, 2}, b{1, 2}, c{2, 3};
if (a <=> b == 0) {
std::cout << "a and b are equal\n";
}
if ((a <=> c) < 0) {
std::cout << "a is less than c\n";
}
return 0;
}
在这个示例中, Point
结构体通过默认的方式实现了三路比较运算符, 这意味着编译器会自动生成所有基于成员变量的比较逻辑.
进一步阅读: C++20 Spaceship 操作符 (‘<=>’): 现代 C++ 的比较利器
2. 函数参数支持占位符类型
C++20 支持在函数参数中使用auto
关键字作为占位符. 这一特性允许你编写更加通用和灵活的函数, 而不需要指定具体的参数类型.
#include <iostream>
auto add(auto a, auto b) { return a + b; }
int main() {
std::cout << "Integers: " << add(1, 2) << "\n"; // 输出3
std::cout << "Doubles: " << add(1.5, 2.5) << "\n"; // 输出4.0
return 0;
}
3. 概念和要求
-
概念(Concepts): 是用于描述模板参数类型必须满足的一组要求的实体. 它们可以用来约束模板参数, 确保只有满足特定条件的类型才能被用作模板参数.
-
要求(Requirements): 通常指模板参数需要满足的具体条件或行为, 比如支持某种操作符, 拥有特定成员函数等.
假设我们想要定义一个函数模板, 该模板只接受那些可以进行比较操作(例如<
)的类型. 在 C++20 之前, 这可能需要使用 SFINAE 或者static_assert
来实现, 但现在我们可以直接使用概念来简化这一过程.
#include <concepts>
#include <string>
// 定义一个名为Sortable的概念, 要求类型T支持小于运算符
template <typename T>
concept Sortable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
// 仅对Sortable类型的参数有效
void sortFunction(Sortable auto& container) {
// 假设这里实现了排序逻辑
}
struct MyType {
int value;
bool operator<(const MyType& other) const { return value < other.value; }
};
int main() {
int arr[] = {1, 3, 2};
sortFunction(arr); // 正确, int支持<运算
MyType myArr[] = {{1}, {3}, {2}};
sortFunction(myArr); // 正确, MyType也重载了<运算符
return 0;
}
关键点解析
-
概念定义: 通过
concept
关键字定义概念, 然后使用requires
子句列出类型必须满足的要求. 在这个例子中,Sortable
概念要求类型支持<
运算, 并且结果可以转换为bool
. -
应用概念: 在模板声明中使用概念作为约束, 如
Sortable auto&
, 这样就限制了传入的容器元素类型必须满足Sortable
概念.
使用概念不仅使代码更易于理解, 而且当传入不符合概念要求的类型时, 编译器能提供更有意义的错误消息, 从而大大提高了开发效率. 此外, 概念还支持复杂的组合和继承关系, 使得构建复杂的需求系统成为可能.
进一步阅读: C++20 Concepts 简介
4. ranges/views 库
C++20 引入了Ranges和Views, 它们是标准库的一部分, 旨在简化容器, 数组和其他序列数据的操作. 通过 Ranges 和 Views, 可以更直观地处理数据序列, 并支持函数式编程风格. 它们提供了强大的工具来创建, 组合以及操作数据流, 而无需手动迭代或复制数据.
Ranges是 C++20 中的一个新概念, 它扩展了 STL(标准模板库)的容器和算法, 使得操作序列更加直观和高效. 一个 Range 是一个可以遍历的数据序列, 它可以是有限的也可以是无限的. Ranges 的核心思想是将算法与容器解耦, 允许以声明式的方式定义数据处理流程.
Views是轻量级, 非拥有的范围, 它们提供了一种方式来查看原始数据的一种特定视角, 而不实际拥有数据. 这意味着 views 不会复制数据, 而是基于原始数据进行计算, 这使得它们非常高效且节省内存.
以下是一个使用 C++20 Ranges 和 Views 的例子, 展示了如何使用这些新特性来过滤和转换数据:
#include <fmt/ranges.h>
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> vec = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 使用views过滤出偶数, 并对每个元素加1
auto result = vec | std::views::filter([](int n) { return n % 2 == 0; }) |
std::views::transform([](int n) { return n * n; });
// 打印结果
fmt::println("{}", result);
// 输出: [0, 4, 16, 36, 64]
return 0;
}
关键点解析
-
管道操作符(
|
): Ranges 和 Views 支持使用管道操作符来链式调用各种视图操作, 如过滤(filter
), 转换(transform
)等, 使得代码更加简洁和易读. -
Filter 和 Transform: 在这个例子中, 我们首先使用
std::views::filter
来筛选出所有偶数, 然后使用std::views::transform
对每个筛选出的数字加 1. 这两个操作都是惰性求值的, 也就是说, 它们只有在真正需要时才会执行相应的计算. -
性能优势: 由于 views 不复制数据, 它们可以在处理大量数据时提供显著的性能优势. 此外, 由于它们的惰性求值特性, 只有当真正访问数据时才会进行计算, 这也进一步提高了效率.
进一步阅读: Modern C++ Ranges/View 库简介
5. std::span
std::span
是一个轻量级, 非拥有的视图, 用于表示连续内存序列(如数组, std::vector
等). 它不拥有数据, 而是提供对数据的引用, 因此避免了不必要的复制, 同时提供了对序列的安全访问.
主要特点:
- 非拥有:
std::span
不管理内存, 仅作为数据的视图. - 连续内存: 适用于连续内存的数据结构, 如数组,
std::vector
等. - 安全性: 支持边界检查, 避免越界访问.
- 灵活性: 可以表示固定大小或动态大小的序列.
#include <fmt/ranges.h>
#include <span>
#include <vector>
void print(std::span<int> span) { fmt::println("{}", span); }
int main() {
// 使用 std::span 查看数组
int arr[] = {1, 2, 3, 4, 5};
print(arr); // 输出: 1 2 3 4 5
// 使用 std::span 查看 vector
std::vector<int> vec = {6, 7, 8, 9, 10};
print(vec); // 输出: 6 7 8 9 10
// 使用 std::span 查看部分数据
std::span<int> partial(vec.data() + 1, 3);
print(partial); // 输出: 7 8 9
return 0;
}
std::span
是处理连续内存序列的强大工具, 尤其适用于需要高效传递和操作数据的场景.
进一步阅读: {{< PostRef path=“/posts/2024-07-31-cpp20-span.md” >}}
6. 类型模板参数扩展
C++20 对非类型模板参数(Non-Type Template Parameters, NTTP)进行了扩展, 允许使用更多类型的非类型模板参数. 在 C++17 及之前, 非类型模板参数只能是整型, 指针, 引用或枚举类型. C++20 扩展了这一能力,
NTTP 支持如下类型:
- 浮点类型
- 结构体和简单类
- Lambda
#include <iostream>
#include <string_view>
// 使用浮点类型作为非类型模板参数
template <double Value>
void print() {
std::cout << "Double value: " << Value << "\n";
}
int main() {
// 浮点类型模板参数
print<3.14>(); // 输出: Double value: 3.14
return 0;
}
优势:
- 更强的表达能力: 支持更多类型的非类型模板参数, 使得模板编程更加灵活.
- 编译时计算: 结合
constexpr
和模板元编程, 可以在编译时进行更复杂的计算和优化.
C++20 的非类型模板参数扩展为模板编程带来了更多可能性, 尤其是在需要编译时处理浮点数或字符串的场景中非常有用.
7. 编译时计算
C++20 在编译时计算(Compile-Time Computing)方面引入了一些新特性, 使得在编译时进行复杂的计算和优化变得更加方便.
constexpr
增强: C++20 进一步扩展了constexpr
的使用范围, 允许在编译时执行更多的操作, 包括动态内存分配, 异常处理等.constinit
: 表示一个全局变量或常static
类型变量在编译时完成初始化.constinit
=constexpr - const
. 注意constinit
修饰的变量是允许后续修改内容的.consteval
: 用于定义必须在编译时求值的函数.
constexpr int square(int x) { return x * x; }
consteval int triple(int x) { return x * 3; }
int main() {
constexpr int CX_VAL = square(5); // 编译期计算
// CX_VAL = 1; // 错误, 常量不可修改
int val = square(5); // 初始化普通变量
square(val); // OK, 可以在运行时计算
constinit static int value = 10; // 在编译期初始化, 但不是常量
value = 20;
// constinit int value2 = 10; // 错误, constinit 只能用于静态变量或者全局变量
const int data = triple(4); // 合法, 因为在编译期求值
// triple(runtime); // 错误, 不能在在运行期求值
return 0;
}
进一步阅读: C++ constexpr, consteval 和 constinit 简要介绍
8. lambda 扩展
C++20 对 Lambda 表达式进行了一些扩展, 使得 Lambda 更加灵活和强大. 以下是 C++20 中 Lambda 表达式的主要扩展特性:
-
模板 Lambda: C++20 允许在 Lambda 表达式中使用模板参数, 使得 Lambda 可以处理不同类型的参数.
auto genericLambda = []<typename T>(T a, T b) { // logic here };
-
this
捕获: C++20 引入了[*this]
捕获方式, 允许在 Lambda 中捕获当前对象的副本, 而不是引用. -
consteval
和constexpr
Lambda: Lambda 表达式现在可以标记为consteval
或constexpr
, 以便在编译时求值.
#include <fmt/core.h>
#include <fmt/ranges.h>
int main() {
// 1. 泛型 Lambda
auto generic = []<typename T>(T a, T b) { return a + b; };
fmt::println("Generic Lambda (int): {}", generic(1, 2)); // 输出: 3
fmt::println("Generic Lambda (double): {}", generic(1.5, 2.5)); // 输出: 4.0
// 2. `constexpr` Lambda
constexpr auto constexprLambda = [](int a, int b) constexpr { return a + b; };
constexpr int result = constexprLambda(3, 4);
fmt::println("Constexpr Lambda: {}", result); // 输出: 7
// 3. `[*this]` 捕获
struct MyStruct {
int value = 42;
auto getLambda() {
return [*this]() { return value; };
}
};
MyStruct obj;
auto lambda = obj.getLambda();
obj.value = 100;
fmt::println("Lambda with [*this] capture: {}", lambda()); // 输出: 42
return 0;
}
9. 格式化输出
C++20 引入了新的格式化输出库 std::format
, 它提供了一种类型安全且更灵活的字符串格式化方式, 类似于 Python 的 str.format()
. std::format
通过使用格式化字符串和占位符来生成格式化的输出, 避免了传统 printf
风格函数中的类型不安全问题.
- 类型安全:
std::format
是类型安全的, 编译器会在编译时检查格式字符串和参数的类型是否匹配. - 简洁易读: 格式化字符串使用
{}
作为占位符, 语法简洁且易于理解. - 支持自定义类型: 可以通过重载
std::formatter
来支持自定义类型的格式化输出.
#include <format>
#include <iostream>
int main() {
int num = 42;
double pi = 3.14159;
std::string name = "Alice";
// 基本格式化
std::cout << std::format("Number: {}, Pi: {:.2f}, Name: {}\n", num, pi, name);
// 输出: Number: 42, Pi: 3.14, Name: Alice
// 格式化字符串存储到变量
std::string formatted =
std::format("Number: {}, Pi: {:.2f}, Name: {}", num, pi, name);
std::cout << formatted << std::endl;
// 输出: Number: 42, Pi: 3.14, Name: Alice
// 格式化字符串中的位置参数
std::cout << std::format("Name: {2}, Pi: {1:.2f}, Number: {0}\n", num, pi,
name);
// 输出: Name: Alice, Pi: 3.14, Number: 42
return 0;
}
进一步阅读: C++23 格式化输出新特性详解: std::print 和 std::println
10. chrono
新增日期和时区
C++20 在 <chrono>
库中引入了对日期, 时间和时区的支持, 使得处理日期和时间变得更加方便和强大. 新的特性包括对日历日期的支持, 时区转换以及时间点之间的计算等.
- 日历日期支持: C++20 引入了
year
,month
,day
等类型, 用于表示日历日期. - 时间点扩展:
sys_time
和local_time
分别表示系统时间和本地时间. - 时区支持:
time_zone
和zoned_time
用于处理时区转换. - 日历运算: 支持日期的加减运算, 如计算两个日期之间的天数.
#include <chrono>
#include <format>
#include <iostream>
int main() {
using namespace std::chrono;
// 1. 创建日历日期
year_month_day today = 2025y / March / 6d;
std::cout << "Today is: " << today << std::endl; // 输出: 2025-03-06
// 2. 创建时间点
sys_time<seconds> sys_now = time_point_cast<seconds>(system_clock::now());
std::cout << "System time now: " << sys_now << std::endl; // 输出当前系统时间
// 3. 时区转换
auto utc_time = sys_now;
auto local_time = zoned_time{current_zone(), utc_time};
std::cout << "Local time now: " << local_time
<< std::endl; // 输出当前本地时间
// 4. 计算两个日期之间的天数
year_month_day next_week = sys_days{today} + days{7};
std::cout << "Next week is: " << next_week << std::endl; // 输出: 2025-03-13
// 5. 格式化输出
std::cout << std::format("Today is {:%Y-%m-%d}\n",
today); // 输出: Today is 2025-03-06
return 0;
}
进一步阅读: 一文读懂 C++ chrono 库: duration, clocks, date, timezone
11. 协程
C++20 的协程设计比较复杂, 可以参考以下链接:
Modern C++ Coroutine 简介
12. std::jthread
和 stop_token
C++20 引入了 std::jthread
和 std::stop_token
, 它们为多线程编程提供了更安全和更方便的线程管理机制. std::jthread
是 std::thread
的增强版本, 支持在离开作用域时自动join
, 而 std::stop_token
提供了一种机制来请求线程停止执行.
-
std::jthread
:- 自动管理线程的生命周期, 析构时会自动调用
join()
, 确保线程在销毁前完成执行. - 支持
std::stop_token
, 允许线程在外部请求停止执行.
- 自动管理线程的生命周期, 析构时会自动调用
-
std::stop_token
和std::stop_source
:std::stop_token
用于检查是否收到停止请求.std::stop_source
用于发出停止请求.- 两者可以配合使用, 实现线程的优雅停止.
以下是一个简单的示例, 展示了如何使用 std::jthread
和 std::stop_token
:
#include <iostream>
#include <thread>
#include <stop_token>
#include <chrono>
void worker(std::stop_token stopToken) {
while (!stopToken.stop_requested()) {
std::cout << "Working...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Stopped by request.\n";
}
int main() {
// 创建一个 jthread, 并传递 worker 函数
std::jthread t(worker);
// 主线程等待 3 秒
std::this_thread::sleep_for(std::chrono::seconds(3));
// 请求线程停止
t.request_stop();
// jthread 析构时会自动 join, 确保线程完成执行
return 0;
}
输出
Working...
Working...
Working...
Stopped by request.
进一步阅读: C++20 std::jthread 完全指南 - 简化多线程编程与线程管理
13. 并发特性
latches
和 barriers
std::latch
是一种同步原语, 用于确保一组线程在继续执行之前等待某个操作完成. std::latch
一旦倒数到零, 就不能重置. 它非常适合用于等待一组线程完成初始化或其他初始化操作.
- 构造函数: 接受一个计数器, 表示需要等待的线程数量.
- count_down(): 递减计数器.
- wait(): 阻塞当前线程, 直到计数器归零.
#include <iostream>
#include <latch>
#include <thread>
#include <vector>
void worker(std::latch& latch) {
// 模拟工作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Worker thread done\n";
latch.count_down();
}
int main() {
std::latch latch(3); // 等待3个线程
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(worker, std::ref(latch));
}
latch.wait(); // 主线程等待所有工作线程完成
std::cout << "All worker threads are done\n";
for (auto& t : threads) {
t.join();
}
return 0;
}
Barriers (栅栏)
std::barrier
类似于 std::latch
, 但它可以被重置. std::barrier
用于一组线程在继续执行之前等待所有线程到达某个同步点. 所有线程到达后, 它们可以继续执行, 而 std::barrier
也可以被重置以便再次使用.
- 构造函数: 接受一个计数器, 表示需要等待的线程数量.
- arrive_and_wait(): 递减计数器并阻塞当前线程, 直到所有线程到达.
- arrive_and_drop(): 递减计数器但不等待其他线程.
- arrive_and_wait(): 递减计数器并阻塞当前线程, 直到所有线程到达.
#include <barrier>
#include <iostream>
#include <thread>
#include <vector>
void worker(std::barrier<>& barrier, int id) {
std::cout << "Worker " << id << " is ready\n";
barrier.arrive_and_wait(); // 等待所有线程到达
std::cout << "Worker " << id << " is processing\n";
barrier.arrive_and_wait(); // 等待所有线程完成处理
std::cout << "Worker " << id << " is done\n";
}
int main() {
std::barrier barrier(3); // 等待3个线程
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(worker, std::ref(barrier), i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
更多阅读: C++ Latch 和 Barrier: 新手指南
Semaphores
C++20 引入了 std::counting_semaphore
和 std::binary_semaphore
作为同步原语, 用于多线程编程中的资源管理. 信号量(Semaphore)是一种用于控制多个线程对共享资源访问的机制. 信号量维护一个计数器, 该计数器表示可用资源的数量. 当一个线程需要访问资源时, 它会尝试获取信号量; 如果计数器大于零, 则线程可以访问资源, 并将计数器减一; 如果计数器为零, 则线程会被阻塞, 直到有其他线程释放资源.
std::counting_semaphore
: 用于管理多个资源的访问. 构造函数接受一个初始计数值, 表示可用资源的数量.std::binary_semaphore
: 特殊的计数信号量, 等价于std::counting_semaphore<1>
, 用于二进制信号量的场景.acquire()
: 尝试获取信号量, 如果计数器大于零, 则计数器减一; 否则, 线程会被阻塞.try_acquire()
: 尝试获取信号量, 如果计数器大于零, 则计数器减一并返回true
; 否则, 立即返回false
.release()
: 释放信号量, 将计数器加一, 唤醒一个等待的线程(如果有).
使用 std::counting_semaphore
- 创建了一个
std::counting_semaphore
, 初始计数为 5, 表示有 5 个可用资源. - 10 个线程尝试获取信号量, 但只有 5 个线程可以同时工作, 其余线程会被阻塞, 直到有其他线程释放信号量.
#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>
std::counting_semaphore<5> sem(5); // 指定模板参数类型为 int
void worker(int id) {
sem.acquire(); // 尝试获取信号量
std::cout << "Worker " << id << " is working\n";
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟工作
std::cout << "Worker " << id << " is done\n";
sem.release(); // 释放信号量
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
使用 std::binary_semaphore
- 创建了一个
std::binary_semaphore
, 初始计数为 0, 表示资源不可用. - 工作者线程尝试获取信号量, 但会被阻塞, 直到生产者线程释放信号量, 表示资源可用.
#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>
std::binary_semaphore sem(0); // 初始状态为0, 表示资源不可用
void worker(int id) {
sem.acquire(); // 尝试获取信号量
std::cout << "Worker " << id << " is working\n";
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟工作
std::cout << "Worker " << id << " is done\n";
}
void producer() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟生产时间
std::cout << "Producer is ready\n";
sem.release(); // 释放信号量, 表示资源可用
}
int main() {
std::thread t1(worker, 1);
std::thread t2(producer);
t1.join();
t2.join();
return 0;
}
更多阅读: 条件变量 vs 信号量: 如何选择适合你的多线程同步工具?
Synchronized Output Streams
C++20 引入了 std::osyncstream
作为同步输出流, 用于确保在多线程环境中对输出流的写操作是线程安全的. std::osyncstream
是一个同步输出流缓冲区, 它将输出操作缓冲起来, 然后在析构时将缓冲区的内容原子地写入到目标输出流中. 这避免了多个线程同时写入输出流时可能出现的交错输出问题.
- 线程安全:
std::osyncstream
确保对输出流的写操作是线程安全的. - 缓冲区: 输出操作会被缓冲, 直到
std::osyncstream
对象被销毁或显式刷新. - 自动刷新: 当
std::osyncstream
对象被销毁时, 缓冲区的内容会自动刷新到目标输出流.
#include <iostream>
#include <syncstream>
#include <thread>
#include <vector>
void worker(std::ostream& os, int id) {
std::osyncstream sync_os(os);
sync_os << "Worker " << id << " is starting\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
sync_os << "Worker " << id << " is done\n";
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(worker, std::ref(std::cout), i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
输出示例
Worker 1 is starting
Worker 1 is done
Worker 0 is starting
Worker 0 is done
Worker 4 is starting
Worker 4 is done
Worker 2 is starting
Worker 2 is done
Worker 3 is starting
Worker 3 is done
每个线程的输出都是完整的, 不会与其他线程的输出交错, 这得益于 std::osyncstream
的线程安全特性.
进一步阅读: C++20 std::osyncstream 完全指南 - 解决多线程输出混乱问题
14. Modules
C++20 引入了模块(Modules), 这是一种新的代码组织方式, 旨在替代传统的头文件(#include
)机制. 模块提供了更好的编译性能, 更清晰的代码结构以及更强的封装性.
- 编译性能提升: 模块避免了头文件的重复解析, 从而显著减少了编译时间.
- 封装性增强: 模块可以控制哪些符号对外可见, 哪些符号仅在模块内部使用.
- 减少宏污染: 模块不会像头文件那样引入宏定义, 避免了宏污染问题.
- 简化依赖管理: 模块的依赖关系更加清晰, 减少了复杂的头文件包含顺序问题.
代码示例
以下是一个简单的模块示例, 展示了如何定义和使用模块:
{{< CodeTabs >}}
{{< Code title=“math.ixx” >}}
export module math;
export int add(int a, int b) { return a + b; }
export int multiply(int a, int b) { return a * b; }
{{< /Code >}}
{{< Code title=“main.cpp” >}}
import math;
#include <iostream>
int main() {
std::cout << "Add: " << add(2, 3) << "\n"; // 输出: Add: 5
std::cout << "Multiply: " << multiply(2, 3) << "\n"; // 输出: Multiply: 6
return 0;
}
{{< /Code >}}
{{< Code title=“CMakeLists.txt” >}}
add_library(simple_module)
target_sources(simple_module
PUBLIC
FILE_SET CXX_MODULES FILES
math.ixx
)
add_executable(simple_demo main.cpp)
target_link_libraries(simple_demo simple_module)
{{< /Code >}}
{{< /CodeTabs >}}
进一步阅读: CMake 构建 C++20 Module 实例(使用 MSVC)
其他改进
String Members starts_with()
and ends_with()
std::string str = "Hello, World!";
// 检查字符串是否以 "Hello" 开头
assert(str.starts_with("Hello"));
// 检查字符串是否以 '!' 结尾
assert(str.ends_with('!'));
受限的 string
成员函数 reserve()
对于 string 来说, 成员函数 reserve
不能再用于请求缩小字符串的容量(内存分配给字符串的值).
新的工具类函数ssize()
// int与size_t的比较会导致编译器警告
for (int i = 0; i < std::ssize(str); ++i) {
}
// 使用ssize, 得到有符号的类型
for (int i = 0; i < std::ssize(str); ++i) {
}
std::source_location
代码位置, 帮助追踪代码位置.
#include <fmt/core.h>
#include <source_location>
int main() {
auto sl = std::source_location::current();
fmt::println("file: {}", sl.file_name());
fmt::println("function: {}", sl.function_name());
fmt::println("line/col: {}/{}", sl.line(), sl.column());
return 0;
}
专栏目录
C++17 新特性总结
C++20 新特性总结
C++23 新特性总结
C++26 新特性预览(Preview)
说明
本专栏的文章同步发布在 CSDN 和我的个人网站上. 由于 CSDN 的排版限制, 诸如代码高亮, 链接到 Compiler Explorer 的代码段等特殊格式可能会被移除, 感兴趣的读者可以访问我的原贴.
此外, 文中包含许多指向我个人网站的链接, 逐一修正这些链接需要大量时间和精力, 暂时无法完成, 敬请谅解. 感谢您的理解与支持!