07 cpp17
cpp17
C++17 语言特性
用auto声明非类型模板参数
遵循 auto 的推导规则,同时尊重非类型模板参数列表的允许类型,模板参数可以从其参数的类型推导:
template <auto... seq>
struct my_integer_sequence {
// 这里是实现 ...
};
// 显式传递类型 `int` 作为模板参数。
auto seq = std::integer_sequence<int, 0, 1, 2>();
// 类型推导为 `int`。
auto seq2 = my_integer_sequence<0, 1, 2>();但并非所有类型都可以用 auto 作为非类型模板参数:
- 允许: 整数(int, long, char)、布尔值(bool)、枚举(enum)、指针。
- 禁止: 浮点数(double, float)、类对象(直到 C++20 才放宽限制)。
折叠表达式
折叠表达式对二进制操作符上的模板参数包执行折叠。
(... op e)或(e op ...)形式的表达式,其中op是折叠操作符,e是未展开的参数包,称为一元折叠。(e1 op ... op e2)形式的表达式,其中op是折叠操作符,称为二元折叠。e1或e2是未展开的参数包,但不是两者都是。
template <typename... Args>
bool logicalAnd(Args... args) {
// 二元折叠。
return (true && ... && args);
}
bool b = true;
bool& b2 = b;
logicalAnd(b, b2, true); // == truetemplate <typename... Args>
auto sum(Args... args) {
// 一元折叠。
return (... + args);
}
sum(1.0, 2.0f, 3); // == 6.0auto从大括号初始化列表推导的新规则
使用统一初始化语法时,auto 推导的变化。之前,auto x {3}; 推导为 std::initializer_list<int>,现在推导为 int。
auto x1 {1, 2, 3}; // 错误:不是单个元素
auto x2 = {1, 2, 3}; // x2 是 std::initializer_list<int>
auto x3 {3}; // x3 是 int
auto x4 {3.0}; // x4 是 doubleconstexpr lambda
使用 constexpr 的编译期lambda,和 constexpr 修饰函数一样,允许修饰的函数在编译期求值,但并非必须在编译期求值。
auto identity = [](int n) constexpr { return n; };
static_assert(identity(123) == 123);constexpr auto add = [](int x, int y) {
auto L = [=] { return x; };
auto R = [=] { return y; };
return [=] { return L() + R(); };
};
static_assert(add(1, 2)() == 3);constexpr int addOne(int n) {
return [n] { return n + 1; }();
}
static_assert(addOne(1) == 2);Lambda按值捕获this
在lambda的环境中捕获 this 之前只能按引用。一个有问题的例子是使用需要对象可用的回调的异步代码,可能超过其生命周期。*this (C++17) 现在会制作当前对象的副本,而 this (C++11) 继续按引用捕获。
struct MyObj {
int value {123};
auto getValueCopy() {
return [*this] { return value; };
}
auto getValueRef() {
return [this] { return value; };
}
};
MyObj mo;
auto valueCopy = mo.getValueCopy();
auto valueRef = mo.getValueRef();
mo.value = 321;
valueCopy(); // 123
valueRef(); // 321内联变量
内联说明符可以应用于变量以及函数。声明为内联的变量具有与声明为内联的函数相同的语义,允许在多个翻译单元中定义变量,而不会违反 ODR(One Definition Rule)。内联变量的一个常见用例是定义常量。
// 使用编译器浏览器的反汇编示例。
struct S { int x; };
inline S x1 = S{321}; // mov esi, dword ptr [x1]
// x1: .long 321
S x2 = S{123}; // mov eax, dword ptr [.L_ZZ4mainE2x2]
// mov dword ptr [rbp - 8], eax
// .L_ZZ4mainE2x2: .long 123它也可以用于声明和定义静态成员变量,这样就不需要在源文件中初始化。
struct S {
S() : id{count++} {}
~S() { count--; }
int id;
static inline int count{0}; // 在类内声明并初始化 count 为 0
};嵌套命名空间
使用命名空间解析操作符来创建嵌套命名空间定义。
namespace A {
namespace B {
namespace C {
int i;
}
}
}上面的代码可以这样写:
namespace A::B::C {
int i;
}结构化绑定
一个去结构化初始化的提案,允许编写 auto [ x, y, z ] = expr;,其中 expr 的类型是一个元组样对象,其元素将绑定到变量 x、y 和 z(这个构造声明)。元组样对象包括 std::tuple、std::pair、std::array 和聚合结构。
using Coordinate = std::pair<int, int>;
Coordinate origin() {
return Coordinate{0, 0};
}
const auto [ x, y ] = origin();
x; // == 0
y; // == 0std::unordered_map<std::string, int> mapping {
{"a", 1},
{"b", 2},
{"c", 3}
};
// 按引用去结构化。
for (const auto& [key, value] : mapping) {
// 对 key 和 value 执行某些操作
}带初始化器的选择语句
if 和 switch 语句的新版本,简化常见代码模式并帮助用户保持作用域紧凑,允许在条件之前声明和初始化变量,且作用域仅限于条件和随后的控制体。
{
std::lock_guard<std::mutex> lk(mx);
if (v.empty()) v.push_back(val);
}
// vs.
if (std::lock_guard<std::mutex> lk(mx); v.empty()) {
v.push_back(val);
}Foo gadget(args);
switch (auto s = gadget.status()) {
case OK: gadget.zip(); break;
case Bad: throw BadFoo(s.message());
}
// vs.
switch (Foo gadget(args); auto s = gadget.status()) {
case OK: gadget.zip(); break;
case Bad: throw BadFoo(s.message());
}constexpr if
根据编译期条件编写实例化的代码,会在编译期求值条件,并且只实例化满足条件的分支,而另一个分支将被丢弃,不会实例化,注意,条件必须是编译期常量表达式,因为此特性必须在编译期完成剪枝。
template <typename T>
constexpr bool isIntegral() {
if constexpr (std::is_integral<T>::value) {
return true;
} else {
return false;
}
}
static_assert(isIntegral<int>() == true);
static_assert(isIntegral<char>() == true);
static_assert(isIntegral<double>() == false);
struct S {};
static_assert(isIntegral<S>() == false);UTF-8 字符字面量
以 u8 开头的字符字面量是类型为 char 的字符字面量。UTF-8 字符字面量的值等于其 ISO 10646 代码点值,注意,在 cpp17 中,此特性仍只适用于 ascii 字符在一个字节的情况,因为它仍属于 char 类型,cpp20 扩展了此特性,允许 utf-8 字符字面量的值超过一个字节,并且类型为 char8_t。
因此,在 cpp17 中,该字面量的作用仅仅是具有 Unicode 语义的 ASCII 字符字面量:
char x = u8'x';枚举的直接列表初始化
在 C++17 之前,如果我们想给 int 起个独立的新名字,可以选择 enum class,但是它的缺点是不能直接初始化枚举值,必须使用 static_cast,这很麻烦,我们必须手动调用 static_cast 来将整数转换为枚举类型。
cpp17 允许直接使用大括号初始化枚举值,编译器会检查值是否在枚举的范围内,如果不在范围内会报错:
enum byte : unsigned char {};
byte b {0}; // OK
byte c {-1}; // 错误
byte d = byte{1}; // OK
byte e = byte{256}; // 错误[[fallthrough]], [[nodiscard]], [[maybe_unused]] 属性
C++17 引入三个新属性:[[fallthrough]]、[[nodiscard]] 和 [[maybe_unused]]。
[[fallthrough]]向编译器表示switch语句中的贯穿是预期行为。这个属性只能在switch语句中使用,并且必须在下一个case/default标签之前放置。
switch (n) {
case 1:
// ...
[[fallthrough]];
case 2:
// ...
break;
case 3:
// ...
[[fallthrough]];
default:
// ...
}[[nodiscard]]当函数或类有此属性且其返回值被丢弃时发出警告。
[[nodiscard]] bool do_something() {
return is_success; // 成功返回 true,失败返回 false
}
do_something(); // 警告:忽略 'bool do_something()' 的返回值,
// 声明了属性 'nodiscard'// 仅当 `error_info` 按值返回时才发出警告。
struct [[nodiscard]] error_info {
// ...
};
error_info do_something() {
error_info ei;
// ...
return ei;
}
do_something(); // 警告:忽略返回值,类型为 'error_info',
// 声明了属性 'nodiscard'[[maybe_unused]]向编译器表示变量或参数可能未使用且是预期的。
void my_callback(std::string msg, [[maybe_unused]] bool error) {
// 不在乎 `msg` 是否是错误消息,只是记录它。
log(msg);
}__has_include
__has_include (operand) 操作符可以在 #if 和 #elif 表达式中使用,以检查头文件或源文件(operand)是否可用于包含。
这个的一个用例是使用两个以相同方式工作的库,如果系统上未找到首选的库,则使用备用/实验的。
#ifdef __has_include
# if __has_include(<optional>)
# include <optional>
# define have_optional 1
# elif __has_include(<experimental/optional>)
# include <experimental/optional>
# define have_optional 1
# define experimental_optional
# else
# define have_optional 0
# endif
#endif它也可以用于包含在不同名称或位置的各种平台上存在的头文件,而不知道程序运行在哪个平台上。OpenGL头文件是一个很好的例子,它们在macOS上位于 OpenGL\ 目录,在其他平台上位于 GL\ 目录。
#ifdef __has_include
# if __has_include(<OpenGL/gl.h>)
# include <OpenGL/gl.h>
# include <OpenGL/glu.h>
# elif __has_include(<GL/gl.h>)
# include <GL/gl.h>
# include <GL/glu.h>
# else
# error No suitable OpenGL headers found.
# endif
#endif类模板参数推导
类模板参数推导 (CTAD) 允许编译器从构造函数参数推导模板参数,而不需要显式指定模板参数。对于类模板的构造函数,编译器将使用构造函数参数来推导模板参数。
std::vector v{ 1, 2, 3 }; // 推导为 std::vector<int>
std::mutex mtx;
auto lck = std::lock_guard{ mtx }; // 推导为 std::lock_guard<std::mutex>
auto p = new std::pair{ 1.0, 2.0 }; // 推导为 std::pair<double, double>*对于用户定义的类型,推导指引可以用来指导编译器如何推导模板参数(如果适用):
template <typename T>
struct container {
container(T t) {}
template <typename Iter>
container(Iter beg, Iter end);
};
// 推导指引
template <typename Iter>
container(Iter b, Iter e) -> container<typename std::iterator_traits<Iter>::value_type>;
container a{ 7 }; // OK:推导为 container<int>
std::vector<double> v{ 1.0, 2.0, 3.0 };
auto b = container{ v.begin(), v.end() }; // OK:推导为 container<double>
container c{ 5, 6 }; // 错误:std::iterator_traits<int>::value_type 不是类型C++17 库特性
std::variant
类模板 std::variant 表示一个类型安全的 union。std::variant 的实例在任何给定时间都保持其替代类型之一的值(它也可能无值)。
std::variant<int, double> v{ 12 };
std::get<int>(v); // == 12
std::get<0>(v); // == 12
v = 12.0;
std::get<double>(v); // == 12.0
std::get<1>(v); // == 12.0
std::visit([](auto&& arg)
{
std::cout << arg; // 输出 12.0
}, v);std::optional
类模板 std::optional 管理可选包含的值,即可能存在或不存在的值。optional的一个常见用例是可能失败的函数的返回值。
std::optional<std::string> create(bool b) {
if (b) {
return "Godzilla";
} else {
return {};
}
}
create(false).value_or("empty"); // == "empty"
create(true).value(); // == "Godzilla"
// optional返回工厂函数可用作while和if的条件
if (auto str = create(true)) {
// ...
}std::any
任何类型的单个值的类型安全容器。
std::any x {5};
x.has_value() // == true
std::any_cast<int>(x) // == 5
std::any_cast<int&>(x) = 10;
std::any_cast<int>(x) // == 10std::string_view
字符串的非所有权引用。用于在字符串之上提供抽象的有用的(例如用于解析)。
// 常规字符串。
std::string_view cppstr {"foo"};
// 宽字符串。
std::wstring_view wcstr_v {L"baz"};
// 字符数组。
char array[3] = {'b', 'a', 'r'};
std::string_view array_v(array, std::size(array));std::string str {" trim me"};
std::string_view v {str};
v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));
str; // == " trim me"
v; // == "trim me"std::invoke
使用参数调用 Callable 对象。可调用对象的示例是 std::function 或lambda;可以像调用常规函数一样调用的对象。
template <typename Callable>
class Proxy {
Callable c_;
public:
Proxy(Callable c) : c_{ std::move(c) } {}
template <typename... Args>
decltype(auto) operator()(Args&&... args) {
// ...
return std::invoke(c_, std::forward<Args>(args)...);
}
};
const auto add = [](int x, int y) { return x + y; };
Proxy p{ add };
p(1, 2); // == 3std::apply
把一个元组(std::tuple、std::pair 或 std::array)拆解开,然后把里面的元素作为参数传给一个函数。
auto add = [](int x, int y) {
return x + y;
};
std::apply(add, std::make_tuple(1, 2)); // == 3std::filesystem
新的 std::filesystem 库提供了一个标准方式来在文件系统中操纵文件、目录和路径。
这里,一个大文件如果有可用空间就复制到临时路径:
const auto bigFilePath {"bigFileToCopy"};
if (std::filesystem::exists(bigFilePath)) {
const auto bigFileSize {std::filesystem::file_size(bigFilePath)};
std::filesystem::path tmpPath {"/tmp"};
if (std::filesystem::space(tmpPath).available > bigFileSize) {
std::filesystem::create_directory(tmpPath.append("example"));
std::filesystem::copy_file(bigFilePath, tmpPath.append("newFile"));
}
}std::byte
新的 std::byte 类型提供了一个表示数据为字节的标准方式。与 char 或 unsigned char 相比使用 std::byte 的好处是它不是字符类型,也不是算术类型;虽然唯一可用的操作符重载是按位操作。
std::byte a {0};
std::byte b {0xFF};
int i = std::to_integer<int>(b); // 0xFF
std::byte c = a & b;
int j = std::to_integer<int>(c); // 0请注意,std::byte 只是一个枚举,枚举的大括号初始化依赖于 枚举的直接列表初始化。
映射和集合的拼接
无需昂贵的副本、移动或堆分配/释放开销,移动节点和合并容器,主要依赖于 extract 和 merge 成员函数。
从一个映射移动元素到另一个:
std::map<int, string> src {{1, "one"}, {2, "two"}, {3, "buckle my shoe"}};
std::map<int, string> dst {{3, "three"}};
dst.insert(src.extract(src.find(1))); // 无损耗的从 `src` 移除和插入 { 1, "one" } 到 `dst`。
dst.insert(src.extract(2)); // 无损耗的的从 `src` 移除和插入 { 2, "two" } 到 `dst`。
// dst == { { 1, "one" }, { 2, "two" }, { 3, "three" } };插入一个整个集合:
std::set<int> src {1, 3, 5};
std::set<int> dst {2, 4, 5};
dst.merge(src);
// src == { 5 }
// dst == { 1, 2, 3, 4, 5 }插入超过容器生命周期的元素:
auto elementFactory() {
std::set<...> s;
s.emplace(...);
return s.extract(s.begin());
}
s2.insert(elementFactory());改变映射元素的键:
std::map<int, string> m {{1, "one"}, {2, "two"}, {3, "three"}};
auto e = m.extract(2);
e.key() = 4;
m.insert(std::move(e));
// m == { { 1, "one" }, { 3, "three" }, { 4, "two" } }并行算法
许多STL算法,如 copy、find 和 sort 方法,开始支持并行执行策略:seq、par 和 par_unseq,分别转换为"顺序"、"并行"和"并行未序列化"。
std::vector<int> longVector;
// 使用并行执行策略查找元素
auto result1 = std::find(std::execution::par, std::begin(longVector), std::end(longVector), 2);
// 使用顺序执行策略排序元素
auto result2 = std::sort(std::execution::seq, std::begin(longVector), std::end(longVector));std::sample
从给定序列中采样n个元素(无替换),其中每个元素有相等的被选中机会。
const std::string ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
std::string guid;
// 从 ALLOWED_CHARS 采样 5 个字符。
std::sample(ALLOWED_CHARS.begin(), ALLOWED_CHARS.end(), std::back_inserter(guid), 5, std::mt19937{ std::random_device{}() });
std::cout << guid; // 例如 G1fW2std::clamp
限制一个值在 [min, max] 区间内。如果超过最大值就等于最大值,如果小于最小值就等于最小值,语法为 std::clamp(value, min, max),其中 value 是要限制的值,min 是最小值,max 是最大值。
std::clamp(42, -1, 1); // == 1
std::clamp(-42, -1, 1); // == -1
std::clamp(0, -1, 1); // == 0
// `std::clamp` 也接受自定义比较器:
std::clamp(0, -1, 1, std::less<>{}); // == 0std::reduce
对给定范围的元素进行折叠。概念上类似于 std::accumulate,但 std::reduce 将并行执行折叠。由于折叠并行进行,如果指定二进制操作,它必须是关联的和可交换的。给定的二进制操作也不应该改变任何元素或使范围内的任何迭代器无效。
默认二进制操作是std::plus,初始值为0。
const std::array<int, 3> a{ 1, 2, 3 };
std::reduce(std::cbegin(a), std::cend(a)); // == 6
// 使用自定义二进制操作:
std::reduce(std::cbegin(a), std::cend(a), 1, std::multiplies<>{}); // == 6另外,你可以为reducer指定转换:
const auto times_ten = [](const auto n) { return n * 10; };
std::transform_reduce(std::cbegin(a), std::cend(a), 0, std::plus<>{}, times_ten); // == 60
const std::array<int, 3> b{ 1, 2, 3 };
const auto product_times_ten = [](const auto a, const auto b) { return a * b * 10; };
std::transform_reduce(std::cbegin(a), std::cend(a), std::cbegin(b), 0, std::plus<>{}, product_times_ten); // == 140前缀和算法
支持前缀和(包括包容和排斥扫描)以及转换。
const std::array<int, 3> a{ 1, 2, 3 };
std::inclusive_scan(std::cbegin(a), std::cend(a),
std::ostream_iterator<int>{ std::cout, " " }, std::plus<>{}); // 1 3 6
std::exclusive_scan(std::cbegin(a), std::cend(a),
std::ostream_iterator<int>{ std::cout, " " }, 0, std::plus<>{}); // 0 1 3
const auto times_ten = [](const auto n) { return n * 10; };
std::transform_inclusive_scan(std::cbegin(a), std::cend(a),
std::ostream_iterator<int>{ std::cout, " " }, std::plus<>{}, times_ten); // 10 30 60
std::transform_exclusive_scan(std::cbegin(a), std::cend(a),
std::ostream_iterator<int>{ std::cout, " " }, 0, std::plus<>{}, times_ten); // 0 10 30GCD 和 LCM
最大公约数 (GCD) 和最小公倍数 (LCM)。
const int p = 9;
const int q = 3;
std::gcd(p, q); // == 3
std::lcm(p, q); // == 9std::not_fn
返回给定函数结果否定的实用函数,给定一个谓词,std::not_fn 返回一个新的谓词,当调用时,它将调用原始谓词并返回其结果的否定。
const std::ostream_iterator<int> ostream_it{ std::cout, " " };
const auto is_even = [](const auto n) { return n % 2 == 0; };
std::vector<int> v{ 0, 1, 2, 3, 4 };
// 打印所有偶数。
std::copy_if(std::cbegin(v), std::cend(v), ostream_it, is_even); // 0 2 4
// 打印所有奇数(不是偶数)。
std::copy_if(std::cbegin(v), std::cend(v), ostream_it, std::not_fn(is_even)); // 1 3字符串与数字的相互转换
将整数和浮点数转换为字符串,反之亦然。转换是不抛异常的,不分配,并且比C标准库的等效物更安全。
用户负责为 std::to_chars 分配足够的存储,否则函数将失败,通过在其返回值中设置错误代码对象。
这些函数允许你可选地传递基数(默认为基数10)或浮点类型输入的格式说明符。
std::to_chars返回一个(非const)char指针,它位于函数写入给定缓冲区内的字符串之后,以及一个错误代码对象。std::from_chars返回一个const char指针,成功时等于传递给函数的结束指针,以及一个错误代码对象。
从这两个函数返回的错误代码对象在成功时等于默认初始化的错误代码对象。
将数字 123 转换为 std::string:
const int n = 123;
// 可以使用任何容器、字符串、数组等。
std::string str;
str.resize(3); // 为 `n` 的每个数字保持足够的存储空间
const auto [ ptr, ec ] = std::to_chars(str.data(), str.data() + str.size(), n);
if (ec == std::errc{}) { std::cout << str << std::endl; } // 123
else { /* 处理失败 */ }从值为 "123" 的 std::string 转换为整数:
const std::string str{ "123" };
int n;
const auto [ ptr, ec ] = std::from_chars(str.data(), str.data() + str.size(), n);
if (ec == std::errc{}) { std::cout << n << std::endl; } // 123
else { /* 处理失败 */ }Chrono时间段和时间点的舍入函数
为 std::chrono::duration 和 std::chrono::time_point 提供abs、round、ceil和floor辅助函数。
std::chrono::milliseconds a{ -5500 };
std::chrono::milliseconds d = std::chrono::abs(a); // == 5500ms
std::chrono::round<seconds>(d); // == 6s
std::chrono::ceil<seconds>(d); // == 6s
std::chrono::floor<seconds>(d); // == 5s致谢
- cppreference - 特别有用于查找新库特性的示例和文档。
- C++ Rvalue References Explained - 一个很好的介绍,我用它来理解右值引用、完美转发和移动语义。
- clang 和 gcc 的标准支持页面。这里还包括我用来查找语言/库特性描述、其目的修复内容和一些示例的提案。
- Compiler explorer
- Scott Meyers' Effective Modern C++ - 强烈推荐的书!
- Jason Turner's C++ Weekly - C++ 相关视频的不错合集。
- What can I do with a moved-from object?
- What are some uses of decltype(auto)?