C++17、C++20等是C++语言的新标准版本。每个新的C++标准版本都引入了新的功能、语法和改进,以满足现代开发的需求并提供更好的开发体验。
C++20是C++语言的第六个标准版本,于2020年发布。C++20引入了许多新特性,包括概念(Concepts)、三路比较操作符(Three-way Comparison)、协程(Coroutines)、范围(Ranges)、模块化编程(Modules)、时间日期库(Date and Time)、数字分隔符(Digit Separators)等。C++20还对已有特性进行了改进,如constexpr、模板元编程、元组、智能指针等。
新的C++标准版本旨在提供更好的语法和功能,以提高开发效率、增强代码可读性、降低开发错误和提供更好的性能特性。为了使用新的C++标准,开发人员需要使用支持该标准的编译器,并相应地更新编译选项。
C++20出来已经一年多了,但是可以全面支持C++20新特性的编译器却没有多少。这从侧面反映了C++20的变化之大。然而,最为广大C++开发的程序员却没有做好迎接新特性的准备。一方面是由于C++的内容知识之多,难度之大,非一般程序员可以掌握,另一方面得益于C++强大的兼容性。30年前写的代码在今天编译器上依旧可以生成和稳定运行,这就话可不是白说的。但是C++20新特性确实可以简化代码,甚至从根本上改变了我们组织代码的方式。
侧面说明:C++的参考手册
我们可以看到C++11作为C++2.0版本,增加了很多内容,达到了12项。而C++17和C++20却只有7,8项。你可能觉得C++20和C++17增加的差不多,不够称之为大版本。如果细看就会发现C++20增加了三个独立的库:功能特性测试宏,概念库,范围库。这是C++17远远达不到的。C++20也正是因为有概念库,范围库而大放光彩。
C++标准的页数
我们同样可以发现相似的结论:C++11作为C++2.0版本,标准页数增加了600多页,这差不多是C++03的总页数。C++14页数几乎没变。C++17因引入了文件系统库这个基础性的功能库,外加上类型推断能力的增强和any新特性的引入,增加了不少页。C++20增加的页数和C++17增加的页数相差不大,但是由于C++标准的内容太多了,C++组委会更改了每页的大小,由美国的信纸大小改为A4大小。造成了页数增加看起来相差不大,其实内容确变化很多。
C++吸收的优秀库
format -> fmt GitHub - fmtlib/fmt: A modern formatting library
Range ->range https//http://github.com/ericniebler/range-v3
Coroutines-> libgo https://github.com/yyzybb537/libgo)
-> openmp GitHub - llvm-mirror/openmp: Mirror kept for legacy. Moved to https://github.com/llvm/llvm-project
…
C++20看到网上非常优秀的库而带来的新特性,就把它们增加到C++的新标准中,以方便了我们使用。但C++标准只是一个标准,具体的实现可能并不是C++标准的责任,标准库可能借鉴这些优秀的库,但不会完全照抄。
正面说明:要想说明正面说明,必须了解C++20增加了什么。这幅图大致几乎囊括了所有的新特性
Modules彻底改变了我们组织源码文件的方式,不需要在分.h和.cpp文件
Concepts改变了我们对模板类型的限制,我们不需要再去思考用语法技巧和语言特性去对模板类型做静态限制(编译期检查)了,这让我们很方便构造在编译期表现出大部分限制和规范(constraints)的模板
Ranges改变了我们使用算法或者算法组合的方式,为我们使用算法的提供了无限的可能
Coroutines让我们可以用同步的代码写出异步的效果
1)Moudules(模块)
它彻底改变了C++源码的组织方式,在项目的编写过程中,我们不必再区分.cpp和.h文件
一个例子
我们在test_m.cpp中写下如下代码:
import iostream
export moudule test_m
export void test_Func(){
std:cout<<"Test from C++20\n";
}
在主程序main.cpp中写下:
import test_m
int main(int argc, char** argv){
test_Func();
return 0;
}
我们可以发现程序仍然可以正常运行,不必再加入pragma once的声明
2)Ranges(范围库)
ranges中的range概念是一种类似于迭代器的东西。但传统的迭代器并不具有类型安全的特性。
其中我认为很有用的是ranges的相关概念——管道操作符
下面是网络上的示例代码:
// 使用管道操作符前
vector data { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result {
transform(reverse(drop(transform(filter(data,isOdd),doubleNum),2)),to_string)
};
不难发现,这段代码的可读性非常差,处理全都叠在了一起。让我们再看看用管道操作符的另外一种写法:
vector data { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto result { data
| views::filter([](const auto& value) { return value % 2 == 0; }
| views::transform([](const auto& value) { return value * 2.0; }
| views::drop(2)
| views::reverse
| views::transform([](int i) { return to_string(i); }
};
代码的可读性有了很大的提升,数据的处理流程更加清楚。而且使用这种处理方式不会产生中间数组,直接进行惰性计算。
3)Concepts(概念库)
它减小了对模板类型的限制,在项目构造过程中,我们可以不必考虑用语言特性对模板类作静态限制
我们不妨做一个对比:
在C++20之前,我们可以用如下方式对模板参数进行限制:
type supprot
trait等等
在C++20及之后,有了Concepts这一灵活的限制。我们可以直接使用。
定义一个concept
template<typename T>
concept test_Concept = requires(T x){
x-=1;
}
在使用时,我们对require限制的位置较为随意
/*函数前限制*/
template<typename T> requires test_Concept<T>
void Func(T t);
我们也可以这么写;
/*函数后限制*/
template<typename T>
void Func(T t) requires test_Concept<T>;
还有许多限制的方法,此处不再一一列出
PS.标准库也将提供一些concept
4)Coroutines(协程)
协程是一个可以记住自身状态,可随时挂起和执行的函数。
下面是一段别人写的简单的协程应用,我将在代码注释内讲解协程相关关键字的含义。
还有许多限制的方法,此处不再一一列出
PS.标准库也将提供一些concept
Coroutines(协程)
协程是一个可以记住自身状态,可随时挂起和执行的函数。
还有一个关键字叫做co_wait,含义是挂起协程以等待其他计算完成
1)[=, this] 需要显式捕获this
变量
C++20 之前 [=]
隐式捕获this
C++20 开始 需要显式捕获this: [=, this]
这个我个人觉得C++组委会觉得对于this的捕获其实更像是引用捕获,而不是值捕获。
2)模板形式的 Lambda 表达式
C++20支持在Lambda表达式中使用模板语法,其使用形式如下:
[]template<T>(T x) {/* ... */};
[]template<T>(T* p) {/* ... */};
[]template<T, int N>(T (&a)[N]) {/* ... */};
模板可以让程序泛化,而不必在乎类型,这是思考的基本出发点。但是我们却忘了模板还有一个最最基础的作用,那就是类型推断。比如函数模板,不用指定类型,使用最佳匹配。C++17更是增强了模板类型推断的能力,让模板更加省事好用。模板形式的Lambda表达是借助模板推断能力,让代码简洁,不用再去想类型推断的事情了。下次其他人问你,C++类型自动推断有几种方法,三种:auto,decltype,template T。具体好处见以下:
简化容器内部类型推断
C++20以前:
auto func = [](auto vec){
// using T = typename decltype(vec)::value_type;
using T = std::decay_t<decltype(x)>;
T copy{x};
T::static_function();
using Iterator = typename T::iterator;
}
C++20之后:
auto func = []<typename T>(vector<T> vec){
T copy{x};
T::static_function();
using Iterator = typename T::iterator;
}
简化完美转发类型推断
C++20以前:
auto func = [](auto&& ...args) {
return foo(std::forward<decltype(args)>(args)...);
}
C++20之后:
auto func = []<typename …T>(T&& …args){
return foo(std::forward(args)...);
}
支持初始化捕捉
C++20以前:
template<class F, class... Args>
auto delay_invoke(F f, Args... args){
return [f, args...]{
return std::invoke(f, args...);
}
}
C++20之后:
template<class F, class... Args>
auto delay_invoke(F f, Args... args){
return [f = std::move(f), args = std::move(args)...](){
return std::invoke(f, args...);
}
}
指定初始化(Designated Initializers)
struct Data {
int anInt = 0;
std::string aString;
};
Data d{ .aString = "Hello" };
在很多时候,我们可能由于成员过多,不记得构造函数的元素循序,进行构造是必须再次查看对应关系才能进行初始化。现在你只要知道你想初始化的条目即可完成正确的构造。帅不帅。
船型操作符 <=>
正规名称: 三路比较运算符
三路比较结果如下:
- (a <=> b) < 0 // 如果 a < b 则为 true
- (a <=> b) > 0 // 如果 a > b 则为 true
- (a <=> b) == 0 // 如果 a 与 b 相等或者等价 则为 true
一般情况: 自动生成所有的比较操作符(6个)
用法:auto X::operator<=>(const Y&) = default;
例子:
#include <compare>
class Point {
int x; int y;
public:
auto operator<=>(const Point&) const = default; // 比较操作符自动生成
}; // 等价于以下代码
// 与上述代码等价形式
class Point {
int x; int y;
public:
friend bool operator==(const Point& a, const Point& b){
return a.x==b.x && a.y==b.y;
}
friend bool operator< (const Point& a, const Point& b){
return a.x < b.x || (a.x == b.x && a.y < b.y);
}
friend bool operator!=(const Point& a, const Point& b) {
return !(a==b);
}
friend bool operator<=(const Point& a, const Point& b) {
return !(b<a);
}
friend bool operator> (const Point& a, const Point& b) {
return b<a;
}
friend bool operator>=(const Point& a, const Point& b) {
return !(a<b);
}
};
高级情况:
支持非默认比较:只需自定义操作符==
支持部分自定义:自定义自己的某些比较操作符
支持选择比较格的类型(见离散数学的定义):严序,弱序,偏序
标准库类型支持 <=> 如:vector, string, map, set, sub_match, …
范围 for 循环语句支持初始化语句
C++17开始支持switch 语句初始化和if 语句初始化,现在C++20发扬风范,开始支持范围 for 循环初始化 ,其样例代码如下:
struct Foo {
std::vector<int> values;
};
Foo GetData() {
return Foo();
}
int main() {
for (auto data = GetData(); auto& value : data.values) {
// Use 'data’
}
}
人可能说这就是一个语法糖,也就是能简化代码而已,没什么大差别。其实不然,这些初始化都是有作用域的,在相应的语句结束之后会自动释放内存。大多数情况下,判断变量和循环变量在操作完之后也就没有什么作用了,及时清理内存和作用域限制给我们帮了极大的忙,这也就是C++标准一直推荐加入语句内初始化的原因。
非类型模板形参支持字符串
C++支持对非类型模板参数。但是C++对其进行了限制,仅以下类型可以:
整型常量/枚举类型
指向对象/函数/成员变量的指针(不允许浮点数指针)
对象/函数的左值引用
std::nullptr_t
现在C++20放松了这个限制,增加了两条:
浮点数指针)
某些类类型(有一些限制)
这次专门支持了字符串类型,这对于正则表达式有很大的意义,不同的pattern是不同的类型,我们就可以利用型别信息做很多事情。比如相互的替换,编译期判断正则表达式的正确性等等等。
示例
template<auto& s> void DoSomething() {
std::cout << s << std::endl;
}
auto m { ctre::match<"[a-z]+([0-9]+)">(str) }
[[likely]], [[unlikely]]
先验概率指导编译器优化
switch (value) {
case 1: break;
[[likely]] case 2: break;
[[unlikely]] case 3: break;
}
[[nodiscard(reason)]]
表明返回值不可抛弃, 加入理由的支持
[[nodiscard("Ignoring the return value will result in memory leaks.")]]
void* GetData() { /* ... */ }
日历(Calendar)功能
增加日历和时区的支持
只支持公历(Gregorian calendar)
其他日历也可通过扩展加入, 并能和 进行交互
示例:
初始化 年、月、日的方法
// creating a year
auto y1 = year{ 2019 };
auto y2 = 2019y;
// creating a mouth
auto m1 = month{ 9 };
auto m2 = September;
// creating a day
auto d1 = day{ 18 };
auto d2 = 18d;
创建完整的日期
year_mouth_day fulldate1{2019y, September, 18d};
auto fulldate2 = 2019y / September / 18d;
year_mouth_day fulldate3{Monday[3]/September/2019}; // Monday[3] 表示第三个星期一
产生了新的时长单位, 类似于秒, 分钟, …
using days = duration<signed interger type of at least 25bits,
ratio_multiply<ratio<24>, hours::period>>;
using weeks = ...;
using mouths = ...;
using years = ...;
时长互相转化
weeks w{1}; // 1 周
days d{w}; // 将 1 周 转换成天数
标准库对为测量时间,提供了计数系统时钟。C++为了增加对时区的支持,标准库拓展提供了地理时区时钟,就和酒店里面各个地方的时间钟一样。
C++20前的系统时钟:
system_clock:日历时钟,可被手工调整
steady_clock:稳定增长的时钟,由机器tick计算而来
high_resolution_clock:拥有最小计次周期的时钟
std::filesystem::file_time_type::clock:文件系统时钟
C++20增加的时钟:
utc_clock:表示协调世界时,人为“协调”(闰秒),从00:00:00, 1 January 1970开始
tai_clock:表示国际原子时,恒定时间,从00:00:00, 1 January 1958开始。
gps_clock:表示GPS时间,没有闰秒,从00:00:00, 6 January 1980开始
file_clock:用于文件系统,它的epoch未指定。
local_t:表示本地(虚拟)时间,未指定时区。
关于UTC,TAI,GPS时间的背景知识,可查阅相关文档。
示例:
新增system_clock相关的别名
template<class Duration>
using sys_time = std::chrono::time_point<std::chrono::system_clock, Duration>;
using sys_seconds = sys_time<std::chrono::seconds>;
using sys_days = sys_time<std::chrono::days>;
日期和时间点的相互转化
system_clock::time_point t = sys_days{ 2019y / September / 18d }; // date -> time_point
auto yearmonthday = year_month_day{ floor<days>(t) }; // time_point -> date
日期 + 时间
auto t = sys_days{2019y/September/18d} + 9h + 35min + 10s; // 2019-09-18 09:35:10 UTC
时区用例
zoned_time ac_zt("Antarctica/Casey", sys_days{2021y/September/15d}+16h+45min);
auto zt = zoned_time{ current_zone(), system_clock::now() };
const auto& tzdb = get_tzdb();
const time_zone* local_tz = tzdb.locate_zoned("America/Los_angeles");
auto ad_zt = zoned_time{ "America/Denver", local_days{Wednesday[3] / September / 2019} + 9h };
auto utc_zt = zoned_time{ "Etc/UTC", ad_zt};
std::span
头文件
某段连续数据的”视图”
能够读和写数据
不持有数据, 不管理数据即不分配和销毁数据
拷贝非常快(类似 string_view)
推荐按值方式传递给函数(像内置类型,平凡类型的数据,类指针类型,一般都推荐值传递)
不支持数据跨步(stride)
可通过运行期确定长度也可编译器确定长度
示例:
int data[42];
std::span<int, 42> a {data}; // fixed-size: 42 ints
std::span<int> b {data}; // dynamic-size: 42 ints
std::span<int, 50> c {data}; // compilation error
std::span<int> d{ ptr, len }; // dynamic-size: len ints
支持以下操作:
Iterators (begin, cbegin, rbegin, …)
front(), back(), operator[], data()
size(), size_bytes(), empty()
first(count): 返回由前n个元素组成的子段
last(count): 返回由后n个元素组成的子段
subspan(offset, count): 返回由offset到offset+count-1元素组成的子段
特性测试宏
通过它可以判断编译器是否支持某个功能, 例如
语言特性
__has_cpp_attribute(fallthrough)
__cpp_binary_literals
__cpp_char8_t
__cpp_coroutines
标准库特性
__cpp_lib_concepts
__cpp_lib_ranges
__cpp_lib_scoped_lock
__cpp_lib_any
__cpp_lib_bool_constant
__cpp_lib_filesystem
version
包含 C++ 标准库版本, 发布日期, 版权证书, 特性宏等
这对于我们检查编译器特性和标准库信息十分有用,对跨平台十分重要,可以提前检验编译器和标准库是否符合当前编码要求。
constexpr的意义
constexpr(常量表达式)是为了解决C++历史遗留问题,它一种比const 更严格的束缚, 它规定了表达式本身在编译期间可知。具体来说有以下特性:
const是运行期常量 constexpr是编译期常量
const其实是readonly(只读),而constexpr才是const(常量)
constexpr 所修饰的函数,返回值则不一定要求是编译期常量 ==>函数返回值一定是编译时常量,且在编译期进行计算(C++20 consteval)
为了突破类类型的只读含义,C++发明了一个关键词mutable。含义是:即使处于const对象里,对象里被mutable修饰的内容仍然可变(运行期),这对const成员函数有极大的意义,在此就不展开了。此时,就更能理解只读的含义了:constexpr是一定全部不可以变的,这才是真正的常量。编译期常量有什么好处呢?函数可以在编译期就进行计算,减少运行期的开销(还记得开头C++之父的目标么),第二个就是可以在元编程世界里大显身手。
constexpr函数你可以理解成一个只于输入有关的函数,也就是说输入相同的参数,结果必定相同。相当于Python的一等函数,或者说闭包函数。(可能描述不准确)。
在C++11标准中,不允许常量表达式作用于virtual的成员函数,因为virtual表示的是运行时的行为,这与constexpr“可以在编译时期进行计算”的意义是冲突的。“在编译时期进行计算”是对编译器的一个建议,C++11标准并没有强制要求编译器一定要在编译时期对常量表达式函数进行计算。要求函数返回值一定是编译时常量,且在编译期进行计算,C++20增加了consteval。也正是有了consteval,C++20放松了constexpr修饰的要求。
C++11:
constexpr函数的返回类型以及所有形参必须是字面值类型
constexpr函数必须有且只有一条return语句
但是constexpr函数可以返回一个非常量
constexpr函数定义建议放在头文件中,但是不强制要求
C++14:
constexpr函数可以使用分支控制语句,拥有多个返回语句
constexpr函数可以使用循环控制语句
可以修改生命周期和常量表达式相同的变量了
C++17:
constexpr可以修饰lambda表达式
constexpr可以修饰if语句 => if constexpr
C++20:
constexpr 虚函数
constexpr 的虚函数可以重写非 constexpr 的虚函数
非 constexpr 虚函数可以重写 constexpr 的虚函数
constexpr 函数可以:
使用 dynamic_cast() 和 typeid
动态内存分配
更改union成员的值
包含 try/catch
但是不允许throw语句
在触发常量求值的时候 try/catch 不发生作用
constexpr string & vector
std::string
和 std::vector
类型现在可以作为 constexpr
为 constexpr
支持反射做准备
consteval 函数
constexpr 函数可能编译期执行, 也可以在运行期执行。consteval 只能在编译器执行, 如果不满足要求编译不通过。
constinit
强制指定以常量方式初始化,这就可以放在编译期初始化了。注意,这只要求了初始化,但没有要求只读。如果要求只读就和constexpr一样了,如果要求可变,好像和普通的变量没什么区别。哦,那编译期初始化了他会放在哪呢?它的生命周期又是多少呢?这样一想,是不是有点像static了!!!
哈哈哈,现在以const开头有关的关键字的特性都掌握了把,是不是有些混乱?这个时候我们就要从两个维度来看问题了,一个是初始化实在什么时候,另一个是可不可以改变,具体结论在下图,慢慢悟吧:
用 using 引用 enum类型
为了提高枚举类型的安全性和数据类型指定性(继承内置整数类型),防止命名冲突,进而引入了emun class来对枚举进行作用域限制。但是代码中必须标明类型名,这样很不方便。因此,C++20使用了与命名空间,类内类型一致解决方法:using。但是为了减少暴露、防止入侵式代码,建议在尽可能小的作用域打开枚举。
示例
enum class CardTypeSuit {
Clubs,
Diamonds,
Hearts,
Spades
};
std::string_view GetString(const CardTypeSuit cardTypeSuit) {
switch (cardTypeSuit) { mingmin222
case CardTypeSuit::Clubs:
return "Clubs";
case CardTypeSuit::Diamonds:
return "Diamonds";
case CardTypeSuit::Hearts:
return "Hearts";
case CardTypeSuit::Spades:
return "Spades";henbufangbian
}
}
std::string_view GetString(const CardTypeSuit cardTypeSuit) {
switch (cardTypeSuit) {
using enum CardTypeSuit; // 这里是关键
case Clubs: return "Clubs";
case Diamonds: return "Diamonds";
case Hearts: return "Hearts";
case Spades: return "Spades";
}
}
这是一个十分类似于Python 的字符串格式化。github上有一个非常经典优秀的实现库:https://github.com/fmtlib/fmt
示例
std::string s = std::format("Hello, world!\n"); // s == "Hello, world!\n"
std::string s = std::format("The answer is {}.", 42); // s == "The answer is 42."
std::string s = std::format("I'd rather be {1} than {0}.", "right", "happy"); // s == "I'd rather be happy than right."
std::vector<int> v = {1, 2, 3}; std::string s = fmt::print("{}\n", v); // s == "[1, 2, 3]"
// 当然还支持各种格式化输出如:时间,日期, 年月
// 还支持各种输出流指定比如文件,控制台,数据库
增加数学常量
头文件
包含 e, log2e, log10e pi, inv_pi, inv_sqrt pi ln2, ln10, sqrt2, sqrt3, inv_sqrt3, egamma
他们的精度很高,我们再也不需要自己去查找定义这些数值了
std::source_location
用来代替__FILE__、__LINE__、__FUNCTION__、__DATA__、__TIME__、
C++类进行封装,这下好处可大了,真的是太好了,越来越C++了!
构造用source_location::current()
获取的是调用方所在位置
他们主要是用于来获取代码位置, 对于日志和错误信息尤其有用!
示例
void LogInfo(string_view info, const source_location& location = source_location::current())
{
cout << location.file_name() << ":" << location.line() << ": " << info << endl;
}
int main()
{
LogInfo("Welcome to Cpp");
}
位运算
加入循环移位, 计数0和1位等功能
endian 指示标量类型的端序
bit_cast: 类型转化
has_single_bit(): 检查一个数是否为二的整数次幂
bit_ceil(): 寻找不小于给定值的最小的二的整数次幂
bit_floor(): 寻找不大于给定值的最大的二的整数次幂
bit_width(): 寻找表示给定值所需的最小位数
rotl(): 计算逐位左旋转的结果
rotr(): 计算逐位右旋转的结果
countl_zero(): 从最高位起计量连续的 0 位的数量
countl_one(): 从最高位起计量连续的 1 位的数量
countr_zero(): 从最低位起计量连续的 0 位的数量
countr_one(): 从最低位起计量连续的 1 位的数量
popcount(): 计量无符号整数中为 1 的位的数量
注释:
左右旋转:又称循环移位
左旋转:二进制左移的时候,低位不用0填充而用高位溢出的部分填充。
右旋转:二进制右移的时候,高位不用0填充而用低位溢出的部分填充。
一些小更新
字符串支持 starts_with, ends_with
map 支持 contains 查询是否存在某个键
list 和 forward list 的 remove, remove_if 和 unique 操作返回 size_type 表明删除个数
增加 shift_left, shift_right
midpoint 计算中位数, 可避免溢出
lerp 线性插值 lerp( float a, float b, float t ) 返回 a+t(b-a)
新的向量化策略 unsequenced_policy(execution::unseq)
这些小的更新很有用也很容易用。
【文章福利】小编推荐自己的Linux C++技术交流群:【1106675687】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送大厂面试题。
资料领取直通车:大厂面试题锦集+视频教程
Linux C/C++开发学习网站:C/C++Linux服务器开发/后台架构师(免费订阅,每天晚上八点直播)
概念(Concepts):引入了概念,用于对模板参数进行约束。
三路比较运算符(Three-way comparison operators):通过添加<=>
操作符,简化对象之间的比较操作。
初始化捕获扩展(Init-capture extension):Lambda函数现在可以使用初始化列表来捕获变量。
协程(Coroutines):引入了协程支持,使得异步编程更加方便和可读。
模块(Modules):提供了对模块化编程的支持,取代了传统的头文件包含方式。
强制执行(Contracts):引入了合约机制,在代码中定义前置条件、后置条件和不变式等,并对其进行检查。
Ranges库(Ranges library):新增了一个统一且功能强大的范围操作库,简化了对容器、视图以及迭代器的处理。
格式化字符串库(Formatted output library):新增了std::format
函数,提供类型安全和格式化字符串输出能力。
无歧义数字分隔符(Digit separator for readability):可以在数字字面量中使用单撇号 '
进行分隔以提高可读性。
块化编程是一种软件设计方法,旨在将程序分解为相互独立、可重用的模块。每个模块都有清晰定义的接口,并且可以通过这些接口与其他模块进行交互。
C++20中引入了与模块相关的特性,主要包括以下几点:
模块声明语法:使用module
关键字来声明一个模块,并使用export module
来导出该模块的接口。
导入语句:使用import
关键字来导入其他模块,并可以选择性地导入其中的具体内容。
接口文件:每个模块都需要提供一个对应的接口文件(以.ixx
或.cppm
扩展名结尾),其中包含了该模块的公共接口。
编译时链接:在编译过程中,编译器会自动处理模块之间的依赖关系,只编译必要的部分并进行链接,以提高构建效率。
完整性检查:编译器会对导入和导出进行完整性检查,确保模块之间的依赖关系正确。
这些特性使得C++20中的模块化编程更加直观和灵活,并且能够改善编译速度和代码组织结构。
Lambda函数是一种匿名函数,它可以在代码中临时定义和使用,通常用于简化代码或者作为函数对象传递。
在C++20中,对lambda函数做出了以下改进:
支持更多的捕获方式:除了以值和引用的方式捕获外,现在还支持以init-capture(初始化捕获)的方式捕获变量。
隐式捕获:当省略了lambda函数的参数列表时,编译器会自动推导需要捕获的变量。
constexpr lambda:允许将lambda函数声明为constexpr(常量表达式),这样就可以在编译期间求值。
模板参数推导:可以在lambda函数中使用模板参数,并通过类型推导来确定具体类型。
这些改进使得C++20中的lambda函数更加灵活、强大和易用。
在C++20中,"concepts"(概念)是一种新的语言特性。它主要用于模板元编程,帮助程序员定义和约束模板参数的类型,并提供了更清晰、更可读的错误信息。
通过使用概念,可以在编译期对模板参数进行静态检查,以确保其满足特定的要求。这使得模板代码更加健壮、易于理解和调试。
概念可以用来描述类型特征、操作符重载要求、函数调用形式等,以限制模板参数的范围。如果一个模板参数不符合所定义的概念要求,则编译器会在编译期报错。
协程(Coroutine)是一种轻量级的并发编程方式,它可以在函数执行过程中暂停和恢复,允许程序按照非抢占式的方式进行协作式调度。通过使用协程,可以简化异步编程、处理高并发场景和实现状态机等任务。
在C++20中,引入了与协程相关的特性:
协程关键字:引入了co_await
、co_yield
和co_return
等关键字来支持协程操作。
std::coroutine_traits
:提供了用于自定义协程类型的模板类,使得用户可以根据需要定义自己的协程特性。
协程句柄(Coroutine handle):通过std::coroutine_handle
类型,可以创建、销毁、恢复和暂停协程,并在必要时传递状态信息。
生成器(Generator):引入了基于范围迭代器的生成器概念,通过使用生成器函数和生成器对象,可以方便地实现迭代序列。
同步编程和异步编程之间的主要区别在于程序的执行方式和控制流。
在同步编程中,代码按照顺序依次执行,每个操作都会阻塞当前线程,直到完成后才会继续执行下一个操作。这种方式适合于简单、线性的任务,但当需要处理大量耗时的操作或等待外部资源时,会导致程序出现延迟或阻塞。
而在异步编程中,程序可以同时执行多个任务,不必等待前一个任务完成再进行下一个任务。通过使用回调函数、事件驱动机制或协程等技术,在执行耗时操作时能够释放当前线程,使其能够处理其他任务。这样可以提高程序的并发性和响应性,并避免阻塞问题。
C++20引入了一些支持异步编程的机制,包括:
协程(Coroutines):通过使用co_await
关键字和协程对象来实现轻量级的异步操作。
std::jthread
:新的线程类,在线程退出时自动清理资源。
无堆栈定时器(Coroutine-friendly Timers):提供基于协程的定时器功能,方便管理异步时间相关的操作。
协作式取消(Cooperative Cancellation):允许协作式地取消正在运行的协程,避免资源泄漏。
原子等待(Atomic Wait):提供原子等待的机制,可以在异步操作中等待多个事件的完成。
在 C++20 中,引入了 std::format 函数,用于格式化输出字符串。相较于传统的格式化方式(如 printf、sprintf),std::format 提供了一些改变和增强,包括:
类型安全:std::format 使用{}作为占位符,可以直接在占位符内指定类型,并通过参数传递对应的值,避免了类型不匹配的问题。
位置参数:可以使用索引来明确指定参数的位置,例如"{0} {1}"将会按照给定的顺序填充对应位置上的参数。
命名参数:可以使用命名参数来标识要填充的具体参数,例如"{name} is {age} years old"。
格式化选项:支持丰富的格式化选项,比如对齐、宽度、精度等。
自定义格式化:可以自定义类型的格式化方式,并通过特定的格式标识符进行调用。
C++20引入了一些新的数据结构和算法,以下是其中一些值得注意的变化:
Ranges库:引入了新的范围(Range)概念,使得对序列的操作更加灵活和直观。
Three-way Comparison(三路比较):通过std::compare_three_way函数以及默认的operator<=>运算符,简化了比较操作。
Calendar and Time Zone库:提供了新的日期、时间和时区处理功能,使得处理时间更加方便。
Coroutines(协程):引入了协程支持,可以更轻松地实现异步操作和事件驱动编程。
各种新增的数据结构和容器:如std::span用于代表连续内存范围、std::bit_span用于位级别操作、std::slist作为单链表容器等。
数学函数扩展:增加了一些数学函数,例如对浮点数进行舍入、取整等操作。
C++20 是一个像 C++11 一样的大版本,非常让人期待。对于 (a <=> b),如果a > b ,则运算结果>0,如果a < b,则运算结果<0,如果a==b,则运算结果等于0,注意下,运算符的结果类型会根据a和b的类型来决定,所以我们平时使用时候最好直接用auto,方便快捷。
C++20是C++编程语言的一个版本,是ISO/IEC 14882标准的最新版本,于2020年12月发布。C++20是C++17的后继版本,包含了许多新的特性和改进,例如概念、模块、协程、范围、初始化、反射、同步、多线程等等。C++20的目标是提高C++的表现力、可读性和可维护性,同时保持C++的高效性和灵活性。
C++20 还给我们准备了语法糖;concept 可以写在模板的参数里面。如果要找C++版本更新的脉络的话,更愿意把它分为3个部分:语言特性、标准库、模板。语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
C++20的新特性和改进包括:
概念:C++20引入了概念,这是一种新的语言特性,可以用来描述类型的要求和约束。
模块:C++20引入了模块,这是一种新的组织代码的方式,可以替代头文件。
协程:C++20引入了协程,这是一种新的并发编程模型,可以用来编写高效的异步代码。
范围:C++20引入了范围,这是一种新的语言特性,可以用来简化代码。
初始化:C++20引入了新的初始化语法,可以用来简化代码。
反射:C++20引入了反射,这是一种新的语言特性,可以用来在运行时获取类型信息。
同步:C++20引入了新的同步原语,可以用来编写高效的并发代码。
多线程:C++20引入了新的多线程库,可以用来编写高效的并发代码。
// Hello from C++20
import <iostream>;
using std::cout, std::endl;
int main() {
cout << "Hello, world!" << endl;
int answer {42};
cout << "The answer to life, the universe, and everything is "
<< answer << endl;
}
你好,世界!
// Hello from C++20
import <iostream>;
using std::cout, std::endl;
int main() {
cout << "Hello, world!" << endl;
int answer {42};
cout << "The answer to life, the universe, and everything is "
<< answer << endl;
}
如何编译?
这里只介绍 macOS 环境。由于 macOS 预装的 clang 版本尚不支持模块机制,我们需要使用更新的开发版本。如果有 Homebrew,直接 brew install llvm 即可安装开发版本的 llvm。接下来使用下列语句即可编译:
/opt/homebrew/opt/llvm/bin/clang++ <filename> -std=c++20 -fmodules
指定 -fmodules 是由于模块机制的实现尚不完善,需要手动开启。
一些新东西
作为第一个 C++20 程序,它看起来还是相当地不一样的。
首先注意到的就是 import 语句。C++20 引入了模块 (Module) 机制,彻底(至少是在理论上)宣告了使用头文件+实现文件组织多文件代码历史的终结。
然而,目前各大编译器对模块机制的支持都不甚完善。为了让代码至少看上去在使用这个新特性,我们使用一种称为模块映射 (Module Mapping)的机制。这种机制可以将头文件映射为模块。目前我还没有深究模块机制的具体内容,因此这里只是这样用来让代码能跑起来而已。Clang为 libc++ 标准库提供了模块映射,因此只需 import <header>,其中 header 为标准库的头文件名,即可导入对应的头文件映射成的模块。
Clang 也定义了一个模块 std,其包含了整个 C++ 标准库。为了方便兼容更早的版本,我在学习过程中将不会直接导入 std 模块,而是导入各头文件映射的模块。
接下来,注意到一个长得很奇怪的变量定义语句。这是 C++11 引入的通用初始化 (Universal Initialization,或称 Brace-init) 机制,旨在提供一种统一、安全的初始化方式。具体来说,在 C++11 前,有许多方式都可以初始化一个变量:
int n = 0;
Point p = {0, 1};
double f(3.14);
char s1[] = {'H', 'e', 'l' ,'l', 'o', '\0'};
而在 C++11 后,建议使用的统一初始化方式为在变量名后的大括号内写上变量的初始内容。比如:
using std::string, std::vector;
int a {0};
string s {"Hello!"};
string s2 {s}; // 复制构造
int b[] {3, 4, 5};
// 直接指定 vector 内容;这在以前是无法实现的
vector<string> v {"Alpha", "Beta", "Gamma"};
同时,通用初始化还加强了安全检查。例如:
int f = 1145141919810;
这个语句最多只会造成编译器给出警告(字面值超出了 int 的范围导致存储的值改变了)。
int f {1145141919810};
但这个语句一定会造成编译错误,因为通用初始化要求初始化提供的参数能不经改变地被指定类型存储。
你可能会觉得这种方式不如以前直接用等号来得自然,尤其是对于基础类型。但是只要你将“初始化”和“赋值”看成两个不同的过程,你就能更好地理解为什么要这么做了。
比如,考虑这样的代码:
void f() {
static int x = 5;
cout << ++x << endl;
}
这里的等号似乎给你一种暗示,它和下面的写法等价:
void f() {
static int x;
x = 5;
cout << ++x << endl;
}
然而,第一种写法中,每次调用时 x 的值都在改变,而第二种写法中,输出的永远是 6。这是因为第一种写法的等号实际上是“初始化”的语义,它只会在 x 定义时执行一次,而非第二种写法的“赋值”语义。因此,将等号的“初始化”语义拆分出来提供一个单独的语法,实际上是更好的做法。
更新:最近发现了使用通用初始化的另一个理由,那就是其可以避免恼人分析 (Most Vexing Parse),下面的例子都来源于这个维基百科链接:
void f(double my_dbl) {
int i(int(my_dbl));
}
这之中 i 所在的那一行实际上被编译器解释为声明一个名为 i 的函数,其返回类型为 int,而有一个名叫 my_dbl 的参数。这是 C++ 允许在参数名前后加额外的括号导致的,但总归来说这仍是一个十分反直觉的事情。
struct Timer {};
struct TimeKeeper {
explicit TimeKeeper(Timer t);
int get_time();
};
int main() {
TimeKeeper time_keeper(Timer());
return time_keeper.get_time();
}
这个例子中,main 函数的第一行同样被解释为函数声明。如果我们使用统一初始化语法,就可以避免这个问题:
int main() {
TimeKeeper time_keeper {Timer{}};
return time_keeper.get_time();
}
联系客服