C++14, 17, 20 的新政策

2021-03-10  本文已影响0人  Platanuses

1. C++14

参考文档

不同于重量级的 C++11 给 C++ 世界带来的脱胎换骨焕然一新,C++14 的体量就比较小。

1.1. 语法级

1.1.1. 字面量

是的,二进制字面量终于来了,如 101010b。数字分位符也来了,如 int i = 424'242;

1.1.2. lambda 形参类型推导

lambda 的形参类型可使用 auto 推导,如:

auto l = [](auto i) { return i + 1; };

1.1.3. 函数返回类型推导

函数的返回类型可使用 auto 推导,如:

auto f(int i) {
    return i + 1;
}

如果函数中有多个 return 语句,则必须可推断为相同的类型。如果函数中存在递归调用,则递归调用之前必须有至少一个可推断返回类型的 return 语句。

1.1.4. constexpr 函数

对 constexpr 函数的限制有所放宽,constexpr 函数中可包含:

1.1.5. 属性

使用 deprecated 属性会在编译期输出警告,如:

[[deprecated("f is thread-unsafe. Use g instead.")]]
void f();

1.2. 标准库级

1.2.1. 自定义字面量

1.2.1.1. 字符串

头文件 <string>
命名空间 std::literals::string_literals

s,创建 std::basic_string,如:

auto str = "abc"s; // std::string s;
1.2.1.2. 时间

头文件 <chrono>
命名空间 std::literals::chrono_literals

hminsmsusns,创建 std::chrono::duration,如:

auto dur = 42s; // std::chrono::seconds dur;
1.2.1.3. 复数

头文件 <complex>
命名空间 std::literals::complex_literals

ifiil,创建 std::complex<float>std::complex<double>std::complex<long double>,如:

auto z = 42i; // std::complex<double> z;

1.2.2. 容器

1.2.2.1. 元组

头文件 <utility>

std::get 函数,当元组中只有一个字段属于某种类型,则可使用该类型来访问该字段,如:

std::tuple<int, std::string, std::string> t(42, "abc", "abc");
int i = std::get<int>(t);

1.2.3. 编译时元编程

头文件 <type_traits>

std::is_final 类用于断言一个类是否禁止继承。

1.2.4. 多线程

头文件 <shared_mutex>

1.2.4.1. shared_timed_mutex

参考文档

std::shared_timed_mutex 类作为读写互斥量,lock_sharedtry_lock_sharedtry_lock_shared_fortry_lock_shared_untilunlock_shared 方法用于读互斥,而在 std::timed_mutex 中也包含同名的 locktry_locktry_lock_fortry_lock_untilunlock 方法用于写互斥。

1.2.4.2. shared_lock

参考文档

std::shared_lock 类与 std::unique_lock 的方法构成完全相同,只是 std::shared_lock 必须基于一个可共享的互斥量,如 std::shared_timed_mutex

2. C++17

参考文档

2.1. 语法级

2.1.1. 结构化绑定

结构化绑定声明可用于数组、std:tuplestd::pair,或用户定义的结构,如:

int arr[2] = {42, 42};
auto [i, j] = arr;
auto &[ri, rj] = arr;

2.1.2. 折叠表达式

折叠表达式简化了模板的变长类型参数的使用,有以下四种折叠:

template<typename... Args> 
void print(Args... args) {
    (std::cout << ... << args) << '\n';
    // print(42, 'a', "abc"); =>
    // (((std::cout << 42) << 'a') << "abc") << '\n';
}

template<typename T, typename... Args> 
void push_back(std::vector<T> &v, Args... args) {
    (v.push_back(args), ...);
    // push_back(v, 42, 42.0, 'a'); =>
    // ((v.push_back(42), v.push_back(42.0)), v.push_back('a'));
}

2.1.3. lamdba 捕获 *this

lambda 以拷贝构造捕获 this,如:

class Cls {
    void f() {
        [*this](){}();
        [=, *this](){}();
    }
};

2.1.4. constexpr if

constexpr if 属于元编程的范畴,使用一个常量表达式作为条件,在编译时选择分支,未被选择的分支最终不会被编译,当然在运行时也不会有跳转,对运行时的性能有所增强,如:

template<typename T> void f() {
    if constexpr (sizeof(T) == 8) {
    } else if constexpr (sizeof(T) == 4) {
    } else {
    }
}

2.1.5. if/switch 初始化

Golang 中惯常使用的,让外层命名空间更精简的语句,如:

