个人博客地址: https://cxx001.gitee.io
前言
自C++11这个大版本更新以来,后来陆续有两次小版本迭代C++14、C++17,它们主要是对C++11的补充扩展,并没有增加太多大的特性。
而这次的C++20,和当年C++11一样,又是一次重大更新,有人甚至说这是一门新语言。
1. 新增关键字
- concept 用于约束模板参数的类型范围,从而限制模板的实例化范围。
#include <iostream>template <typename T>
concept Arithmetic = std::is_arithmetic_v<T>; // 约束参数T只能是算术类型template <Arithmetic T>
T add(T a, T b)
{return a + b;
}int main()
{int result = add(5, 10); // 正确,int 是算术类型//double result = add("Hello", "World"); // 错误,字符串不是算术类型return 0;
}
一些常用的类型约束还有:
std::is_integral<T>::value:检查类型 T 是否为整数类型(包括有符号和无符号整数)。
std::is_floating_point<T>::value:检查类型 T 是否为浮点类型。
std::is_pointer<T>::value:检查类型 T 是否为指针类型。
std::is_array<T>::value:检查类型 T 是否为数组类型。
std::is_class<T>::value:检查类型 T 是否为类类型。
std::is_enum<T>::value:检查类型 T 是否为枚举类型。
std::is_function<T>::value:检查类型 T 是否为函数类型。
std::is_same<T, U>::value:检查类型 T 是否与类型 U 相同。
- requires 用于在concept中定义更复杂的约束条件。通过使用
requires
关键字,可以指定更多的条件,以进一步限制模板参数的属性。
template <typename T>
concept Incrementable = requires(T a) {{ ++a } -> std::same_as<T&>; // 要求 a 可以递增,并返回 T&std::is_arithmetic_v<T>; // 要求 T 是算术类型
};template <Incrementable T>
T increment(T value) {++value;return value;
}int main() {int result = increment(5); // 正确,int 可以递增// double result = increment(3.14); // 错误,double 不可递增return 0;
}
- constinit 用于指定一个对象必须以静态初始化方式进行初始化,提高程序的性能和可预测性。
在 C++ 中,对象的初始化可以分为两种方式:静态初始化和动态初始化。静态初始化是指在程序启动时或者在第一次使用之前,由编译器自动完成的初始化过程。动态初始化是指在运行时通过代码执行来完成的初始化过程。
使用 constinit
关键字可以确保对象以静态初始化方式进行初始化,从而避免了动态初始化的开销和潜在的不确定性。
constinit int x = 42; // 编译器将确保 x 以静态初始化方式进行初始化。
注意,constinit
关键字只能用于具有静态存储期的对象,例如全局变量、静态变量或者在命名空间作用域内定义的变量。它不能用于局部变量或者动态分配的对象。
- consteval 用于声明一个函数必须在编译时进行求值,编译器将在编译时执行该函数,并将结果替换到调用点。以提供更高的性能和优化机会。
consteval int square(int x) {return x * x;
}
注意与C++11的constexpr
区别,上面示例用这两个效果一样,都可以实现在编译期求值。不过constexpr
用于声明可以在编译时求值的常量表达式或函数,而consteval
关键字只能用于函数声明,不能用于变量或其他语句。
-
co_await 、co_return、co_yield 这些是C++20引入协程编程新增的关键字,协程编程是一种轻量级的并发编程机制,**它允许函数在执行过程中暂停和恢复。**在后面会详细介绍。
-
char8_t 用于表示 UTF-8 编码的字符。
char8_t
类型的大小为一个字节(8位),与 UTF-8 编码方式一致。可以使用char8_t
类型的指针来遍历和操作 UTF-8 字符串,而无需进行字符集转换或编码处理。更加方便了。
#include <iostream>int main() {const char8_t* utf8String = u8"Hello, 世界!";std::cout << "UTF-8 String: " << reinterpret_cast<const char*>(utf8String) << std::endl;return 0;
}
- import、module 这是C++20引入的又一重大特性,模块。后面详细介绍。(和js的模块机制很像~)
2. 模块
在C++20中,终于引入了模块化编程的概念,这和各脚本语言中的模块概念很类似。模块化编程旨在提供更好的代码组织、封装和可重用性。
模块化编程通过使用module
关键字来定义模块,将相关的实体(变量、函数、类等)封装在一个单独的单元中,并使用import
关键字在其他模块中导入所需的实体。
// vs2022中添加模块文件math.ixx
export module math; // 创建模块mathexport int add(int a, int b)
{return a + b;
}export int subtract(int a, int b)
{return a - b;
}
// main.cpp
#include <iostream>import math; // 导入模块int main()
{int result = add(5, 3);std::cout << result;return 0;
}
使用模块化编程可以带来许多优势,例如:
- 更好的代码组织:模块将相关的实体封装在一起,使代码结构更清晰,易于维护和理解。
- 更好的封装:模块可以选择性地导出实体,控制对外部的可见性,提供更好的封装性。
- 更快的编译速度:由于模块只包含所需的实体,而不是整个头文件,编译速度可能会更快。
- 避免头文件的预处理器宏:模块不需要使用预处理器宏来处理头文件的重复包含和条件编译。
3. 新增ranges标准库组件
C++20引入了一种新的标准库组件,称为Ranges(范围),用于简化和增强对序列(包括容器、数组、迭代器等)的操作和处理。
Ranges的设计目标是通过使用管道操作符(|
)和函数式编程的风格,提供一种更现代、更可读、更易于组合的方式来操作序列。使用Ranges,你可以将多个操作链接在一起,形成一个连续的操作链,每个操作都是对序列进行转换、筛选、排序等操作。
#include <iostream>
#include <ranges>
#include <vector>int main()
{std::vector<int> numbers = { 1, 2, 3, 4, 5 };// 首先对numbers数组中每个元素*2变为{2, 4, 6, 8, 10},然后筛选被3整除的元素。// 整个操作流程使用管道符"|"连接。auto result = numbers | std::views::transform([](int n) { return n * 2; })| std::views::filter([](int n) { return n % 3 == 0; });for (int n : result) {std::cout << n << " ";}// 输出: 6return 0;
}
需要注意的是,使用Ranges需要包含头文件<ranges>
,并使用命名空间std::views
和std::ranges
来访问Ranges的操作。
下面列出Ranges的一些常用的操作:
-
转换序列:使用
std::views::transform
将序列中的元素进行转换,例如将整数序列转换为字符串序列或进行数值计算。 -
筛选元素:使用
std::views::filter
根据特定条件筛选序列中的元素,例如筛选出满足某个谓词的元素。 -
排序序列:使用
std::views::sort
对序列进行排序操作,可以根据元素的某个属性进行排序。 -
分组元素:使用
std::views::group_by
将序列中的元素按照某个条件进行分组,例如按照元素的奇偶性进行分组。 -
聚合操作:使用
std::views::reduce
或std::views::transform_reduce
对序列中的元素进行聚合操作,例如计算序列的总和、平均值等。 -
切片操作:使用
std::views::take
、std::views::drop
或std::views::slice
对序列进行切片操作,例如获取前几个元素或跳过前几个元素。 -
迭代操作:使用
std::ranges::for_each
对序列中的每个元素进行迭代操作,例如打印序列中的所有元素。 -
查找元素:使用
std::ranges::find
或std::ranges::find_if
在序列中查找特定的元素或满足某个条件的元素。 -
合并序列:使用
std::views::concat
将多个序列合并为一个序列,方便进行统一的操作。 -
转换为容器:使用
std::ranges::to
将序列转换为特定类型的容器,例如将范围视图转换为std::vector
或std::list
。
注意std::views::xx和std::ranges::xx区别,std::views下的操作是懒操作,在用到时才执行,并且不会修改原序列,返回一个操作后的新视图。而std::ranges下的操作是立即执行,操作修改原序列,不返回任何值。
4. 协程
这特性又是一大干货哈,终于是引入标准了。也是很多脚本语言常用特性,感觉越往后发展,C++编码门槛会越来越低哈,会和写脚本一样丝滑~~
协程是一种轻量级的并发编程机制,本质是一个特殊函数,只是它允许函数在执行过程中暂停和恢复。协程通常用于处理异步操作,例如网络请求、文件读写等。
主要关键字:
co_await
:co_await
用于在协程内部暂停执行,通常用于等待异步操作完成。- 当协程遇到
co_await
表达式时,它将被暂停,并在异步操作完成后恢复执行。
co_yield
:co_yield
用于生成值并将其发送给调用者。它通常用于生成序列中的下一个值。- 当协程遇到
co_yield
表达式时,它会生成值并将其返回给调用者,然后暂停协程的执行,以便在需要时继续生成更多值。
co_return
:co_return
用于结束协程的执行并返回一个值。它通常用于返回协程的最终结果。- 当协程遇到
co_return
表达式时,它将结束执行,并将指定的值返回给调用者。
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>//! coro_ret 协程函数的返回值,内部定义promise_type,承诺对象
template <typename T>
struct coro_ret {struct promise_type;using handle_type = std::coroutine_handle<promise_type>;//! 协程句柄handle_type coro_handle_;coro_ret(handle_type h): coro_handle_(h){}coro_ret(const coro_ret&) = delete;coro_ret(coro_ret&& s): coro_handle_(s.coro_){s.coro_handle_ = nullptr;}~coro_ret(){//! 自行销毁if (coro_handle_)coro_handle_.destroy();}coro_ret& operator=(const coro_ret&) = delete;coro_ret& operator=(coro_ret&& s){coro_handle_ = s.coro_handle_;s.coro_handle_ = nullptr;return *this;}//! 恢复协程,返回是否结束bool move_next(){coro_handle_.resume();return coro_handle_.done();}//! 通过promise获取数据,返回值T get(){return coro_handle_.promise().return_data_;}//! promise_type就是承诺对象,承诺对象用于协程内外交流struct promise_type {promise_type() = default;~promise_type() = default;//! 生成协程返回值auto get_return_object(){return coro_ret<T> { handle_type::from_promise(*this) };}//! 注意这个函数,返回的就是awaiter//! 如果返回std::suspend_never{},就不挂起,//! 返回std::suspend_always{} 挂起//! 当然你也可以返回其他awaiterauto initial_suspend(){// return std::suspend_never{};return std::suspend_always {};}//! co_return 后这个函数会被调用void return_value(T v){return_data_ = v;return;}//!auto yield_value(T v){std::cout << "yield_value invoked." << std::endl;return_data_ = v;return std::suspend_always {};}//! 在协程最后退出后调用的接口。//! 若 final_suspend 返回 std::suspend_always 则需要用户自行调用//! handle.destroy() 进行销毁,但注意final_suspend被调用时协程已经结束//! 返回std::suspend_always并不会挂起协程(实测 VSC++ 2022)auto final_suspend() noexcept{std::cout << "final_suspend invoked." << std::endl;return std::suspend_always {};}//void unhandled_exception(){std::exit(1);}// 返回值T return_data_;};
};// 这就是一个协程函数
coro_ret<int> coroutine_7in7out()
{// 进入协程看initial_suspend,返回std::suspend_always{};会有一次挂起std::cout << "Coroutine co_await std::suspend_never" << std::endl;// co_await std::suspend_never{} 不会挂起co_await std::suspend_never {};std::cout << "Coroutine co_await std::suspend_always" << std::endl;co_await std::suspend_always {};std::cout << "Coroutine stage 1 ,co_yield" << std::endl;co_yield 101;std::cout << "Coroutine stage 2 ,co_yield" << std::endl;co_yield 202;std::cout << "Coroutine stage 3 ,co_yield" << std::endl;co_yield 303;std::cout << "Coroutine stage end, co_return" << std::endl;co_return 808;
}int main(int argc, char* argv[])
{bool done = false;std::cout << "Start coroutine_7in7out ()\n";// 调用协程,得到返回值c_r,后面使用这个返回值来管理协程。auto c_r = coroutine_7in7out();// 第一次停止因为initial_suspend 返回的是suspend_always// 此时没有进入Stage 1std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();// 此时是,co_await std::suspend_always{}std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();// 此时打印Stage 1std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;return 0;
}
Start coroutine_7in7out ()
Coroutine isn't done ret =0
Coroutine co_await std::suspend_never
Coroutine co_await std::suspend_always
Coroutine isn't done ret =0
Coroutine stage 1 ,co_yield
yield_value invoked.
Coroutine isn't done ret =101
Coroutine stage 2 ,co_yield
yield_value invoked.
Coroutine isn't done ret =202
Coroutine stage 3 ,co_yield
yield_value invoked.
Coroutine isn't done ret =303
Coroutine stage end, co_return
final_suspend invoked.
Coroutine is done ret =808
5. Lambda表达式更新
就是进行了些小扩展,具体细节这个不介绍了,本质就是一个匿名函数。
6. constexpr常量表达式更新
也是增强了,限制更少了,具体细节这个也不介绍了,本质就是常量化,编译期求值。
7.原子(Atomic)智能指针
C++20引入了原子智能指针,这是对C++原子操作的一种扩展。原子智能指针允许你在多线程环境中安全地共享和操作智能指针。std::atomic
原子智能指针可以保证对智能指针的操作是线程安全的,并且可以防止数据竞争。
#include <atomic>
#include <iostream>
#include <memory>
#include <thread>
#include <vector>// 共享的数据结构
struct Data {int value;Data(int val): value(val){}
};// 全局共享的std::shared_ptr,使用std::atomic包装
std::atomic<std::shared_ptr<Data>> sharedData;// 线程函数,从共享的std::shared_ptr中读取数据
void worker(int threadId)
{// 从原子std::shared_ptr中加载数据std::shared_ptr<Data> localPtr = std::atomic_load(&sharedData);// 使用本地副本进行操作std::cout << "Thread " << threadId << " read value: " << localPtr->value << std::endl;
}int main()
{// 初始化共享的std::shared_ptrsharedData.store(std::make_shared<Data>(42));// 创建线程池std::vector<std::thread> threads;for (int i = 0; i < 4; ++i) {threads.emplace_back(worker, i);}// 等待所有线程完成for (std::thread& thread : threads) {thread.join();}return 0;
}
Thread 2 read value: 42Thread
Thread 1 read value: 0 read value: 42
42
Thread 3 read value: 42
由于我们使用原子操作来加载std::shared_ptr
,多个线程可以同时访问和操作它而不会发生竞态条件。所以打印的结果是随机的。
8.原子引用类型std::atomic_ref
C++20 中引入的一种引用类型,用于支持原子操作。它允许您在不使用指针的情况下对共享数据进行原子操作。std::atomic_ref
可以用于提供线程安全的访问和修改共享变量的能力。
#include <atomic>
#include <iostream>
#include <thread>int main()
{int sharedValue = 0;std::atomic_ref<int> atomicSharedValue(sharedValue);std::thread thread1([&]() {for (int i = 0; i < 100000; ++i) {atomicSharedValue.fetch_add(1, std::memory_order_relaxed);}});std::thread thread2([&]() {for (int i = 0; i < 100000; ++i) {atomicSharedValue.fetch_add(1, std::memory_order_relaxed);}});thread1.join();thread2.join();std::cout << "Final shared value: " << sharedValue << std::endl;return 0;
}
Final shared value: 200000
9. 自动合流和可中断的线程
- 线程自动合流是指在线程执行完毕后,程序自动等待线程的结束并回收线程资源,而无需显式调用线程的
join()
方法。这使得线程的合流变得更加便捷和安全,避免了忘记合流或手动合流时出现的问题。
#include <chrono>
#include <iostream>
#include <thread>void threadFunction()
{std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Thread is done." << std::endl;
}int main()
{std::jthread myThread(threadFunction);std::cout << "Main thread is doing some work." << std::endl;// 不需要手动调用 join(),std::jthread 会自动合流// 线程会在这里自动合流std::cout << "Main thread is done." << std::endl;return 0;
}
Main thread is doing some work.
Main thread is done.
Thread is done.
- 可中断线程是指线程具有一种机制,允许它在执行过程中被其他线程或外部事件中断,然后执行特定的操作。这使得可以更灵活地控制线程的执行。以前一般都是用一个全局变量来外部控制线程中断,容易出错,也不灵活。
#include <iostream>
#include <thread>
#include <stop_token>
#include <chrono>void worker(std::stop_token token) {while (!token.stop_requested()) {// 执行工作std::this_thread::sleep_for(std::chrono::seconds(1));}std::cout << "Thread is interrupted." << std::endl;
}int main() {std::jthread t(worker);// 主线程等待一段时间后请求线程停止std::this_thread::sleep_for(std::chrono::seconds(3));t.request_stop();t.join(); // 阻塞,等待线程返回后往下执行std::cout << "Main thread is done." << std::endl;return 0;
}
Thread is interrupted.
Main thread is done.
10. 新的同步库
C++20引入了新的同步库,该库提供了多种同步工具,以帮助开发人员编写并发和多线程代码。
下面是一些C++20同步库的主要组件:
std::latch
:std::latch
是一个计数器,它允许您等待某个事件发生。当计数器归零时,所有等待的线程都将被唤醒。使用std::latch::wait()
等待计数器归零。
#include <iostream>
#include <latch>
#include <thread>void worker(std::latch& lt, int id)
{std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Thread " << id << " is done." << std::endl;lt.count_down(); // 减少计数器
}int main()
{const int numThreads = 3;std::latch latch(numThreads);for (int i = 0; i < numThreads; ++i) {std::thread(worker, std::ref(latch), i).detach();}// 等待所有线程完成,即当计数器为0时才往下执行latch.wait();std::cout << "All threads are done. " << std::endl;return 0;
}
Thread 1 is done.
Thread 0 is done.Thread 2 is done.All threads are done.
std::latch
可以用于控制一组线程的同步点,等待它们都完成后再进行下一步操作。这对于需要等待多个线程完成某项任务后再继续的情况非常有用。
-
std::barrier
:std::barrier
是一个同步原语,它允许一组线程相互等待,直到所有线程都到达某个点。只有当所有线程都到达屏障点时,这些线程才能继续执行。与
std::latch
不同,std::barrier
允许线程多次参与同步,即在达到同步点后,线程可以再次加入到下一轮同步中。
#include <barrier>
#include <iostream>
#include <thread>
#include <vector>const int numThreads = 3;
std::barrier barrier(numThreads);void worker(int id)
{std::cout << "Thread " << id << " is ready." << std::endl;barrier.arrive_and_wait(); // 等待所有线程达到同步点std::cout << "Thread " << id << " continues." << std::endl;barrier.arrive_and_wait(); // 再次等待所有线程达到同步点std::cout << "Thread " << id << " is done." << std::endl;
}int main()
{std::vector<std::thread> threads;for (int i = 0; i < numThreads; ++i) {threads.emplace_back(worker, i);}for (std::thread& thread : threads) {thread.join();}std::cout << "All threads are done." << std::endl;return 0;
}
Thread 1 is ready.Thread 2 is ready.Thread 0 is ready.
Thread 0Thread continues.Thread
2 continues.1continues.
Thread Thread 1Thread 0 is done. is done.
2 is done.All threads are done.
在这个示例中,我们使用了两次同步点。std::barrier
允许线程多次参与同步,适用于需要多轮协作的情况,例如迭代式的并行计算或其他需要多次同步的场景。在每个同步点,所有线程都会等待,直到所有线程都到达同步点后才会继续执行。这可以用于更复杂的多线程协作任务。
std::promise
和std::future
:std::promise
和std::future
提供了一种在不同线程之间传递数据的方式。std::promise
允许您在一个线程中设置一个值或异常,而std::future
可以在另一个线程中获取该值或异常。您可以使用std::promise::set_value()
设置值,使用std::promise::set_exception()
设置异常,并使用std::future::get()
获取值。
#include <iostream>
#include <thread>
#include <future>void do_work(std::promise<int>& p) {int result = 42; // 执行一些工作并产生一个结果p.set_value(result); // 设置结果
}int main() {std::promise<int> p;std::future<int> f = p.get_future();std::thread(do_work, std::ref(p)).detach();int result = f.get(); // 阻塞等待并获取结果std::cout << "Result: " << result << std::endl;return 0;
}
Result: 42
这在多线程之间通信和协作非常有用。
std::semaphore
:std::semaphore
是一种信号量,可以用于控制并发访问共享资源的线程,确保同时只有有限数量的线程可以访问共享资源,从而避免竞争条件和提高多线程程序的性能。
#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>std::counting_semaphore<10> sem(2); // 创建一个初始计数为2的信号量, 10是最大值void worker(int id)
{sem.acquire(); // 获取资源,如果没有资源则等待std::cout << "Thread " << id << " acquired resource." << std::endl;// 模拟工作std::this_thread::sleep_for(std::chrono::seconds(2));sem.release(); // 释放资源std::cout << "Thread " << id << " released resource." << std::endl;
}int main()
{std::vector<std::thread> threads;for (int i = 0; i < 5; ++i) {threads.emplace_back(worker, i);}for (std::thread& thread : threads) {thread.join();}return 0;
}
Thread 1 acquired resource.Thread
0 acquired resource.
Thread Thread 4 acquired resource.1 released resource.ThreadThread 0 released resource.2acquired resource.
Thread Thread 3 acquired resource.2 released resource.Thread 4released resource.Thread 3 released resource.
11. 其它更新
1. 指定初始化
它允许您在初始化复合数据结构(如结构体和数组)时,为特定的成员或元素提供初始化值,而不必按顺序初始化所有成员或元素。并且可以直接在声明时初始化。
#include <iostream>
#include <vector>struct Point {int x = 1;int y = 2;int z = 3;
};int main()
{Point p1 = { .x = 10, .y = 20 };Point p2 = { .y = 70, .z = 80 };std::cout << "p1: x=" << p1.x << " y=" << p1.y << " z=" << p1.z << std::endl;std::cout << "p2: x=" << p2.x << " y=" << p2.y << " z=" << p2.z << std::endl;return 0;
}
p1: x=10 y=20 z=3
p2: x=1 y=70 z=80
2. 航天飞机操作符<=>
也叫三路比较运算符,用于比较两个对象的关系,返回值是std::strong_ordering
类型,该类型包括三个可能的值,std::strong_ordering::less
、std::strong_ordering::equal
和std::strong_ordering::greater
,分别表示左操作数小于、等于或大于右操作数。
#include <iostream>
#include <compare>int main() {int a = 5;int b = 7;std::strong_ordering result = a <=> b;if (result == std::strong_ordering::less) {std::cout << "a is less than b" << std::endl;} else if (result == std::strong_ordering::equal) {std::cout << "a is equal to b" << std::endl;} else if (result == std::strong_ordering::greater) {std::cout << "a is greater than b" << std::endl;}return 0;
}
<=>三路比较运算符对比传统的<, =, >,更加方便简洁,内部实现了3种关系比较,不需要分别一个个手动判断了。例如std::strong_ordering
类型的结果可以直接用于排序算法,对于自定义类型排序只需要重载operator<=>
运算符一次,然后可以轻松地进行比较操作。
#include <algorithm>
#include <compare>
#include <iostream>
#include <vector>struct Person {std::string name;int age;// 按年龄大小排序,std::sort内部自动按这个排序算法auto operator<=>(const Person& other) const{return age <=> other.age; // 如果要按从大到小反过来就行了, other.age <=> age}
};int main()
{std::vector<Person> people = {{ "Alice", 30 },{ "Bob", 25 },{ "Charlie", 35 },{ "David", 28 }};// 使用 std::sort 对容器进行排序std::sort(people.begin(), people.end());// 输出排序后的结果for (const auto& person : people) {std::cout << person.name << " - " << person.age << std::endl;}return 0;
}
Bob - 25
David - 28
Alice - 30
Charlie - 35
3.范围for循环支持初始化语句
在前面C++17中我们讲了if-switch
支持初始化语句,现在for循环语句遍历容器时也支持初始化语句了。
#include <iostream>
#include <vector>int main()
{std::vector<int> numbers = { 1, 2, 3, 4, 5 };// 使用 C++20 范围 for 循环的初始化语句for (int i = 0; int value : numbers) {std::cout << "Value at index " << i << ": " << value << std::endl;++i; // 初始化语句中声明的 i 变量用于记录索引}return 0;
}
Value at index 0: 1
Value at index 1: 2
Value at index 2: 3
Value at index 3: 4
Value at index 4: 5
4. 非类型模板形参支持字符串
这意味着您可以在模板中使用字符串作为模板参数,以实现更灵活的泛型编程。
#include <iostream>
#include <string>template <const char* str>
void printString() {std::cout << str << std::endl;
}int main() {constexpr const char* message = "Hello, World!";printString<message>(); // 以字符串作为模板参数return 0;
}
注:在vs2022上编译不过,可能是这个特性在当前编译器上还没有完美支持。
5. [[likely]], [[unlikely]]标记
它们是用于标记代码分支的建议性属性,以帮助编译器优化执行路径。这些属性的主要目的是告诉编译器哪些分支更可能被执行,以便它可以更好地进行优化。
[[likely]]
属性:用于标记代码分支,表示这个分支更可能被执行。这有助于编译器在生成机器代码时对这个分支进行更好的优化。[[unlikely]]
属性:用于标记代码分支,表示这个分支更不可能被执行。这有助于编译器避免在生成机器代码时对这个分支进行过多的优化,以提高性能。
if (condition) [[likely]] {// 告诉编译器这个分支很可能会执行// 在性能敏感的代码路径上使用
} else [[unlikely]] {// 告诉编译器这个分支不太可能会执行// 在不太可能执行的代码路径上使用
}
这些属性是可选的,编译器可以根据它们来进行代码优化,但不是必需的。它们主要用于提高性能,并且在性能敏感的代码路径上使用得最多,以确保编译器对这些路径进行更好的优化。
请注意,这些属性的效果取决于编译器的实现,不同的编译器可能会有不同的优化策略。因此,在使用这些属性时,最好进行性能测试,以确保它们对代码的性能产生了预期的影响。
6. 日历和时区功能
C++20引入了标准库中的<chrono>
头文件的重大更新,其中包括了对日历和时区的新功能。这些功能使得在C++中处理日期、时间和时区更加容易和灵活。
以下是C++20中日历和时区功能的主要亮点:
- 日历支持:C++20引入了
std::chrono::year_month_day
、std::chrono::year_month
和std::chrono::weekday
等类型,以更方便地表示日期和时间。这些类型可以帮助您执行日期算术操作,如添加或减去日期、计算两个日期之间的时间间隔等。 - 格式化和解析:C++20引入了
std::format
函数和std::chrono::from_stream
函数,用于方便地格式化和解析日期和时间。这些函数可以将日期和时间以不同的格式进行字符串表示,并将字符串解析为日期和时间对象。 - 时区支持:C++20引入了
std::chrono::zoned_time
类型,用于表示带有时区信息的日期和时间。这使得在不同的时区之间进行转换和比较变得更容易。此外,C++20还引入了std::chrono::current_zone()
函数,用于获取当前的本地时区。 std::chrono::sys_time
类型:std::chrono::sys_time
是一个系统级别的时钟,用于表示与特定时钟不相关的时间点。这对于在不同时钟之间进行时间计算和比较非常有用。- 时钟和时钟间隔:C++20引入了
std::chrono::leap
时钟,用于处理闰秒,以及std::chrono::den
时钟间隔,用于表示任意固定时钟周期。这些时钟和时钟间隔类型增加了时间计算的灵活性。 - 时区数据库:C++20的
std::chrono
库中包含了一个时区数据库,其中包含了大量的时区信息,允许您轻松地执行与时区相关的操作。
这些功能的引入使得C++在日期、时间和时区处理方面变得更加强大和标准化。使用这些功能,您可以更容易地处理不同时区的时间、执行日期算术操作、进行时间间隔计算等。
7. std::span
这是一个用于表示一段连续内存的非拥有式、轻量级的容器。std::span
的目的是提供对现有内存的安全引用,而不进行内存分配或拷贝。通过它来引用数组传递使用就不会有越界风险,它内部自动实现了边界检查。它配合STL容器一起使用,更加灵活。
#include <iostream>
#include <vector>
#include <span>int main() {std::vector<int> data = {1, 2, 3, 4, 5};std::span<int> span(data.data(), data.size());// 使用 std::span 遍历数据for (int value : span) {std::cout << value << " ";}std::cout << std::endl;// 修改数据,span 反映了原始数据的更改span[2] = 99;// 再次遍历数据,看看修改后的结果for (int value : span) {std::cout << value << " ";}std::cout << std::endl;return 0;
}
1 2 3 4 5
1 2 99 4 5
8. 新增特性测试宏
C++20引入了一组特性测试宏,用于在代码中检测编译器是否支持特定的C++20功能或库。
以下是一些常用的C++20特性测试宏:
-
__cpp_concepts
:用于测试是否支持C++20的概念(Concepts)特性。#ifdef __cpp_concepts // 在支持概念的编译器中编写的代码 #endif
-
__cpp_consteval
:用于测试是否支持C++20的consteval
特性,该特性允许在编译时执行函数。#ifdef __cpp_consteval // 在支持 consteval 的编译器中编写的代码 #endif
-
__cpp_modules
:用于测试是否支持C++20的模块(Modules)特性。#ifdef __cpp_modules // 在支持模块的编译器中编写的代码 #endif
-
__cpp_ranges
:用于测试是否支持C++20的范围(Ranges)库特性。#ifdef __cpp_ranges // 在支持范围库的编译器中编写的代码 #endif
-
__cpp_coroutines
:用于测试是否支持C++20的协程(Coroutines)特性。#ifdef __cpp_coroutines // 在支持协程的编译器中编写的代码 #endif
-
__cpp_lib_concepts
、__cpp_lib_consteval
、__cpp_lib_modules
、__cpp_lib_ranges
、__cpp_lib_coroutines
等:用于测试编译器是否支持C++20库中的相关功能。#ifdef __cpp_lib_ranges // 在支持范围库的编译器中编写的代码 #endif
9. using可以为enum类型取别名
它允许您为枚举类型定义更具有意义的别名,这可以帮助提高代码的可读性和可维护性。
#include <iostream>// 定义一个枚举类
enum class Color {Red,Green,Blue
};int main() {// 使用 using 引用枚举类类型using ColorType = Color;ColorType color = Color::Red;// 使用引用的别名来声明变量if (color == ColorType::Red) {std::cout << "Color is Red" << std::endl;} else {std::cout << "Color is not Red" << std::endl;}return 0;
}
10. std::format格式化库
类似于C语言中的printf
函数或Python中的str.format()
方法。std::format
库的目标是提供一种类型安全、可扩展和国际化友好的方式来构建格式化字符串。
使用{}
作为占位符,并且支持添加格式说明符。
#include <iostream>
#include <format>int main() {int age = 30;std::string name = "Alice";double pi = 3.14159265359;// 使用 std::format 格式化字符串std::string formatted = std::format("Name: {}, Age: {}, Pi: {:.2f}", name, age, pi);std::cout << formatted << std::endl;return 0;
}
11. 增加数学常量
在C++20中,标准库引入了一组常见的数学常量,这些常量定义在<numbers>
头文件中。这些数学常量是通过std::numbers
命名空间提供的,可以用于进行数学计算,例如π、自然对数的底数e等。
以下是一些C++20中引入的数学常量示例:
-
π(圆周率):可以使用
std::numbers::pi
访问。double pi = std::numbers::pi;
-
e(自然对数的底数):可以使用
std::numbers::e
访问。double e = std::numbers::e;
-
黄金比例:可以使用
std::numbers::phi
访问。double phi = std::numbers::phi;
-
平方根2:可以使用
std::numbers::sqrt2
访问。double sqrt2 = std::numbers::sqrt2;
-
平方根3:可以使用
std::numbers::sqrt3
访问。double sqrt3 = std::numbers::sqrt3;
-
自然对数的2:可以使用
std::numbers::ln2
访问。double ln2 = std::numbers::ln2;
-
自然对数的10:可以使用
std::numbers::ln10
访问。double ln10 = std::numbers::ln10;
这些数学常量使得在C++中执行常见的数学计算更加方便和可读。它们以类型安全的方式提供了数学常数,避免了传统的宏定义或手动输入常数值的问题。
12. std::source_location
它允许在代码中获取当前文件的名称、当前行号、当前列号以及调用点的函数名称。这对于调试和日志记录非常有用,可以帮助定位代码中的问题。
std::source_location
定义在<source_location>
头文件中,并提供了以下常用成员函数:
file_name()
:返回当前源文件的名称。line()
:返回当前源文件的行号。column()
:返回当前源文件的列号。function_name()
:返回调用点的函数名称。
#include <iostream>
#include <source_location>void printSourceLocation(const std::source_location& loc = std::source_location::current()) {std::cout << "File: " << loc.file_name()<< " Line: " << loc.line()<< " Column: " << loc.column()<< " Function: " << loc.function_name()<< std::endl;
}int main() {printSourceLocation();return 0;
}
File: C:\Users\cxx\Desktop\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.cpp Line: 18 Column: 5 Function: main
13. [[nodiscard(reason)]]
在C++17和C++20中,可以使用[[nodiscard]]
属性来告诉编译器应该注意忽略函数的返回值。C++20进一步扩展了这个特性,允许您提供一个可选的字符串参数,以解释为什么返回值应该被注意忽略。如果返回值没有被使用,编译则会给出这个警告信息。
#include <iostream>[[nodiscard("Please check the return value for error handling")]] int divide(int a, int b)
{if (b == 0) {// 返回值应该用于错误处理return -1;}return a / b;
}int main()
{int result = divide(10, 2); // 正确使用返回值std::cout << "Result: " << result << std::endl;divide(10, 0); // 忽略返回值,但编译器会发出警告return 0;
}
已启动重新生成...
1>------ 已启动全部重新生成: 项目: ConsoleApplication1, 配置: Debug x64 ------
1>正在扫描源以查找模块依赖项...
1>math.ixx
1>正在编译...
1>math.ixx
1>ConsoleApplication1.cpp
1>C:\Users\cxx\Desktop\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.cpp(20,11): warning C4858: 正在放弃返回值: Please check the return value for error handling
1>ConsoleApplication1.vcxproj -> C:\Users\cxx\Desktop\ConsoleApplication1\x64\Debug\ConsoleApplication1.exe
1>已完成生成项目“ConsoleApplication1.vcxproj”的操作。
========== “全部重新生成”: 1 成功,0 失败,0已跳过 ==========
========= 重新生成 开始于 3:04 PM,并花费了 01.026 秒 ==========
14. 增加循环移位,计算位中0和1数量等功能
这些功能是通过C++20的标准库中的<bit>
头文件引入的。
- 循环左移和循环右移:
std::rotl
用于执行循环左移操作,std::rotr
用于执行循环右移操作。注意普通左移2位会将位移出并丢弃,而循环左移2位会将位重新循环回来,保持位数不变,即将移除的最高位的位数添加回数的最低位。
#include <iostream>
#include <bit>int main() {unsigned int value = 0b1100; // 二进制表示 1100// 循环左移两位unsigned int result_left = std::rotl(value, 2); // 结果为 0b0011,二进制表示 0011// 循环右移两位unsigned int result_right = std::rotr(value, 2); // 结果为 0b0011,二进制表示 0011std::cout << "Left Rotation: " << result_left << std::endl;std::cout << "Right Rotation: " << result_right << std::endl;return 0;
}
- 计算位中0和1的数量:
std::countr_zero
用于计算从右侧开始的连续0的数量,std::countr_one
用于计算从右侧开始的连续1的数量。
#include <iostream>
#include <bit>int main() {unsigned int value = 0b11001100; // 二进制表示 11001100int zeros = std::countr_zero(value); // 连续0的数量为 2int ones = std::countr_one(value); // 连续1的数量为 2std::cout << "Count of Zeros: " << zeros << std::endl;std::cout << "Count of Ones: " << ones << std::endl;return 0;
}
以上就是C++20的主要新增特性了,当然还一些细节扩展,那些就待使用时再查阅相关文档了。总结下来,C++11主要是引入了类型自动推导的auto,智能指针。C++20主要是模块、协程、原子操作。总体发展线路可以看出,C++编码风格在向脚本语言靠拢了。