
一、The Big Four
1.1 Concepts
C++中模板编程极大地简化了代码的编写,但同时也会带来一些使用上的疑惑,例如一下代码片段,判断两个数是否相等,只要是重载了== 运算符,自定义类型也可以使用该模板实例化对应的比较函数:
template <typename T>
auto isEqual(T left, T right) {
return left == right;
}
int main() {
cout << std::boolalpha;
double a = 2.334;
double b = 2.335;
cout << isEqual(2, 2) << endl;
cout << isEqual(b - a, 0.001) << endl;
return 0;
}
我们知道,浮点数因为在存储中涉及精度问题,所以不能直接去判断两个浮点数是否想定,需要自定义精度去判断。模板函数依然可以正确执行,但结果却不是我们想要的,因此我们需要使用该模板的类型做一些限制。假如说我们限定次函数的类型为整型,当然,这只是举个例子,如果单纯地限定为整型,也就没有使用模板的必要。我们可以采用C++11中的静态断言static_asser ,该断言会在编译期执行检测,判断类型是否为整型,如下:
template <typename T>
auto isEqual(T left, T right) {
static_assert(std::is_integral<T>::value);
return left == right;
}
此时使用浮点数进行比较,便会触发断言,编译失败,如下图所示。还有个问题就是此时断言在函数内部,也就是说必须通过模板实例化对应函数,然后执行该函数时才能触发断言,某些情况下很可能编译成功,在运行期才会发生错误。

C++17提供了concept机制,用来检查模板参数类型,代码如下:
template <typename T>
concept Integeral = std::is_integral<T>::value;
template <Integeral T>
auto isEqual(T left, T right) {
return left == right;
}
int main() {
cout << std::boolalpha;
double a = 2.334;
double b = 2.335;
cout << isEqual(2, 2) << endl;
cout << isEqual(b - a, 0.001) << endl;
return 0;
}

此时的报错信息更清晰,方便了我们去排查错误。如上的concept使用方法还有以下3种写法,效果是一样的,有人推崇第四种写法,省略了typename关键字,但是我觉得第一种写法更清晰:
template <typename T>
requires Integeral<T>
auto isEqual(T left, T right) { return left == right; }
template <typename T>
auto isEqual(T left, T right) requires Integeral<T> {
return left == right;
}
Integeral auto isEqual(Integeral auto left, Integeral auto right) { return left == right; }
1.2 Range library
ranges :代表一段元素,之前版本使用begin 和end 标识一段元素,那么用ranges有什么好处呢?
- 简化语法和操作;
- 防止begin,end迭代器的不配对使用;
- 使得类似管道
| 的串行操作成为可能。
相关概念:
- View:延迟计算,只有读权限
- Actions:即时处理,读或写
- Algorithms:操作range
- Views和Actions的串联操作
具体看代码(需要包含<ranges> 头文件):
简化操作:
std::sort 和std::ranges::sort 比较
std::vector<int> vec{3, 1, 2, 5, 6, 4};
std::sort(vec.begin(), vec.end());
std::copy(vec.begin(), vec.end(), std::ostream_iterator<int>{cout, " "});
std::ranges::sort(vec);
std::ranges::copy(vec, std::ostream_iterator<int>{cout, " "});
串联视图:
定义了一个vector,even用来筛选偶数,square返回参数的平方
std::vector<int> vec{1, 2, 3, 4, 5, 6};
auto even = [](int i) { return i % 2 == 0; };
auto square = [](int i) { return i * i; };
auto result = vec | std::views::filter(even) | std::views::transform(square);
for (auto i : result) {
cout << i << " ";
}
过滤和变换:
std::vector vec{1, 2, 3, 4, 5};
auto result = vec | std::views::transform([](int i) { return i * i; }) | std::views::take(3);
cout << std::accumulate(result.begin(), result.end(), 0) << endl;
1.3 Coroutines
这个特性没有太多的资料,编译器也没有完全实现,所以根据网上资料大概整理一下:
所谓协程,是一个函数,能在保持状态的时候暂停或者继续。具有关键字:
co_await :挂起协程,等待其它计算完成
co_return :退出协程
co_yield :弹出一个值,挂起协程,等待下一次调用,类似于Python中的生成器
限制:
协程不能使用可变参数、普通返回语句或占位符返回类型(auto或concept)。constexpr函数、构造函数、析构函数和主函数不能是协程。
以下代码是利用协程特性创建一个斐波那契数列的生成器(使用MSVC 编译器):
#include <coroutine>
#include <experimental/generator>
#include <iostream>
using std::cout, std::endl;
using namespace std::experimental;
generator<int> fibonacci(size_t num) {
int first = 0, second = 1;
int result;
for (size_t i = 0; i < num; ++i) {
result = first;
co_yield result;
first = second;
second = result + first;
}
}
int main() {
auto fibs = fibonacci(10U);
for (auto fib : fibs) {
cout << fib << " ";
}
return 0;
}
1.4 Modules
如果在MSVC 环境下,记得开启以下两项:

