初探现代 C++:C++17 之后的有趣特性
看到一个视频才发现自己似乎对 C++17 之后的新特性不是很了解。这片文章记录了我发现的一些有趣的新特性。
本文的特性都是 C++17 之后的特性,而 NOI 系列赛事使用的是 C++14,请勿在考场上使用这些特性(目前最新版本的 GCC 默认标准是 C++17,要使用 C++14 需要手动指定 -std=c++14)。
C++17
C++14 之后的第一个版本。
inline 变量
继内联函数之后,C++17 又添加了内联变量。
:::info{open}
值得注意的是,inline 的含义早已发生改变。目前 inline 是指允许多个定义,而非建议编译器进行内联优化
::::
结构化绑定
结构化绑定可以将指定的名称绑定到初始化器的子对象或元素。
听着可能很抽象,实际上结构化绑定最常见的作用是和 std::tie 类似用于解绑 std::tuple 或者 std::pair,但是不能在结构化绑定中使用 std::ignore。
例如:
pair<int, int> p = make_pair(1, 2);
auto [x, y] = p;
在遍历 std::map和 std::unordered_map 的时候很有用。
map<int, int> mp;
for (auto [index, val] : mp) {
cout << index << " " << val << "\n";
}
值得一提的是,结构化绑定也可以用来解绑结构体或者数组。
类模板参数推导
继函数模板参数推导之后,类模板参数也支持推导了。
这个的好处就是可以少几个填 pair 和 tuple 的模板参数了。
pair p(1, 1.1); // = pair<int, float> p(1, 1.1);
并行算法与执行策略
C++17 支持使用多线程并行优化算法库中的算法,并提供执行策略来指定允许的并行类型。
标准库中的执行策略包括 std::execution::seq、std::execution::par、std::execution::par_unseq 和 std::execution::unseq。
这里我们介绍前两个(因为后两个我也不知道是什么意思)。
std::execution::seq 表示不进行并行优化,顺序执行。而 std::execution::par 表示允许使用多线程并行优化。
以 std::sort 为例:
std::sort(std::execution::par, a.begin(), a.end()); // 并行优化
std::sort(std::execution::seq, a.begin(), a.end()); // 顺序执行,不进行并行优化
此外,C++17 新增了一个函数 std::reduce,其实就是带并行优化的 std::accumulate。由于需要并行优化,因此 std::reduce 操作的类型要有交换律和结合律。
:::warning{open} 请注意,C++ 的浮点加法不满足交换律和结合律。 ::::
当然,使用了并行优化也会带来一些额外的问题,例如竞态条件和死锁等。
新的类型/类模板/容器
C++17 新增了 std::optional 和 std::variant 两个类模板和 std::any 这个新容器。
std::optional 用来表示一个可能存在也可能不存在的值。它可以强转成 bool,如果存在这个值就返回 true,否则为 false。通常用于函数返回值。
std::variant 有点像 C 语言的联合体,在同一时刻只能存储模板参数之一的值。
std::any 容器可以存放任意类型的数值。
C++17 还增加了 std::byte 类型,用于表示一个字节。与 unsigned char 不同,std::byte 不支持四则运算,只支持位运算。
新的算法
C++17 新增了 std::clamp、 std::gcd 和 std::lcm(终于来了)。
std::clamp(v, l, r) 的返回值是
- 如果
v 在[l,r] 范围内,则返回v 。 - 否则,返回
l,r 两个边界中最靠近v 的那个。
std::gcd 和 std::lcm 用于返回两个数的最大公因数和最小公倍数(终于!)。
:::warning{open}
不同于之前的 __gcd,目前 GCC 的 std::gcd 使用二进制 GCD 算法而不是欧几里得算法。请注意潜在的复杂度问题。
::::
C++20
:::warning{open} 目前,GCC 对以下内容的支持处于实验性阶段。 ::::
新头文件
<bit>
包含 countl_zero、countr_zero、countl_one、countr_one 和 popcount 等一系列的位运算操作。
相当于是之前一系列内建函数的标准库版本。
<format>
还有谁说 C++ 没有好用的字符串格式化工具?再见 sprintf!
这个头文件提供了 std::format 函数用于格式化字符串。
std::format("{}, {}!", "Hello", 2025); // 返回 std::string("Hello, 2025!")
真是越来越像 python 了。
<numbers>
定义了一些数学常量,比如 std::numbers::pi)和 std::numbers::sqrt2)。
三路比较运算符
三路比较运算符 <=> 可以用于比较两个对象并返回一个对象。
- 如果
a<=>b小于0 ,则a < b。 - 如果等于
0 ,则a == b。 - 否则,
a > b。
模块
允许你使用 import 导入模块(没有 #)。
likely 和 unlikely
用于告诉编译器分支条件时那些代码更可能被执行,那些更不可能被执行,编译器可以据此进行优化。在这之前有 __builtin_expect。
if (x) [[likely]] {
cout << "x = true\n";
// 更可能执行
}
else [[unlikely]] {
cout << "x = false\n";
// 更不可能执行
}
约束与概念
感谢 @masonxiong 对这部分的补充。
约束制定了对模板参数的要求,只需要在模板声明之后添加 requires <条件> 即可。
举个例子:考虑你写了如下 modular 类:
template <ll MOD, class INT_TYPE = ll>
struct modular {
INT_TYPE x;
modular() : x(0) {}
modular(INT_TYPE _x) {
x = _x % MOD;
if (x < 0) {
x += MOD;
}
return;
}
modular operator+(const modular &b) const { return modular(x + b.x); }
modular operator*(const modular &b) const { return modular(x * b.x); }
modular operator+=(const modular &b) { return *this = *this + b; }
modular operator*=(const modular &b) { return *this = *this * b; }
modular operator-(const modular &b) const { return modular(x - b.x); }
modular operator-=(const modular &b) { return *this = *this - b; }
modular operator-() { return modular(-x); }
bool operator==(const modular &b) const { return x == b.x; }
};
用户可以根据模数的大小来自定义使用的整数类型。显然,你希望第二个模板参数填的要是一个整数,但是可能有人会放一些怪东西进去。
这个时候你可以使用约束来限制 INT_TYPE 必须传入整数类型。如果传入的模板参数不满足这个条件就会在编译时报错。
template <ll MOD, class INT_TYPE = ll>
requires integral<INT_TYPE> // 限制 INT_TYPE 必须是整数
struct modular {
INT_TYPE x;
modular() : x(0) {}
modular(INT_TYPE _x) {
x = _x % MOD;
if (x < 0) {
x += MOD;
}
return;
}
modular operator+(const modular &b) const { return modular(x + b.x); }
modular operator*(const modular &b) const { return modular(x * b.x); }
modular operator+=(const modular &b) { return *this = *this + b; }
modular operator*=(const modular &b) { return *this = *this * b; }
modular operator-(const modular &b) const { return modular(x - b.x); }
modular operator-=(const modular &b) { return *this = *this - b; }
modular operator-() { return modular(-x); }
bool operator==(const modular &b) const { return x == b.x; }
};
modular<121, double> a; // ce:integral<double> 为 false,double 不是整数类型。
你也可以使用概念来为一个限制命名:
template <class T>
concept C = integral<T> || floating_point<T>;
之后你就可以使用 requires C<T> 来限制传入的是浮点数或者整数。
C++23 标准也提供了一些内置的概念。除了上面的整数和浮点数之外,还有:
- 判断两个类型相同:
same_as。 - 判断一个类型派生于另一个:
derived_from。 - 全序关系:
totally_ordered和totally_ordered_with。
等等。
范围与视图
感谢 @masonxiong 对这部分的补充。
范围
在 C++20 中,一个范围由一个迭代器对组成。其中一个指向范围的开头(begin()),另一个指向超尾(end(),又被称为哨兵),范围可以使用基于范围的 for 循环遍历。
C++20 引入了受约束的算法(此处的约束指的是上文提到的约束),你可以将范围传入各类 STL 函数,替换掉传入两个迭代器的写法。值得一提的是,STL 容器本身可以转化为一个范围,开头为对应的 begin() 迭代器,哨兵是 end()。
因此,在 C++20 中可以这么写:
vector<int> v{-1, 0, -2, 100, -1111};
std::ranges::sort(v);
不用再传入两个迭代器了。
但是,这就是全部了吗?
在 C++20 中,哨兵并非只能是迭代器,只需要满足 sentinel_for 约束,哨兵可以是任何类型。这个约束是指:
- 有默认构造函数和复制构造函数。
- 支持和
begin()的类型进行operator==操作。
因此,你可以这样写:
template <class it>
struct is_negative {
bool operator==(const it &b) const { return *b < 0; }
};
for (auto x : std::ranges::subrange(v.begin(),
is_negative<vector<int>::iterator>())) {
cout << x << endl;
}
std::subrange 用于取子区间。这样就会在遍历到第一个负数的时候停止。
值得一提的是,范围是懒惰求值的,因此只有你尝试访问范围中的元素时求值才会发生。单纯声明一个范围并不会把元素复制到另一个地方,因此时间和空间都是常数级别的。
视图
又被称为范围适配器。用来对范围内的元素做一些操作。
:::warning{open} 视图不会对范围内部的元素更改,也不会延长原本元素的生命周期,因此视图只能对左值应用。 ::::
<ranges> 头文件定义了以下视图:
transform(op):对范围内每个元素执行op操作的结果。filter(op):只保留范围内满足条件op的元素。take(n):只保留范围内前n 项元素。drop(n):丢弃范围内前n 项元素。
等。
视图可以使用(从 )管道运算符组合以避免各种函数的嵌套。因此你可以这样写:shell 偷的
vector<int> v{-1, 0, 3, 100, -1111};
auto r = v | std::views::transform([](int a) { return a + 2; })
| std::views::filter([](int a) { return a > 0; })
| std::views::take(3);
for (auto x : r) {
cout << x << "\n";
}
表示将范围 v(由容器转化而来)中的元素每个
C++23
:::warning{open} 目前,GCC 对以下内容的支持处于实验性阶段。 ::::
终于!我们来到了 C++ 标准的当前修订版。
新的输出函数
谁还说 C++ 没有好用的格式化输出函数!再见 printf!
提供 std::print 和 std::println。格式化方式和 std::format 一样。
真的是越来越像 python 了。
assume
用于告诉编译器指定的表达式永远为真,编译器可以据此进行优化。
[[assume(x > 0)]];
// x 永远大于 0,编译器可以据此进行优化
新模块
C++23 增加了新的模块 std 和 std.compact,用于导入 std 命名空间中的声明。
是的,你可以用 import std; 替换掉 #include <bits/stdc++.h> 了,而且不是 GCC 专属。
新容器
增加了 std::flat_map 和 std::flat_set。支持 std::set 以及 std::map 一致。
这两个容器实际上是把数据排序后存放在 vector 里,查询时进行二分,因此这两个的迭代器支持随机访问(std::map 和 std::set 不行,注意,std::flat_set 没有 operator[],不能直接使用这个来访问排名为某个数的元素)。
支持使用迭代器 extract() 提取底层容器。可以用来替换掉离散化。
C++26
:::warning{open} 目前,GCC 对以下内容的支持处于高度实验性阶段。 ::::
C++ 标准的下一代。
契约
contracts == ++assert;。
更加灵活的 assert。
我们可以用 contract_assert 来替换 assert。好处是,在 contract_assert 失败的时候(或者被称为契约被违反的时候),你可以选择四种不同的应对策略(可以在编译时使用 -fcontract-semantic= 更改):
- 直接忽略。对应
-fcontract-semantic=ignore。 - 像
assert打印诊断信息并退出程序。对应-fcontract-semantic=enforce。 - 打印诊断信息,但是不退出。对应
-fcontract-semantic=observe。 - 直接退出,没有额外动作。对应
-fcontract-semantic=quick-enforce。
此外,部分实现允许你自定义 handle_contract_violation 函数用于处理违反契约的情况。
此外,契约还允许你在函数调用前后检查参数和返回值。例如:
int f(int x) pre(x >= 0) post(r : r <= 0)
{
return -x;
}
表示在调用函数之前检查参数 x 是否大于 post(r : 条件) 表示用 r 代表函数返回值,可以在条件中使用。
基本线性代数算法
位于 <linalg> 头文件中,包含了向量加法,点积和矩阵乘法等算法。
目前似乎没有任何编译器支持了这个。
:::info[AI 使用说明] 本文使用 DeepSeek 修正了部分语法错误和错别字。 ::::