if (int i = f(); g(i) > i) {

2.1.6. 类模板类型参数推导

初始化一个类模板的对象,模板的类型参数可自动推导,如:

std::tuple t(42, 42.0); // std::tuple<int, double>

2.1.7. 模板非类型参数类型推导

模板非类型参数的类型可使用 auto 推导,如:

template<auto i> class Cls;
Cls<42> c; // template<int i> class Cls

2.1.8. 嵌套命名空间简化

对于嵌套的命名空间,之前需要写成如:

namespace N {
    namespace N1 {
    }
}

现可简化为:

namespace N::N1 {
}

2.2. 标准库级

2.2.1. 容器

2.2.1.1. 字符串

头文件 <charconv>

std::from_charsstd::to_chars 函数用于字符串转换。

2.2.1.2. 字符串 view

头文件 `<string_view>
参考文档

std::basic_string_view 类类似 std::basic_string 的读写行为,但不掌管底层内存的生命周期。

2.2.1.3. map

头文件 <map>

std::maptry_emplace 方法,仅当 key 不存在时才执行与 emplace 相同的操作。

std::mapinsert_or_assign 方法,顾名思义。

std::mapextract 方法,更换 map 中分量的 key 而不重新分配空间的唯一方式,如:

std::map<int, std::string> m;
auto node = m.extract(42);
node.key() = 7;
m.insert(std::move(node));

std::mapmerge 方法,将传入容器中的分量抽取到 this 中,key 存在的分量则不抽取,如:

std::map<int, std::string> m1 = {{1, "a"}, {2, "b"}};
std::map<int, std::string> m2 = {{2, "d"}, {3, "c"}};
m1.merge(m2);
// m1 == {{1, "a"}, {2, "b"}, {3, "c"}}
// m2 == {{2, "d"}}
2.2.1.4. 元组

头文件 <tuple>

std::apply 函数,将一个 std::tuplestd::pairstd::array 中的分量作为参数来调用可调用对象,如:

std::apply([](auto a, auto b) { return a + b; }, std::tuple(42, 42));
2.2.1.5. optional

头文件 <optional>
参考文档

std::optional 类,类似诸多含有 null-safety 特性的语言中的 Option 类,如:

std::optional<std::string> opt("abc");
std::string &rs = opt.value();
std::optional<int> opt1(std::nullopt);
opt1.has_value(); // false
2.2.1.6. any

头文件 <any>
参考文档

std::any 类,一种类型安全的泛型单值容器。注意这不是一个类模板,不同实际类型的对象之间可以相互赋值、交换,可放入同一个线性、关联容器,如:

std::any a(42);
int &ri = std::any_cast<int&>(a);
2.2.1.7. variant

头文件 <variant>
参考文档

std::variant 类,一种类型安全的联合体,不可存放引用、数组和 void 类型,如:

std::variant<std::string, int> var(42);
int &ri = std::get<int>(var); // 42
int &rj = std::get<1>(var); // 42
var.index(); // 1

2.2.2. 算法

头文件 <algorithm>

std::clamp 函数用于夹。

头文件 <numeric>

std::reduce 函数,是的就是那个 reduce。

std::inclusive_scanstd::exclusive_scan 函数用于前缀运算。参考文档

std::gcd 函数用于求最大公约数。std::lcm 函数用于求最小公倍数。

2.2.3. 文件系统

头文件 <filesystem>
参考文档

文件系统 API 终于正式进入标准库。

2.2.4. 动态内存管理

参考文档

2.2.4.1. new 操作符

newnew[] 操作符现在可以传入第二个类型为 std::align_val_t 的参数用于内存对齐。

2.2.4.2. 分配器

头文件 <memory_resource>

std::pmr::polymorphic_allocator 类实现了一个可供容器使用的运行时多态分配器,由一个 std::pmr::memory_resource 类的派生类提供分配策略,包括简单使用 newdelete 操作符的 std::pmr::new_delete_resource 类和池化的 std::pmr::synchronized_pool_resource 类等。

2.2.4.3. 未初始化内存算法

头文件 <memory>

std::uninitialized_move 函数将对象移动构造至未初始化内存。std::uninitialized_default_construct 函数缺省构造至未初始化内存。std::uninitialized_default_construct 函数值初始化构造至未初始化内存。

std::destroy_at 函数析构指针指向的对象。std::destroy 函数析构迭代器指向的对象。

2.2.5. 编译时元编程

头文件 <type_traits>

2.2.5.1. 静态断言

std::is_swappable_with 类用于断言两个类型之间是否可调用 std::swap

std::is_invocable 类用于断言一个可调用类型是否可由一个类型序列作为参数来调用。

std::is_aggregate 类用于断言一个类型是否聚合类型。

2.2.5.2. 模板元类

std::conjunction 类构成类型之间的逻辑与。参考文档

std::disjunction 类构成类型之间的逻辑或。参考文档

std::negation 类构成类型的逻辑非。参考文档

2.2.6. 多线程

2.2.6.1. shared_mutex

头文件 <shared_mutex>
参考文档

shared_mutex 类之于 shared_timed_mutex,如同 mutex 之于 timed_mutexshared_mutex 之于 mutex,如同 shared_timed_mutex 之于 timed_mutex

2.2.6.2. scoped_lock

头文件 <mutex>
参考文档

scoped_lock 类用于 RAII 式的互斥量包装,在析构时释放互斥量。

2.2.7. 数学特殊函数

头文件 <cmath>
参考文档

3. C++20

参考文档

3.1. 语法级

3.1.1. 字符

char8_t 类型为 8 位字符类型,表示一个 utf-8 的编码,与 unsigned char 性质相同。C++20 起使用前缀 u8 产生的字符串字面量,其字符类型变为 char8_t

3.1.2. 三路比较操作符

是的你没有看错,C++20 居然增加了一个用非字母书写的二元操作符,一个看起来挺鬼畜的符号。三路比较操作符返回一个序类型,参考 3.2.1. 章节。

3.1.3. 初始化

指派初始化器,每个指派符必须指定一个直接非静态数据成员,初始化表达式中指定的顺序必须与类型定义中的成员顺序相同,未指定的字段进行值初始化,如:

struct S { int a; int b; int c; };
S s{ .a = 1, .c = 2 };

3.1.4. 范围 for 初始化

类似 if 初始化,如:

for (auto v = f(); auto e : v) {

3.1.5. 常量表达式

consteval 关键字声明函数立即函数,即每次调用必须产生常量表达式,蕴含 constexprinline

constinit 关键字声明变量拥有静态或线程生命周期。

3.1.6. 概念

我们知道 C++ 的一项原则,要增加任何新内容,能在标准库实现的就不增加新语法。从 C++98 以来,即使是增加新语法,也无非是在现有语言要素上作出调整和补充,比如类型的自动推导、模板的变长类型参数就算其中的重大更新了,而闭包也只是仿函类的语法糖。但这次的概念(concept),则是新增了一项全新的语言要素类型,堪称 C++20 三巨头之首。

概念是一个对模板类型实参的约束,可对模板类型实参进行编译时元编程的断言。而一个模板的模板类型形参可声明受概念的约束,编译器实例化模板时,会对模板类型实参执行概念的编译时元编程代码,检查其是否满足约束。类似的,我们能在诸如 Rust/Java 中约束泛型参数必须继承于某个类或实现了某个接口,相比之下显然 C++ 概念的表达能力更强,直逼 Haskell 的 typeclass。

概念的定义,如:

template<typename T, typename U>
concept ConceptA = std::is_xxx<T, U>::value && std::is_yyy<U, T>::value;

template<typename T, typename U>
concept ConceptB = std::is_zzz<T, U>::value || !ConceptA<U, T>;

template<typename T, typename U>
concept ConceptC = requires(T t, U u) {
    t + u;
    typename T::U;
};

显然,可以将概念看作一个常量布尔表达式或一个函数。概念不能递归定义,不能显式实例化、特化,不能约束另一个概念。以 requires 开头的表达式类似一个函数,其 requires 体中的语句分为以下类型,可混合出现:

template<typename T, typename U>
concept Concept = requires(T t, U u) {
    t + u;
};
template<typename T>
concept Concept = requires(T t) {
    typename T::U;
};
template<typename T>
concept Concept = requires(T t) {
    {t + 1} -> std::same_as<T>; // to check std::same_as<decltype((t + 1)), T>::value
};
template<typename T>
concept Concept = requires(T t) {
    requires ConceptA<T*>;
};

模板指定概念时,可在模板参数列表中用概念替代 typename,或在模板参数列表之后使用 requires 子句,或者声明的主体之后使用 requires 子句,或在以上三处中的多处同时出现而构成逻辑与的关系,如:

template<ConceptA T, ConceptB<int> U> requires ConceptC<T, U>
void f(T, U) requires ConceptD<T>;
// T is constrainted by ConceptA<T> and ConceptC<T, U> and ConceptD<T>
// U is constrainted by ConceptB<U, int> and ConceptC<T, U>

requires 子句中也可使用逻辑与、或、非,如:

template<typename T> requires (ConceptA<T> && ConceptB<T>)
void f(T);

例如,一个函数接受一个对象及其 run 方法的参数,run 方法有特定的参数列表,如果对象存在此 run 方法,则传参调用该方法并返回 true,否则不调用并返回 false。显然,在动态语言中这个函数很容易实现,但在 C++ 这样的静态语言中则需要使用到编译时的模板元编程技巧。在 C++11 中,我们可以用两层的 sfinae 来实现:

#include <type_traits>

template<typename T, typename ...Args> struct has_method_run {
private:
    template<typename U> static auto f(int) ->
        decltype(std::declval<U>().run(std::declval<Args>()...), std::true_type());
    template<typename U> static std::false_type f(char);
public:
    enum { value = decltype(f<T>(0))::value };
};

template<typename T, typename ...Args> auto run(T &&t, Args &&...args) ->
        typename std::enable_if<has_method_run<T, Args...>::value, bool>::type {
    t.run(std::forward<Args>(args)...);
    return true;
}

template<typename T, typename ...Args> auto run(T &&t, Args &&...args) ->
        typename std::enable_if<!has_method_run<T, Args...>::value, bool>::type {
    return false;
}

看起来有些天书。在 C++17 中,可以更优雅一点,我们可以使用 if constexpr 来消除 run 函数必需的重载,从而让 sfinae 减少到只有一层:

#include <type_traits>

template<typename T, typename ...Args> struct has_method_run {
private:
    template<typename U> static auto f(int) ->
        decltype(std::declval<U>().run(std::declval<Args>()...), std::true_type());
    template<typename U> static std::false_type f(char);
public:
    enum { value = decltype(f<T>(0))::value };
};

template<typename T, typename ...Args> bool run(T &&t, Args &&...args) {
    if constexpr (has_method_run<T, Args...>::value) {
        t.run(std::forward<Args>(args)...);
        return true;
    }
    return false;
}

而在 C++20 中,我们可以使用 concept 让 sfinae 变得更优雅:

#include <type_traits>

template<typename T, typename ...Args> concept must_have_method_run = requires(T t, Args ...args) {
    t.run(args...);
};

template<typename T, typename ...Args> struct has_method_run {
private:
    template<typename U> requires must_have_method_run<U, Args...> static std::true_type f(int);
    template<typename U> static std::false_type f(char);
public:
    enum { value = decltype(f<T>(0))::value };
};

template<typename T, typename ...Args> bool run(T &&t, Args &&...args) {
    if constexpr (has_method_run<T, Args...>::value) {
        t.run(std::forward<Args>(args)...);
        return true;
    }
    return false;
}

3.1.7. 协程

[参考文档](https://en.cppreference.com/w/cpp/language/coroutines

C++20 三巨头之二。专门一篇

3.1.8. 模块

[参考文档](https://en.cppreference.com/w/cpp/language/modules

C++20 三巨头之三。模块与命名空间之间还是正交的,日你先人,略。

3.2. 标准库级

3.2.1. 比较与排序

头文件 <compare>

是的,在 Rust 当中会见到的那些抽象代数的概念,现在来到了 C++ 的标准库中。

std::partial_ordering 类实现了偏序概念。其中包含以下同类 constexpr 静态成员常量:lessequivalentgreaterunordered,分别表示小于、等价、大于、不可比较。不可隐式转换为 std::weak_orderingstd::strong_ordering。对于三路比较返回 std::partial_ordering 的类型,其 <==> 操作符可均返回 false

std::weak_ordering 类实现了弱序概念。其中包含以下同类 constexpr 静态成员常量:lessequivalentgreater,分别表示小于、等价、大于。可隐式转换为 std::partial_ordering,不可隐式转换为 std::strong_ordering。对于三路比较返回 std::weak_ordering 的类型,其 <==> 操作符需有且仅有一个返回 true

std::strong_ordering 类实现了强序概念。其中包含以下同类 constexpr 静态成员常量:lessequivalentequalgreater,分别表示小于、等价、等价、大于。可隐式转换为 std::partial_orderingstd::weak_ordering。对于三路比较返回 std::strong_ordering 的类型,其 <==> 操作符需有且仅有一个返回 true

例如,两个 int 类型的变量之间的三路比较操作符返回一个 std::strong_ordering 类型,而两个 double 类型则返回 std::partial_ordering 类型,因为浮点数存在不可比较的 NaN 值。以上三个序类均重载了 ==<><=>=<=> 操作符,可与同类型对象或 0 字面量进行比较。

std::common_comparison_category 类断言多个类之间能转换为的最强序类。

std::compare_three_way_result 类断言一个或两个类之间的三路比较操作符的返回类型。

3.2.2. 概念

头文件 <concepts>
参考文档

3.2.3. 工具库

3.2.3.1. 源码信息

头文件 <source_location>
参考文档

std::source_location 类用于表示源码信息,作为 __FILE__ 等宏的替代方案,如:

auto sl = std::source_location::current();
sl.line();
3.2.3.2. 格式化

头文件 <format>
参考文档

格式化库作为 printf 函数族的替代方案,如:

std::string s = std::format("{} {}", "Hello", "world");
3.2.3.3. 时间

头文件 <chrono>
参考文档

时间库中增加了日历和时区。

3.2.3.4. 函数绑定

头文件 <functional>

std::bind_front 函数将多个参数绑定到可调用对象的首部形参。

3.2.4. 容器

3.2.4.1. 字符串

头文件 <string>

std::string 终于有 starts_withends_with 方法了。

头文件 <cuchar>

std::c8rtombstd::mbrtoc8 函数用于单个编点的 utf-8 与窄多字节字符表示之间的转换。

3.2.4.2. span

头文件 <span>
参考文档

std::span 类模板用于抽象描述一个线性序列,可拥有静态或动态的长度。

3.2.4.3. range

头文件 <ranges>
参考文档

众所周知三巨头一定有四位,range 库正是 C++20 三巨头之四。

3.2.5. 算法

头文件 <numeric>

std::midpoint 函数用于计算中点。

头文件 <bit>
参考文档

提供二进制算法,如 std::popcount 函数作为 __builtin_popcount 的替代方案,std::countl_zero 函数作为 __builtin_clz 的替代方案。

3.2.6. 动态内存管理

头文件 <memory>

std::make_obj_using_allocator 函数用于创建对象!

std::make_shared 函数添加了大量重载。参考文档

3.2.7. 编译时元编程

头文件 <type_traits>

std::remove_cvref 类用于生成去掉引用类型和 cv 限定符的类型。

std::is_bounded_array 类用于断言一个类型是否是有已知固定长度的数组。

3.2.8. 多线程

3.2.8.1. osyncstream

头文件 <syncstream>
参考文档

std::osyncstream 作为线程安全的 std::ostream

3.2.8.2. latch

头文件 <latch>
参考文档

std::latch 类作为单次线程屏障。其计数不能增加或重置,因此只能使用一次。count_down 方法减计数,arrive_and_wait 方法减计数且阻塞线程直至计数为零,wait 方法阻塞线程直至计数为零。

3.2.8.3. barrier

头文件 <barrier>
参考文档

std::barrier 类作为可复用线程屏障。不同于 latch,其计数为零时重置,因此可使用多次;不同于 latch,每个计数周期内,一个线程只能减计数一次。可指定一个初始计数和周期回调,计数为零时调用周期回调,周期回调返回之后,所有阻塞线程的方法才返回,并恢复初始计数。arrive 方法减计数,arrive_and_drop 方法减计数且减初始计数,arrive_and_wait 方法减计数且阻塞线程直至计数为零,wait 方法阻塞线程直至计数为零。

3.2.8.4. semaphore

头文件 <semaphore>
参考文档

std::counting_semaphore 类作为信号量。acquire 方法在计数大于 0 时减计数,否则阻塞线程直至成功减计数,release 方法加计数,同时包含 try_acquire 方法族。

3.2.9. 数学常数

头文件 <numbers>
参考文档

A. 附录

A.1. 关键字

A.1.1. C++11 中新增的关键字

alignas alignof char16_t char32_t constexpr decltype final noexcept nullptr override static_assert thread_local

A.1.2. C++11 中含义有变的关键字

auto class default delete export extern inline mutable sizeof struct using

A.1.3. C++17 中含义有变的关键字

register

A.1.4. C++20 中新增的关键字

char8_t concept consteval constinit co_await co_return co_yield module requires

A.1.5. C++20 中含义有变的关键字

export

上一篇下一篇

猜你喜欢

热点阅读