关键字:import ,export
新建测试文件test_module.ixx 和main.cpp ,注意模块文件后缀名为.ixx ,如果为.cpp 文件可能需要去命令行编译,使用方式和include 差不多,但是说使用模块比使用include 效率要高很多:
export module test_module;
namespace _module {
auto _print() { return "hello world"; }
export auto print() { return _print(); }
}
import test_module;
#include <iostream>
int main() {
std::cout << _module::print() << std::endl;
return 0;
}
模块优势:
- 没有头文件
- 声明和实现仍然可以分离,但没有必要
- 可以显式指定导出目标对象(函数,类)
- 没有头文件重复包含风险
- 模块名称可以相同
- 模块只处理一次,编译更快(头文件每次引入都需要处理)
- 预处理宏只在模块内有效
- 模块引入顺序无关紧要
二、Core Language
2.1 特性测试宏
通过它可以判断编译器是否支持某个功能,例如:
__has_cpp_attribute(fallthrough)
__cpp_binary_literals
__cpp_char8_t
__cpp_coroutines
__cpp_lib_concepts
__cpp_lib_ranges
__cpp_lib_scoped_lock
2.2 三路比较运算符<=>
(x <=> y) < 0
(x <=> y) > 0
(x <=> y) == 0
可以使用以下代码代替所有的比较运算符:
auto X::operator<=>(const X&) = default;
2.3 范围for循环初始化
在C++17中增加了if,switch 语句中的初始化,C++20中增加了范围for循环的初始化,如下:
struct Data {
std::vector<int> values;
};
Data getData() { return Data(); }
int main() {
for (auto data = getData(); auto &value : data.values) {
cout << value << " ";
}
return 0;
}
2.4 lambda表达式
C++20中需要显式捕获this指针
C++20之前,使用[=] 会隐式捕获到this 指针,C++20中移除了这一行为,如果需要访问this 指针,需要显式捕获:
[=, this]
lambda表达式支持模板语法
auto func = [](auto vec) { using T = typename decltype(vec)::value_type; };
auto func = []<typename T>(std::vector<T> vec) {
};
auto func = [](auto const& x){
using T = std::decay_t<decltype(x)>;
T copy = x; T::static_function();
using Iterator = typename T::iterator;
}
auto func = []<typename T>(const T& x){
T copy = x; T::static_function();
using Iterator = typename T::iterator;
}
auto func = [](auto&& ...args) {
return foo(std::forward<decltype(args)>(args)...);
}
auto func = []<typename …T>(T&& …args){
return foo(std::forward(args)...);
}
初始化包展开
template<class F, class... Args>
auto delay_invoke(F f, Args... args){
return [f, args...]{
return std::invoke(f, args...);
}
}
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...);
}
}
2.5 消除上下文中不必要的typename
template <typename IterT>
void workWithIterator(IterT it) {
typename std::iterator_traits<IterT>::value_type tmp(*it);
std::iterator_traits<IterT>::value_type tmp(*it);
}
2.6 consteval和constinit
constexpr函数可以在编译期执行,也可以在运行期执行,consteval函数只能在编译期执行,如果不满足要求则编译失败
constinit:强制指定以常量方式初始化
const char *GetStringDyn() { return "dynamic init"; }
constexpr const char *GetString(bool constInit) { return constInit ? "constant init" : GetStringDyn(); }
constinit const char *a = GetString(true);
constinit const char *b = GetString(false);
int main() { return 0; }
2.7 可以使用 using 引用 enum 类型
enum class Gender { Man, Women };
void foo1(Gender gender) {
switch (gender) {
case Gender::Man:
break;
case Gender::Women:
break;
default:
break;
}
return;
}
void foo2(Gender gender) {
switch (gender) {
using enum Gender;
case Man:
break;
case Women:
break;
default:
break;
}
return;
}
2.8 结构化绑定新增自定义查找规则
在C++20中可以自定义绑定的第几个是哪个类型,而且可以制定解绑的个数。
自定义实现:
- 在类外实现
get<int>(Type) 函数或者在类内实现Type::get<int>() 成员函数 - 在std命名空间特化tuple_size和tuple_element结构体
get<int>() 的返回路径数量必须与tuple_size 指定的数值相等,tuple_element 特化的索引数量(且必须从0开始)必须与tuple_size 指定的数值相等get<int N>() 函数中N的值对应的返回类型必须与tuple_element 对应索引指定的类型相同
struct A {
int m_a;
int m_b;
};
struct X : private A {
std::string val1;
std::string val2;
};
template <int N>
auto &get(X &x) {
if constexpr (0 == N) {
return x.val2;
}
}
namespace std {
template <>
class tuple_size<X> : public std::integral_constant<int, 1> {};
template <>
class tuple_element<0, X> {
public:
using type = std::string;
};
}
int main() {
X x;
auto &[y] = x;
return 0;
}
struct A {
int a;
int b;
};
struct X : protected A {
std::string value1;
std::string value2;
template <int N>
auto &get() {
if constexpr (N == 0)
return value1;
else if constexpr (N == 1)
return a;
}
};
namespace std {
template <>
class tuple_size<X> : public std::integral_constant<int, 2> {};
template <>
class tuple_element<0, X> {
public:
using type = std::string;
};
template <>
class tuple_element<1, X> {
public:
using type = int;
};
}
int main() {
X x;
auto &[y1, y2] = x;
return 0;
}
2.9 类类型的非类型模板参数
struct A {
int value;
constexpr bool operator==(const A &v) const { return value == v.value; }
};
template <auto a, auto b>
struct Equal {
static constexpr bool value = a == b;
};
int main() {
static constexpr A a{10}, b{20};
std::cout << std::boolalpha << Equal<a, b>::value << std::endl;
std::cout << std::boolalpha << Equal<a, a>::value << std::endl;
return 0;
}
2.10 new表达式的数组元素个数的推导
2.11 聚合初始化推导类模板参数
2.12 新增属性
[[no_unique_address]]
不怎么理解这个特性,所使用编译器也不支持这个属性,以后有时间再探究。
[[likely]]和[[unlikely]]
在分支语句中,告诉编译器哪个分支更容易执行,哪个分支不容易被执行,方便编译器做优化:
constexpr long long fact(long long num) noexcept {
if (num > 1) [[likely]]
return num * fact(num - 1);
else [[unlikely]]
return 1;
return 1;
}
[[nodiscard(reason)]]
表明返回值不可忽略,可以添加提示信息,如下代码所示:
[[nodiscard("This return value is important!")]] void *getData() {
return nullptr;
}
int main() {
getData();
return 0;
}

