作者:billy
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处
前言
C++20 是 C++ 标准中的一个重要版本,引入了许多新特性和改进,包括模块(Modules)、协程(Coroutines)、概念(Concepts)、三向比较运算符(<=>)、范围(Ranges)、日期时间库(Date and Time)、数字分隔符(Digit Separators)等等。
有的小伙伴可能会需要参考手册的链接,这里给一个网页版的:C++ 参考手册 - 网页版
一. 语言特性
1. 模块(Modules)
C++20 的模块通过引入 “导入(import)” 和 “导出(export)” 概念,替代传统的头文件机制,减少了编译时间,避免头文件互相包含的问题,使代码组织更清晰,提高了封装性。
// example_module.cppm (在 .cppm 文件中定义模块)
export module example_module;
import <string>; // 使用 import 代替 #include 来导入 <string>
export namespace Example
{
// 定义一个学生类
class Student
{
public:
Student(std::string _name, int _age) : name(_name), age(_age) {}
std::string getName() const { return name; }
int getAge() const { return age; }
private:
std::string name;
int age;
};
}
// main.cpp
import example_module; // 导入自定义的模块 example_module
#include <iostream>
//import <iostream>; 这里用 import <iostream> 失败了,可能 vs2019 中支持还不够,还是用的原始的 #include
int main()
{
Example::Student stu("billy", 18);
std::cout << "Student: " << stu.getName() << ", " << stu.getAge() << std::endl;
return 0;
}
在这个例子中,example_module.cppm 定义了一个名为 Example 的命名空间,并在其中导出了 Student 类。main.cpp 通过 import 语句导入了example_module,然后直接使用了 Student。这种清晰的模块边界和导入机制,使得代码更加整洁、易于管理和维护。
模块化编程是 C++ 语言发展的重要一步,它解决了长期困扰 C++ 开发者的编译时间和代码组织问题。虽然在实际应用中可能会遇到一些挑战,但通过合理的规划和实践,开发者可以充分利用这一特性,提升开发效率和代码质量。随着编译器对 C++20 标准支持的不断成熟,模块化编程将成为现代 C++ 开发不可或缺的一部分。
2. 协程(Coroutines)
协程就是一个特殊的函数,可以在执行过程中挂起(suspend)并在稍后恢复(resume)。你可以暂停执行去做其他事情,然后在适当的时候恢复到暂停的位置继续执行。协程让异步编程更接近同步编程风格,减少了异步操作的复杂度,在游戏脚本、网络通信、高并发服务器等领域有很大作用。
C++ 提供了三个方法挂起协程:co_await, co_yield 和 co_return。如果一个函数中存在这三个关键字之一,那么它就是一个协程。
- co_await:用于暂停协程的执行,等待一个可等待对象(awaitable)完成。
- co_yield:用于将一个值返回给协程的调用者,并暂停协程的执行。
- co_return:用于从协程中返回一个值,并终止协程的执行。
// co_await 和 co_return 示例
#include <iostream>
#include <future>
#include <thread>
// 协程函数
std::future<int> coroutine()
{
// 创建一个异步任务,休眠 2 秒后返回 42
auto task = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
});
// 使用 co_await 等待异步任务完成
int result = co_await std::move(task);
co_return result;
}
int main()
{
auto fut = coroutine();
// 获取协程的结果
int value = fut.get();
std::cout << "Result: " << value << std::endl;
return 0;
}
// co_yield 示例
#include <iostream>
#include <coroutine>
#include <optional>
// 自定义的协程返回类型,包含一个 promise_type
template <typename T>
struct Generator
{
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type
{
T value_;
std::exception_ptr exception_;
Generator get_return_object() {
return Generator{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); }
template <std::convertible_to<T> From>
std::suspend_always yield_value(From&& from)
{
value_ = std::forward<From>(from);
return {};
}
void return_void() {}
};
handle_type h_;
Generator(handle_type h) : h_(h) {}
~Generator() { if (h_) h_.destroy(); }
std::optional<T> operator()()
{
if (!h_ || h_.done()) return {};
h_.resume();
if (h_.promise().exception_)
std::rethrow_exception(h_.promise().exception_);
return h_.promise().value_;
}
};
// 生成斐波那契数列的协程
Generator<int> fibonacci()
{
int a = 0, b = 1;
while (true)
{
co_yield a;
int temp = a;
a = b;
b = temp + b;
}
}
int main()
{
// 调用 fibonacci 协程,并通过 Generator 的 operator() 方法获取生成的值
auto gen = fibonacci();
for (int i = 0; i < 10; ++i)
{
auto value = gen();
if (value) {
std::cout << *value << " ";
}
}
std::cout << std::endl;
return 0;
}
3. 概念(Concepts)
在传统的模板编程中,模板参数的类型约束通常依赖于隐式接口,即模板代码在实例化时才会检查类型是否满足要求。这可能导致在编译过程中出现复杂且难以理解的错误信息。C++20 引入的概念(Concepts)是一项强大的新特性,它为模板编程提供了更强大的类型约束机制,使得代码更加清晰、易读,并且能够在编译期更早地发现错误。概念通过显式地定义模板参数必须满足的条件,使得模板的使用更加安全和可维护。
#include <iostream>
#include <concepts>
// 定义一个概念,检查类型是否为整数类型
template <typename T>
concept Integral = std::is_integral_v<T>;
// 方式一:使用 Integral 概念约束模板参数
template <Integral T>
T add(T a, T b)
{
return a + b;
}
// 方式二:使用 requires 子句
template <typename T>
requires Integral<T>
T subtract(T a, T b)
{
return a - b;
}
int main()
{
int x = 5, y = 3;
std::cout << "Addition: " << add(x, y) << std::endl;
std::cout << "Subtraction: " << subtract(x, y) << std::endl;
// 以下代码会导致编译错误,因为 double 不满足 Integral 概念
// double d1 = 5.5, d2 = 3.3;
// std::cout << "Addition: " << add(d1, d2) << std::endl;
return 0;
}
// 复杂概念约束示例
#include <iostream>
#include <concepts>
// 定义一个概念,检查类型是否为整数类型
template <typename T>
concept Integral = std::is_integral_v<T>;
// 定义一个概念,要求类型支持 ++ 运算符
template <typename T>
concept Incrementable = requires(T t)
{
{ ++t } -> std::same_as<T&>;
};
// 定义一个复杂概念,要求类型既是整数类型,又支持 ++ 运算符
template <typename T>
concept IntegralAndIncrementable = Integral<T> && Incrementable<T>;
// 使用复杂概念约束模板参数
template <IntegralAndIncrementable T>
void incrementAndPrint(T& value)
{
++value;
std::cout << "Incremented value: " << value << std::endl;
}
int main()
{
int num = 10;
incrementAndPrint(num);
return 0;
}
4. 三向比较运算符(<=>)
也叫太空船运算符,用于比较两个对象,返回小于、等于、大于三种结果。类似于 C 的 strcmp 函数返回 -1, 0, 1。
int a = 5, b = 3;
auto result = a <=> b;
// result > 0 表示 a > b
// result == 0 表示 a == b
// result < 0 表示 a < b
可以通过下面的代码自动生成 ==、!=、<、<=、>、>= 等运算符,它简化了多重比较运算的实现过程。
auto X::operator<=>(const Y&) = default;
如果对象是结构体则会逐个比较
#include <iostream>
#include <compare>
struct Point
{
int x, y;
// 自动生成所有比较运算符
auto operator<=>(const Point&) const = default;
};
int main()
{
Point p1 = { 1, 2 };
Point p2 = { 1, 3 };
if (p1 < p2) {
std::cout << "p1 is less than p2\n";
}
if (p1 != p2) {
std::cout << "p1 is not equal to p2\n";
}
}
5. 范围 for 循环的语句初始化
从 C++ 17 开始支持 if 和 switch 的语句初始化,现在 C++20 中也可以在范围循环中进行初始化。
for ( std::vector v {1, 2, 3}; auto& e : v )
{
std::cout << e << std::endl;
}
二. 标准库特性
1. Ranges 库
Ranges 库是对标准模板库(STL)的一个重要扩展,它提供了一种现代、简洁的方式来处理序列(如数组、容器等)上的操作。范围库的主要特点是允许通过组合函数和适配器链接操作,使代码更直观和易读。
#include <iostream>
#include <ranges>
#include <vector>
int main()
{
std::vector<int> numbers = { 1, 9, 6, 7, 5, 8, 4, 3, 2, 10, 16, 14, 13, 12, 15, 11 };
// 使用 ranges 进行惰性计算
auto result = numbers
| std::ranges::views::filter([](int n) { return n % 2 == 0; }) // 只保留偶数
| std::ranges::views::transform([](int n) { return n * 2; }) // 每个数乘以2
| std::ranges::views::take(6) // 获取范围内前6个数
| std::ranges::views::drop(2) // 丢弃范围内前2个数
| std::ranges::views::reverse; // 反转范围内元素的顺序
// 遍历输出结果
for (int n : result)
{
std::cout << n << std::endl;
}
}
2. 日历和时区库
C++20 引入了强大的日历和时区库,提供了处理日期、时间、日历系统和时区的功能,使得在 C++ 中进行日期和时间的操作变得更加方便和直观。
- std::chrono::year_month_day:表示公历中的一个具体日期,由年、月、日组成。
- std::chrono::year_month_weekday:表示公历中一个月内的第几个星期几,例如 2025 年 2 月的第 2 个星期五。
- std::chrono::day、std::chrono::month、std::chrono::year:分别表示日、月、年的类型。
- std::chrono::time_zone:表示一个时区,包含时区的名称、偏移量等信息。
- std::chrono::zoned_time:表示一个带有时区信息的时间点。
// 时间操作示例
#include <iostream>
#include <chrono>
int main()
{
using namespace std::chrono;
// 创建一个具体日期
year_month_day date = 2025y / February / 7d;
// 输出日期信息
std::cout << "Year: " << static_cast<int>(date.year()) << std::endl;
std::cout << "Month: " << static_cast<unsigned>(date.month()) << std::endl;
std::cout << "Day: " << static_cast<unsigned>(date.day()) << std::endl;
// 检查日期是否有效
if (date.ok()) {
std::cout << "The date is valid." << std::endl;
} else {
std::cout << "The date is invalid." << std::endl;
}
return 0;
}
// 时区操作示例
#include <iostream>
#include <chrono>
int main()
{
using namespace std::chrono;
// 获取当前系统时钟的时间点
auto now = system_clock::now();
// 获取本地时区
const time_zone* local_tz = current_zone();
// 创建带有时区信息的时间点
zoned_time local_time(local_tz, now);
// 输出本地时间
std::cout << "Local time: " << local_time << std::endl;
// 获取另一个时区(例如纽约)
const time_zone* ny_tz = locate_zone("America/New_York");
zoned_time ny_time(ny_tz, now);
// 输出纽约时间
std::cout << "New York time: " << ny_time << std::endl;
return 0;
}
3. std::span
在 C++20 中引入的 std::span 是一个轻量级、非拥有的视图,它可以表示连续的对象序列。它提供了一种安全且高效的方式来处理数组、std::array、std::vector 等连续存储的数据结构,而无需复制数据。
主要特点
- 轻量级:std::span 仅包含指向数据的指针和长度信息,没有内存所有权,因此创建和复制 std::span 的开销非常小。
- 通用性:可以与各种连续存储的数据结构(如 C 风格数组、std::array、std::vector 等)一起使用。
- 安全性:std::span 会跟踪所引用数据的长度,避免越界访问。
#include <iostream>
#include <span>
#include <vector>
#include <array>
int main()
{
// 从 std::vector 创建 std::span
std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> vecSpan(vec);
// 从 std::array 创建 std::span
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::span<int> arrSpan(arr);
// 从 C 风格数组创建 std::span
int cArr[] = {1, 2, 3, 4, 5};
std::span<int> cArrSpan(cArr, std::size(cArr));
// 获取子视图
std::span<int> subSpan = vecSpan.subspan(1, 3);
// 输出子视图中的元素
for (int element : subSpan)
{
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
4. std::jthread
std::jthread 是 C++20 标准库中引入的一个新线程类,std::jthread 与 std::thread 相比安全性更高,std::jthread 会自动处理线程的清理工作,避免了可能出现的资源泄漏问题。并且提供了内置的停止机制,使得线程的取消操作更加方便和安全。
#include <iostream>
#include <thread>
#include <chrono>
void cancellableThread(std::stop_token st)
{
while (!st.stop_requested())
{
std::cout << "Thread is working..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Thread is stopping." << std::endl;
}
int main()
{
std::jthread jt(cancellableThread);
// 模拟一段时间后请求线程停止
std::this_thread::sleep_for(std::chrono::seconds(3));
jt.request_stop();
return 0;
}
5. std::format
C++20 引入了 std::format 库,提供了一种功能强大且灵活的字符串格式化方式。与传统的 printf 和 std::stringstream 方法相比,std::format 更加简洁且安全,支持类型安全的格式化,并且可以更方便地与现代 C++ 标准库集成。
#include <iostream>
#include <format>
int main()
{
std::string name = "billy";
int age = 18;
double pi = 3.1415926;
std::cout << std::format("Name: {}, Age: {}", name, age) << std::endl;
std::cout << std::format("Pi: {:.4f}", pi) << std::endl;
std::cout << std::format("{:>10}", age) << std::endl; // 右对齐,宽度为10
std::cout << std::format("{:<10}", age) << std::endl; // 左对齐,宽度为10
std::cout << std::format("{:0>10}", age) << std::endl; // 用0填充,宽度为10
std::cout << std::endl;
std::cout << std::format("Decimal: {}", age) << std::endl;
std::cout << std::format("Hexadecimal: {:#x}", age) << std::endl; // 带有前缀0x
std::cout << std::format("Octal: {:#o}", age) << std::endl; // 带有前缀0
std::cout << std::format("Binary: {:#b}", age) << std::endl; // 带有前缀0b
return 0;
}
// 自定义类型的格式化
#include <iostream>
#include <format>
#include <string>
struct Point {
int x;
int y;
};
template<>
struct std::formatter<Point>
{
constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }
auto format(const Point& p, std::format_context& ctx)
{
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
int main()
{
Point p{3, 4};
std::string result = std::format("The point is {}.", p);
std::cout << result << std::endl;
return 0;
}
6. std::source_location
std::source_location 是 C++20 标准库引入的一个新特性,它允许在代码中获取当前代码的源文件位置信息,包括文件名、行号、列号以及函数名等。这对于调试、日志记录和错误报告等场景非常有用。
// 日志记录
#include <iostream>
#include <source_location>
void log(const std::string& message, const std::source_location& location = std::source_location::current())
{
std::cout << "File: " << location.file_name() << '\n'
<< "Line: " << location.line() << '\n'
<< "Column: " << location.column() << '\n'
<< "Function: " << location.function_name() << '\n'
<< "Message: " << message << '\n';
}
int main()
{
log("This is a log message.");
return 0;
}
// 错误处理
#include <iostream>
#include <source_location>
#include <stdexcept>
void divide(int a, int b, const std::source_location& location = std::source_location::current())
{
if (b == 0) {
throw std::runtime_error(std::format("Division by zero at {}:{}", location.file_name(), location.line()));
}
std::cout << "Result: " << a / b << std::endl;
}
int main()
{
try {
divide(10, 0);
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
7. std::map 和 std::set 的 contains 函数
在 C++20 中,std::map 和 std::set 引入了 contains 函数,这为判断容器中是否包含某个特定元素提供了更加简洁和直观的方式。在 C++20 之前,要判断 std::set 或 std::map 中是否包含某个元素或键,通常使用 find 函数。虽然 find 函数也能实现相同的功能,但 contains 函数的语义更加清晰,代码也更加简洁。
#include <iostream>
#include <set>
#include <map>
#include <string>
int main()
{
std::set<int> mySet = { 1, 2, 3, 4, 5 };
// 检查集合中是否包含元素 3
// if (mySet.find(3) != mySet.end()) { // find 函数的判断方式
if (mySet.contains(3)) {
std::cout << "Set contains 3." << std::endl;
}
else {
std::cout << "Set does not contain 3." << std::endl;
}
std::map<std::string, int> myMap = {
{"apple", 1},
{"banana", 2},
{"cherry", 3}
};
// 检查映射中是否包含键 "banana"
// if (myMap.find("banana") != myMap.end()) {
if (myMap.contains("banana")) {
std::cout << "Map contains key 'banana'." << std::endl;
}
else {
std::cout << "Map does not contain key 'banana'." << std::endl;
}
return 0;
}
8. 栅栏(barriers)、闩锁(latches)
C++20 中引入了 std::latch(闩锁)和 std::barrier(栅栏)这两个同步原语,它们用于在多线程环境中协调线程的执行,确保线程在特定的点上进行同步。
std::latch 是一种一次性的同步原语,它允许一个或多个线程等待,直到达到预先设定的计数值。一旦计数值达到零,所有等待的线程都会被释放,并且之后不能再使用这个 std::latch 进行同步。
#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 finished its work." << std::endl;
// 完成工作后,减少闩锁的计数,将计数值减 1
latch.count_down();
}
int main()
{
const int numThreads = 3;
// 初始化闩锁,预期计数值为 3,表示需要等待 3 个线程完成工作
std::latch latch(numThreads);
std::vector<std::jthread> threads;
for (int i = 0; i < numThreads; ++i)
{
threads.emplace_back(worker, std::ref(latch));
}
// 主线程调用 latch.wait() 阻塞,直到计数值变为 0,即所有工作线程都完成了工作
std::cout << "Main thread waiting for workers..." << std::endl;
latch.wait();
std::cout << "All workers finished, main thread can continue." << std::endl;
return 0;
}
std::barrier 是一种可重复使用的同步原语,它允许一组线程在某个点上进行同步。当所有线程都到达栅栏时,会执行一个可选的完成函数,然后所有线程继续执行。之后这个 std::barrier 可以再次用于同步。
#include <iostream>
#include <barrier>
#include <thread>
#include <vector>
// 栅栏的完成函数
void onCompletion()
{
std::cout << "All threads reached the barrier, continue..." << std::endl;
}
// 模拟一些工作
void worker(std::barrier<>& barrier)
{
std::cout << "Worker thread starting work." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Worker thread reached the barrier." << std::endl;
// 到达栅栏并等待其他线程
barrier.arrive_and_wait();
std::cout << "Worker thread continues after the barrier." << std::endl;
}
int main()
{
const int numThreads = 3;
// 初始化栅栏,预期线程数量为 3,并指定完成函数
std::barrier barrier(numThreads, onCompletion);
// 启动 3 个工作线程,每个线程在完成一部分工作后调用 barrier.arrive_and_wait() 到达栅栏并等待其他线程。
// 当所有线程都到达栅栏时,会执行完成函数 onCompletion,然后所有线程继续执行后续代码。
std::vector<std::jthread> threads;
for (int i = 0; i < numThreads; ++i)
{
threads.emplace_back(worker, std::ref(barrier));
}
return 0;
}
9. std::future 和 std::promise
std::promise 和 std::future 通常一起使用来实现线程间的同步和数据传递。std::promise 用于存储一个值或异常,供后续的 std::future 对象获取;而 std::future 则用于异步地获取 std::promise 所存储的值或异常,通过这种方式可以方便地在不同线程之间共享数据和状态。
std::future 主要成员函数:
- get():阻塞当前线程,直到关联的 std::promise 设置了值或异常,然后返回该值或抛出异常。
- valid():检查 std::future 对象是否有效,即是否与一个 std::promise 或 std::packaged_task 关联。
- wait():阻塞当前线程,直到关联的 std::promise 设置了值或异常,但不获取该值。
- wait_for():在指定的时间内等待关联的 std::promise 设置值或异常,如果超时则返回一个状态。
- wait_until():等待直到指定的时间点,如果在该时间点前关联的 std::promise 设置了值或异常,则返回。
std::promise 主要成员函数:
- set_value():设置存储的值,一旦调用此函数,关联的 std::future 对象就可以安全地获取该值。
- set_exception():设置存储的异常,关联的 std::future 对象在获取值时会抛出该异常。
- get_future():返回一个与该 std::promise 关联的 std::future 对象,用于获取存储的值或异常。
#include <iostream>
#include <future>
#include <thread>
#include <vector>
// 工作线程函数
void multipleWorkers(std::promise<int>& prom, int id)
{
std::this_thread::sleep_for(std::chrono::seconds(id));
prom.set_value(id * 10);
}
int main()
{
// 创建了一个包含多个 std::promise 和 std::future 的向量,以及一个线程向量
const int numPromises = 3;
std::vector<std::promise<int>> promises(numPromises);
std::vector<std::future<int>> futures;
std::vector<std::thread> threads;
// 遍历 std::promise 向量,为每个 std::promise 获取对应的 std::future,并启动一个工作线程处理该 std::promise
for (int i = 0; i < numPromises; ++i)
{
futures.emplace_back(promises[i].get_future());
threads.emplace_back(multipleWorkers, std::ref(promises[i]), i);
}
// 主线程遍历 std::future 向量,调用 get() 方法获取每个 std::future 的结果并输出
for (int i = 0; i < numPromises; ++i)
{
int result = futures[i].get();
std::cout << "Result from promise " << i << ": " << result << std::endl;
}
// 等待所有工作线程完成
for (auto& t : threads)
{
t.join();
}
return 0;
}
10. std::is_constant_evaluated
std::is_constant_evaluated 是 C++20 引入的一个函数,用于在编译时判断当前代码是否在常量求值上下文中执行。这可以让函数根据不同的上下文执行不同的逻辑。
#include <iostream>
#include <type_traits>
constexpr int compute(int x)
{
if (std::is_constant_evaluated()) {
// 在编译时执行的逻辑
return x * x;
} else {
// 在运行时执行的逻辑
return x + x;
}
}
int main()
{
// 编译时计算
constexpr int compileTimeResult = compute(5);
std::cout << "Compile-time result: " << compileTimeResult << std::endl;
// 运行时计算
int num = 10;
int runtimeResult = compute(num);
std::cout << "Runtime result: " << runtimeResult << std::endl;
return 0;
}
三. 其他改进
1. Lambda 表达式的增强
1)模板 Lambda
在 C++20 之前,Lambda 表达式的参数类型必须是具体的类型。C++20 引入了模板 Lambda,允许在 Lambda 表达式中使用模板参数,使得 Lambda 可以处理不同类型的参数。
#include <iostream>
int main()
{
// 模板Lambda,使用 auto 关键字
auto add = [](auto a, auto b) {
return a + b;
};
std::cout << add(1, 2) << std::endl; // 处理整数
std::cout << add(1.5, 2.5) << std::endl; // 处理浮点数
return 0;
}
2)约束 Lambda
C++20 引入了概念(Concepts),可以将其应用于 Lambda 表达式,对 Lambda 的参数类型进行约束。
#include <iostream>
#include <concepts>
int main()
{
// 约束Lambda,要求参数是整数类型
auto printInt = []<std::integral T>(T value) {
std::cout << value << std::endl;
};
printInt(10); // 可以正常调用
// printInt(3.14); // 编译错误,因为 3.14 不是整数类型
return 0;
}
3)Lambda 默认构造函数
在 C++20 之前,lambda 表达式没有默认构造函数,这意味着你不能默认初始化一个 lambda 对象。而从 C++20 开始,无捕获的 lambda 表达式可以默认构造,这为代码的编写和使用带来了更多的灵活性。
#include <iostream>
// 定义一个函数,接受一个无捕获的 lambda 类型的参数
template<typename Func>
void callFunction(Func func) {
func();
}
int main()
{
// 定义一个无捕获的 lambda 类型
using MyLambda = void(*)();
// C++20 中,无捕获的 lambda 可以默认构造
MyLambda lambda{};
// 为 lambda 赋值一个无捕获的 lambda 表达式
lambda = []() {
std::cout << "Hello, C++20 Lambda Default Constructor!" << std::endl;
};
// 调用 lambda 表达式
lambda();
// 也可以将 lambda 传递给模板函数
callFunction(lambda);
return 0;
}
4)使用模板形参
C++20 允许在 Lambda 表达式中显式使用模板形参,这使得 Lambda 更加灵活和强大,就像普通的模板函数一样。
#include <iostream>
#include <concepts>
// 定义一个概念,要求类型为算术类型
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
int main()
{
// 带有模板形参和概念约束的 Lambda
auto arithmeticLambda = []<Arithmetic T>(T value) {
std::cout << "The arithmetic value is: " << value << std::endl;
};
// 调用 Lambda 处理算术类型的数据
arithmeticLambda(10);
arithmeticLambda(3.14);
// 下面这行代码会编译错误,因为字符串不是算术类型
// arithmeticLambda("Not an arithmetic type");
return 0;
}
2. constexpr 改进
1)constexpr 动态内存分配
C++20 允许在 constexpr 函数中进行动态内存分配和释放,这意味着在编译时可以使用 new 和 delete 运算符。不过,这些动态分配的内存必须在编译时释放,否则会导致编译错误。
#include <iostream>
constexpr int* allocateAndFill(int value)
{
int* ptr = new int(value);
return ptr;
}
constexpr void deletePtr(int* ptr)
{
delete ptr;
}
int main()
{
// 在编译时分配内存,并在编译时访问该内存中的值
constexpr int* ptr = allocateAndFill(42);
constexpr int value = *ptr;
// 在编译时释放内存
deletePtr(ptr);
std::cout << "Value: " << value << std::endl;
return 0;
}
2)constexpr std::vector 和其他标准库容器
C++20 允许在 constexpr 上下文中使用一些标准库容器,如 std::vector、std::string 等。这使得在编译时可以创建和操作容器。
#include <iostream>
#include <vector>
constexpr std::vector<int> createVector()
{
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
return vec;
}
int main()
{
// 在编译时创建一个 std::vector,并遍历该容器输出其中的元素
constexpr auto vec = createVector();
for (const auto& value : vec)
{
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
3)constexpr 虚函数调用的限制放宽
在 C++20 之前,constexpr 函数不能调用虚函数。C++20 放宽了这一限制,只要虚函数调用在编译时可以解析,就可以在 constexpr 函数中进行虚函数调用。
4)constexpr std::initializer_list
C++20 允许 std::initializer_list 在 constexpr 上下文中使用,这使得在编译时可以使用初始化列表来初始化对象。
#include <iostream>
#include <initializer_list>
constexpr int sum(std::initializer_list<int> list)
{
int result = 0;
for (int value : list)
{
result += value;
}
return result;
}
int main()
{
// 使用初始化列表调用 sum 函数,并将结果存储在 constexpr 变量 result 中。
constexpr int result = sum({1, 2, 3, 4});
std::cout << "Sum: " << result << std::endl;
return 0;
}
3. using 扩展
在 C++11 及以后版本就支持使用 using 定义模板别名,C++20 进一步增强了模板别名在泛型编程中的灵活性,允许使用非类型模板参数。
#include <iostream>
#include <vector>
#include <array>
// c++11 定义一个模板别名
template<typename T>
using Vec = std::vector<T>;
// c++20 定义一个模板别名,使用了非类型模板参数 std::size_t
template<typename T, std::size_t N>
using FixedArray = std::array<T, N>;
int main()
{
Vec<int> vec = {1, 2, 3};
for (int num : vec)
{
std::cout << num << " ";
}
std::cout << std::endl;
FixedArray<int, 5> arr = {1, 2, 3, 4, 5};
for (int num : arr)
{
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
4. 即时函数(Immediate Functions)
即时函数(Immediate Functions)是一种特殊类型的函数,与常量表达式密切相关,使用 consteval 关键字来声明即时函数。
主要特性
- 即时函数必须在编译时求值,如果无法在编译时计算函数的结果,编译器会报错。
- 即时函数只能在常量表达式的上下文中调用,并且调用的结果也会作为常量表达式使用。
- 即时函数可以进行递归调用,但递归调用也必须在编译时能够终止。
// constexpr 函数与 consteval 函数比较示例
#include <iostream>
// constexpr 函数:可以在编译时或运行时求值,具体取决于调用的上下文。
// 如果调用 constexpr 函数的参数是常量表达式,那么函数会在编译时求值;否则,函数会在运行时求值。
constexpr int add(int a, int b)
{
return a + b;
}
// consteval 即时函数
consteval int multiply(int a, int b)
{
return a * b;
}
int main()
{
// 编译时求值
constexpr int compileTimeSum = add(2, 3);
constexpr int compileTimeProduct = multiply(2, 3);
int x = 4, y = 5;
// 运行时求值
int runtimeSum = add(x, y);
// 错误:不能在运行时上下文调用 consteval 函数
// int runtimeProduct = multiply(x, y);
std::cout << "Compile-time sum: " << compileTimeSum << std::endl;
std::cout << "Compile-time product: " << compileTimeProduct << std::endl;
std::cout << "Runtime sum: " << runtimeSum << std::endl;
return 0;
}
5. char8_t 类型
在 C++20 之前,UTF - 8 编码的字符串通常使用 char 类型来表示。然而,char 类型既可以用于表示有符号整数,也可以用于表示无符号整数,这取决于编译器的实现,这就导致了在处理 UTF - 8 字符串时可能会出现一些未定义行为或移植性问题。为了解决这些问题,C++20 引入了 char8_t 类型,它是无符号的,专门用于表示 UTF - 8 编码的字符,从而提高了代码的安全性和可移植性。
char8_t 的特点
- 无符号类型:char8_t 是无符号类型,这意味着它的取值范围是从 0 到 255,避免了有符号 char 可能带来的符号扩展问题。
- 与 UTF - 8 编码紧密相关:char8_t 类型专门用于处理 UTF - 8 编码的字符和字符串,使得代码更加清晰和安全。
- 标准库支持:C++ 标准库提供了对 char8_t 的支持,例如 std::u8string 用于表示 UTF - 8 字符串。
#include <iostream>
#include <string>
int main()
{
// 使用 u8 前缀定义一个 char8_t 字符
char8_t ch = u8'A';
// 使用 u8 前缀定义一个 char8_t 字符串
const char8_t* str = u8"Hello, 世界!";
// 使用 u8 前缀定义一个 std::u8string 对象
std::u8string u8str = u8"你好,C++20!";
// 注意:std::cout 不能直接输出 char8_t 字符串,需要将其转换为 char 类型
std::cout << "Character: " << static_cast<char>(ch) << std::endl;
// 输出字符串
std::cout << "String: ";
for (const char8_t* p = str; *p; ++p)
{
std::cout << static_cast<char>(*p);
}
std::cout << std::endl;
// 输出 std::u8string 的长度
std::cout << "Length of u8string: " << u8str.length() << std::endl;
// 遍历 std::u8string 中的每个字符
for (char8_t ch : u8str)
{
std::cout << static_cast<char>(ch);
}
std::cout << std::endl;
return 0;
}
6. 智能指针优化
在 C++20 中,智能指针(如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr)本身没有语法上的重大改变,但 C++20 引入的一些新特性可以帮助我们更好地使用和优化智能指针的使用。
1)使用consteval和constexpr进行编译时计算
constexpr 和 consteval 可以在编译时执行代码,对于智能指针相关的常量初始化和编译时计算非常有用。
#include <iostream>
#include <memory>
// 编译时函数,返回一个 unique_ptr
consteval auto createUniquePtr()
{
return std::make_unique<int>(42);
}
int main()
{
// 在编译时创建 unique_ptr
constexpr auto ptr = createUniquePtr();
static_assert(*ptr == 42);
std::cout << *ptr << std::endl;
return 0;
}
2)使用 std::span 和智能指针结合
将智能指针管理的数组与 std::span 结合使用,避免不必要的复制。
#include <iostream>
#include <memory>
#include <span>
void printSpan(std::span<int> sp)
{
for (int num : sp)
{
std::cout << num << " ";
}
std::cout << std::endl;
}
int main()
{
auto arr = std::make_unique<int[]>(5);
for (int i = 0; i < 5; ++i)
{
arr[i] = i;
}
// 使用 std::span 包装智能指针管理的数组
std::span<int> sp(arr.get(), 5);
printSpan(sp);
return 0;
}
3)使用 std::jthread 和智能指针
使用智能指针管理 std::jthread 对象,确保线程资源的正确释放。
#include <iostream>
#include <memory>
#include <thread>
void threadFunction()
{
std::cout << "Thread is running." << std::endl;
}
int main()
{
auto threadPtr = std::make_unique<std::jthread>(threadFunction);
// 当 threadPtr 离开作用域时,线程会自动加入
return 0;
}
4)使用 std::atomic 和智能指针
C++20 对 std::atomic 进行了改进,可以使用 std::atomic<std::shared_ptr>
实现线程安全的共享指针操作。
#include <iostream>
#include <memory>
#include <atomic>
#include <thread>
std::atomic<std::shared_ptr<int>> atomicPtr;
void writer()
{
auto newPtr = std::make_shared<int>(42);
atomicPtr.store(newPtr, std::memory_order_release);
}
void reader()
{
std::shared_ptr<int> localPtr = atomicPtr.load(std::memory_order_acquire);
if (localPtr) {
std::cout << "Read value: " << *localPtr << std::endl;
}
}
int main()
{
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
return 0;
}
5)使用概念(Concepts)来约束智能指针的使用
C++20 引入的概念可以用于约束模板参数,确保智能指针操作的类型安全。
#include <iostream>
#include <memory>
#include <concepts>
// 定义一个概念,要求类型可以解引用
template <typename T>
concept Dereferenceable = requires(T t)
{
*t;
};
// 接受任何可解引用的类型
template <Dereferenceable Ptr>
void printValue(Ptr ptr)
{
std::cout << *ptr << std::endl;
}
int main() {
auto ptr = std::make_unique<int>(42);
printValue(ptr);
return 0;
}
7. constinit 变量
constinit 是 C++20 引入的一个新的变量声明说明符,用于确保变量在程序启动时进行常量初始化。与 constexpr 不同,constinit 不要求变量本身是常量,只要求它的初始化表达式是常量表达式。
#include <iostream>
// 使用 constinit 声明变量,虽然 globalValue 不是常量,但它的初始化表达式 10 * 2 是常量表达式
constinit int globalValue = 10 * 2;
int main()
{
// 访问 constinit 变量
std::cout << "Global value: " << globalValue << std::endl;
// 修改 constinit 变量的值
globalValue = 30;
std::cout << "Modified global value: " << globalValue << std::endl;
return 0;
}
8. 哈希查找优化
1)自定义哈希函数
对于自定义类型,需要提供自定义的哈希函数和相等比较函数。在 C++20 中,可以使用 std::hash 模板来简化哈希函数的定义。
#include <iostream>
#include <unordered_map>
#include <string>
// 自定义类型
struct Person
{
std::string name;
int age;
// 重载相等比较运算符
bool operator==(const Person& other) const {
return name == other.name && age == other.age;
}
};
// 自定义哈希函数
namespace std
{
template <>
struct hash<Person>
{
std::size_t operator()(const Person& p) const
{
// 使用 std::hash 组合多个成员的哈希值
auto nameHash = std::hash<std::string>{}(p.name);
auto ageHash = std::hash<int>{}(p.age);
return nameHash ^ (ageHash << 1);
}
};
} // namespace std
int main()
{
std::unordered_map<Person, std::string> personMap = {
{{ "Alice", 25 }, "Engineer"},
{{ "Bob", 30 }, "Doctor"}
};
Person target = { "Alice", 25 };
auto it = personMap.find(target);
if (it != personMap.end()) {
std::cout << target.name << " is a " << it->second << std::endl;
} else {
std::cout << "Person not found" << std::endl;
}
return 0;
}
2)std::erase_if
C++20 引入了 std::erase_if 函数,可用于删除满足特定条件的元素,避免了手动遍历和删除元素时的迭代器失效问题,同时也可能间接影响哈希表的性能。
#include <iostream>
#include <unordered_map>
int main()
{
std::unordered_map<int, std::string> myMap = {
{1, "Apple"},
{2, "Banana"},
{3, "Cherry"}
};
// 删除值为 "Banana" 的元素
std::erase_if(myMap, [](const auto& pair) {
return pair.second == "Banana";
});
for (const auto& pair : myMap)
{
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
3)调整哈希表参数
std::unordered_map 和 std::unordered_set 提供了一些成员函数来调整哈希表的参数,如 rehash 和 reserve,可以在插入大量元素之前预先分配足够的空间,减少哈希冲突的概率。
#include <iostream>
#include <unordered_map>
int main()
{
std::unordered_map<int, std::string> myMap;
// 预先分配足够的空间
myMap.reserve(100);
// 插入元素
for (int i = 0; i < 100; ++i)
{
myMap[i] = "Value " + std::to_string(i);
}
// 查找元素
auto it = myMap.find(50);
if (it != myMap.end()) {
std::cout << "Found: " << it->second << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}