2.13 constexpr限制更宽松
- 虚函数可以为constexpr函数
- constexpr虚函数可以重写非constexpr虚函数
- 非constexpr虚函数可以重写constexpr虚函数
- constexpr函数:
- 可以使用
dynamic_cast 和typeid - 可以动态分配内存
2.14 atomic 智能指针
atomic<shared_ptr<T>>
atomic<weak_ptr<T>>
智能指针使用时保证线程安全
2.15 缩略函数模板
C++20中可以简化函数模板的写法,如下:
void f1(auto);
void f2(C1 auto);
void f3(C2 auto...);
void f4(const C3 auto*, C4 auto&);
template <class T, C U>
void g(T x, U y, C auto z);
三、Library
3.1 日历时区库std::chrono
头文件<chrono> ,简单使用:
auto y1 = year{2021};
auto y2 = 2021y;
auto m1 = month{7};
auto m2 = July;
auto d1 = day{21};
auto d2 = 21d;
year_month_day date1{2021y, July, 21d};
auto date2 = 2021y / July / 21d;
year_month_day date3{Monday[3] / July / 2021};
weeks w{1};
days d{w};
- 新的时钟类型:
utc_clock, tai_clock, gps_clock, file_clock
system_clock::time_point t = sys_days{2021y / July / 21d};
auto date = year_month_day{floor<days>(t)};
auto datetime = sys_days{2021y / July / 21d} + 21h + 52min + 33s;
cout << datetime << endl;
zoned_time t1 = {"America/Denver", datetime};
auto t2 = zoned_time{"America/Denver", local_days{Monday[1] / July / 2021} + 9h};
auto t3 = zoned_time{current_zone(), system_clock::now()};
3.2 std::span
某段连续数据的视图,不持有数据,不分配和销毁数据,类似于std::string_view 。拷贝操作高效,可通过运行期确定长度,也可通过编译期确定长度。
int data[42];
std::span<int, 42> sa{data};
std::span sb{data};
3.3 std::version
包含 C++ 标准库版本, 发布日期, 版权证书, 特性宏等
3.4 std::format
Python风格的字符串格式化
std::string str = std::format("hello {}", "world");
cout << str << endl;
3.5 std::numbers
数学常量库,如图可以看到包含了很多数学常量:

3.6 std::source_location
用于获取代码位置信息,对于日志库和错误信息比较方便:
#include <format>
#include <iostream>
#include <source_location>
using namespace std;
int main() {
auto loc = source_location::current();
string logInfo = format("{}:{}:{}: info: this is a note.", loc.file_name(),
loc.line(), loc.column());
cout << logInfo << endl;
return 0;
}
3.7 std::endian
判断当前环境为大端序还是小端序:
if constexpr (std::endian::native == std::endian::big) {
std::cout << "big-endian" << std::endl;
} else if constexpr (std::endian::native == std::endian::little) {
std::cout << "little-endian" << std::endl;
} else {
std::cout << "mixed-endian" << std::endl;
}
3.8 std::make_shared支持数组构造
3.9 std::remove_cvref
移除const,varient,引用属性
判断数组是否有界
四、其他更新
- string支持
starts_with 和ends_with - map支持
contains 查询某个键是否存在 <algorithm> 增加shift_left 和shift_right <numeric 增加midpoint 方法方便计算中位数,该方法可避免溢出,还有lerp 函数,计算线性差值
|