闭关之现代 C++ 笔记汇总(二):特性演化

目录

  • 前言
  • C++98
    • C++98 之前
    • C++98 的主要语言特性
      • 特性总结
        • dynamic_cast
        • RAII
      • 标准库组件
        • 总结
        • find_if
      • 其他语言对 C++ 影响 (非 C++98 内容 )
      • C++ 对其他语言影响 (非 C++98 内容 )
  • C++11
    • C++11 语言特性
      • 内容
      • 标准库组件
    • C++11:并发支持 (并发会有单独的学习计划,这里只记关键的笔记)
      • 内存模型
      • 线程和锁
      • 线程和锁
    • C++11:简化使用
      • auto 和 decltype
      • range-for
      • 移动语义
      • unique_ptr 和 shared_ptr
      • 统一初始化
      • nullptr 略
      • constexpr 函数
      • 用户定义字面量
      • 原始字符串字面量
      • 属性 attribute
    • C++11:改进对泛型编程的支持
      • lambda 表达式
      • 变参模板
      • 别名 template aliases
      • tuple
    • C++11:提高静态类型安全
    • C++11:支持对库的开发
      • 实现技巧
      • 元编程支持 Metaprogramming Support
      • noexcept 规约
    • C++11:Standard-Library Components
  • C++ 14: 完成 C++11
    • C++14 的主要语言特性
      • 数字分隔符
      • 变量模板
      • 函数返回类型推导
      • 泛型 lambda 表达式
      • constexpr 函数中的局部变量
  • C++17:大海迷航
    • C++17 的主要语言特性
      • 特性总结
      • 标准库组件
      • 具体内容
        • 构造函数模板参数推导
        • 结构化绑定
        • variant、optional 和 any
        • 并发
        • 并行 STL
        • 文件系统
        • 条件的显式测试
  • C++20:方向之争
    • C++20 的主要语言特性
      • 特性总结
      • 次要特性
      • 具体内容
        • 模块
        • 协程
        • 编译期计算支持
        • <=> 飞船运算符 spaceship operator
        • 范围 Ranges
        • 日期和时区
        • 格式化 Format
        • 跨度 Span
        • 并发

前言

   本节是对 Bjarne Stroustrup 的论文《Thriving in a Crowded and Changing World: C++ 2006–2020》的总结。

  • 论文链接
  • 中文翻译链接

C++98

C++98 之前

  • new 函数
    • 为成员函数创建运行的环境
  • delete 函数
    • 则执行相反的操作
  • const
    • 支持接口和符号常量的不变性
  • 虚函数 virtual functions
    • 提供运行期多态
  • 引用 References
    • 持运算符重载和简化参数传递
  • 运算符和函数重载 Operator and function overloading
    • 除了算术和逻辑运算符的重载外,还包括以下重载
      • 允许用户定义 =(赋值)
      • ()(调用;支持函数对象)???
      • [](下标访问)
      • ->(智能指针)
  • 类型安全链接 Type-safe linkage
    • 消除许多来自不同翻译单元中不一致声明的错误
  • 抽象类 abstract classes
    • 提供纯接口

C++98 的主要语言特性

特性总结

  • 模板 Templates
    • 无约束的、图灵完备的、对泛型编程的编译期支持
  • 异常 Exceptions
    • 一套在单独(不可见的)路径上返回错误值的机制,由调用方栈顶上的“在别处”的代码处理
  • dynamic_cast 和 typeid
    • 一种非常简单的运行期反射形式(“运行期类型识别”,又名 RTTI)
  • namespace
    • 允许程序员在编写由几个独立部分组成的较大程序时避免名称冲突
  • 条件语句内的声明 Declarations in conditions
    • 让写法更紧凑和限制变量作用域
  • 具名类型转换 Named casts
    • static_cast、reinterpret_cast 和 const_cast
    • 消除了 C 风格的类型转换中的二义性,并使显式类型转换更加显眼
  • bool
    • 一种被证明非常有用和流行的布尔类
    • C 和 C++ 曾经使用整数作为布尔变量和常量

dynamic_cast

  • 是一个运行期操作,依赖于存储在类的虚拟函数表中的数据
  • 虽然高效易用,但不是零开销
  • 两种用法
    • Circle* pc = dynamic_cast(p);
    • Circle& rc = dynamic_cast(r);

RAII

  • C++98 中最重要的技术之一

    • Resource Acquisition Is Initialization, 资源获取即初始化
  • 思想

    • 构造函数获取资源、析构函数隐式地释放它
    • 任何携带资源的指针(内存),如果在异常时没有释放,会出现内存泄漏
    • RAII 的做法
      • 使用类封装操作,构造时获取资源,异常、析构时释放资源
  • RAII 加上智能指针消除了对垃圾收集的需求

标准库组件

总结

  • STL
    • 创造性的、通用的、优雅的、高效的容器、迭代器和算法框架
  • 特征 Traits
    • 对使用模板编程有用的编译期属性集
  • string
    • 一种用于保存和操作字符序列的类型。字符类型是一个模板参数,其默认值是 char
  • iostream
    • 处理各种各样的字符类型、区域设置和缓冲策略
  • bitset
    • 用于保存和操作比特位集合的类型
  • locale
    • 一个精致的文化约定框架,主要与 I/O 相关
  • valarray
    • 一个数值数组,带有可优化的向量运算
  • auto_ptr
    • 在 C++11 中,它被 shared_ptr 和 unique_ptr 替代

find_if

  • 该算法在三个维度上都是通用的

    • 元素的存储方式 (vector 、 list 等)
    • 元素的类型 (string 、 int 等)
    • 用于确定何时找到元素的谓词 (Less_than 、 Greater_than 等)
  • 算法没有用到任何面向对象的方法。只依赖模板的泛型编程,有时也被称为编译期多态

    • 编译期多态

其他语言对 C++ 影响 (非 C++98 内容 )

  • auto
    • 从初始化器推断类型的能力。已由来已久,不知最早起源
  • tuple
    • std::tuple 从函数式编程传统语言受到启发,派生自 boost::tuple
  • regex
    • 加入 C++11 的标准库,从 Unix 和 JavaScript 的功能中拷贝来的
  • 函数式编程 Functional programming
    • STL 受到函数式编程的启发
    • 函数式编程特性和 C++ 构造之间有许多明显的相似之处
  • future 和 promise ???
    • 源自 Multilisp
  • 范围 for
    • 直接启发来自 STL sequences
  • variant、any 和 optional
    • 受到多种语言的启发
  • lambda 表达式
    • 函数式语言中 lambda 表达式的应用
  • final 和 override
    • 用于更明确地管理类层次结构
  • 三向比较运算符 <=>
    • 受 C 的 strcmp 及 PERL、PHP、Python 和 Ruby 语言的运算符的启发
  • await ???
    • C++ 里最早的协程
    • 受 Simula 启发
    • C++20 中的无栈协程的思想主要来自 F#

C++ 对其他语言影响 (非 C++98 内容 )

  • Java 和 C# 中的泛型
  • Java、Python 等的资源释放的方法
  • D 编程语言进行编译期求值
  • C++ 基于构造函数和析构函数的对象生存期模型是 Rust 灵感的一部分
  • C 采用了 C++11 的内存模型、函数声明和定义语法、以声明为语句、const、// 注释、inline 以及 for 循环中的初始化表达式
  • C++ 与其他语言之间的许多差异源于 C++ 对析构函数的使用。这使得垃圾收集的语言很难直接从 C++ 借用

C++11

C++11 语言特性

内容

  • 内存模型 memory model
    • 一个高效的为现代硬件设计的底层抽象,作为描述并发的基础
  • auto 和 decltype
    • 避免类型名称的不必要重复
  • 范围 for range-for
    • —对范围的简单顺序遍历
  • 移动语义和右值引用 move semantics and rvalue references
    • 减少数据拷贝
  • 统一初始化 uniform initialization
    • 对所有类型都(几乎)完全一致的初始化语法和语义
  • nullptr
    • 给空指针一个名字
  • constexpr 函数
    • 在编译期进行求值的函数
  • 用户定义字面量 user-defined literals
    • 为用户自定义类型提供字面量支持
  • 原始字符串字面量 raw string literals
    • 不需要转义字符的字面量,主要用在正则表达式中
  • 属性 attributes
    • 将任意信息同一个名字关联
  • lambda 表达式
    • 匿名函数对象
  • 变参模板 variadic templates
    • 可以处理任意个任意类型的参数的模板
  • 模板别名 • template aliases
    • 能够重命名模板并为新名称绑定一些模板参数
  • noexcept
    • 确保函数不会抛出异常的方法
  • override 和 final
    • 用于管理大型类层次结构的明确语法
  • static_assert
    • 编译期断言
  • long long
    • 更长的整数类型
  • 默认成员初始化器 default member initializers
    • 给数据成员一个默认值,这个默认值可以被构造函数中的初始化所取代
  • enum class
    • 枚举值带有作用域的强类型枚举

标准库组件

  • unique_ptr 和 shared_ptr
    • 依赖 RAII 的资源管理指针
  • 内存模型和 atomic 变量 memory model and atomic variables
  • thread、mutex、condition_variable 等
    • 为基本的系统层级的并发提供了类型安全、可移植的支持
  • future、promise 和 packaged_task 等
    • 稍稍更高级的并发
  • tuple
    • 匿名的简单复合类型
  • 类型特征(type trait)
    • 类型的可测试属性,用于元编程
  • 正则表达式匹配 regular expression matching
  • 随机数 random numbers
    • 带有许多生成器(引擎)和多种分布
  • 时间 Time
    • time_point 和 duration
  • unordered_map 等
    • 哈希表
  • forward_list
    • 单向链表
  • array
    • 具有固定常量大小的数组,并且会记住自己的大小
  • emplace 运算
    • 在容器内直接构建对象,避免拷贝
  • exception_ptr
    • 允许在线程之间传递异常

C++11:并发支持 (并发会有单独的学习计划,这里只记关键的笔记)

内存模型

  • 内存模型很大程度上是由 Linux 和 Windows 内核的需求驱动的
  • 内存模型基于 happens-before relations [Lamport 1978]
  • 既支持宽松的内存模型(relaxed memory models), 顺序一致模型(sequentially consistent)
  • 在此上,C++11 还提供了对原子类型(atomic types)和 无锁编程(lock-free programming)的支持
  • atomic 类型
    • atomic x;
    • 双重检查锁定 (double-checked locking )
      • 可使用 atomic 优化
      • 使用相对开销低的 atomic 保护开销大得多的 mutex 的使用
        mutex mutex_x;
        atomic<bool> init_x;  // 初始为 false
        int x;
        
        if (!init_x) {
            lock_guard<mutex> lck(mutex_x);
            if (!init_x) x = 42;
            init_x = true ;
        }  // 在此隐式释放 mutex_x(RAII)
        
        // ... 使用 x ...
        
    • lock_guard
      • 是一种 RAII 类型
      • ,它确保会解锁它所控制的 mutex
  • C++11 还引入了用于无锁编程的关键运算,例如比较和交换
    template<typename T>
    class stack {
        std::atomic<node<T>*> head;
    public:
        void push(const T& data)
        {
            node<T>* new_node = new node<T>(data);
            new_node->next = head.load(std::memory_order_relaxed);
            while(!head.compare_exchange_weak(new_node->next, new_node,
                std::memory_order_release, std::memory_order_relaxed)) ;
        }
        // ...
    };
    

线程和锁

  • thread
    • 系统的执行线程,支持 join() 和 detach()
  • mutex
    • 系统的互斥锁,支持 lock()、unlock() 和保证 unlock() 的 RAII 方式
  • condition_variable
    • 系统中线程间进行事件通信的条件变量
  • thread_local
    • 线程本地存储
  • 类型安全库支持的设计主要依赖变参模板
  • unique_lock
    • 是一个 RAII 对象,确保用户不会忘记在这个 mutex 上调用 unlock()
    • 提供了一种防止最常见形式的死锁的方法
      • lock(lck1,lck2,lck3);
      • C++17 有一个更优雅的解决方案
  • C++20 提供了停止令牌机制来支持阻止线程运行完成的能力

线程和锁

  • future
    • handle, 通过它你可以从一个共享的单对象缓冲区中 get() 一个值,可能需要等待某个 promise 将该值放入缓冲区
  • promise
    • handle, 通过它你可以将一个值 put() 到一个共享的单对象缓冲区,可能会唤醒某个等待 future 的 thread
  • packaged_task
    • class, 它使得设置一个函数在线程上异步执行变得容易,由 future 来接受 promise 返回的结果
  • async()
    • function, 可以启动一个任务并在另一个 thread 上执行
  • 使用这一切的最简单方法是使用 async()。
    • 给定一个普通函数作为参数,async() 在一个 thread 上运行它,处理线程启动和通信的所有细节
    • 示例 略
    • 感觉跟JS的Ajax很像
    • 标准库的 packaged_task 自动化了这个过程,可以将普通函数包装成一个函数对象,负责 promise/future 的自动配置并处理返回和异常

C++11:简化使用

  • C++11 提供了一些特别的功能,旨在简化初学者和非语言专家对 C++ 的使用, 如:
    • auto
    • 范围 for
    • 移动语义和右值引用
    • 资源管理指针
    • 统一初始化
    • nullptr
    • constexpr
    • 用户定义字面量
    • 原始字符串字面量
    • 属性
    • 与可选的垃圾收集器之间的接口
    • lambda 表达式

auto 和 decltype

  • auto 允许从初始化表达式中推导出对象的静态类型
  • 如果要用动态类型的变量,应该使用 variant 或者 any
  • 把引用的类型也推导为一个引用
    • decltype 运算符
      • decltype(r) r2 = r;
  • C++11 中添加了一种弱化的 auto 用法,把返回类型的说明放到参数后面。
    • template auto vector::begin() -> iterator { /* ... */ }
    • auto f() -> int;
  • C++ 20 用概念来约束返回类型
    • Channel auto y = flopscomps(x,3); // y 可以当做 Channel 使用

range-for

  • range-for 是用来顺序遍历一个序列中所有元素的语句
    • for (auto i : {1,2,3,5,8}) sum+=i;

移动语义

  • 传统做法是在自由存储区(堆、动态内存)上分配空间,然后传递指向该空间的指针作为函数参数
    • 它是显式使用指针的主要来源之一,导致了写法上的不便、显式的内存管理,以及难以查找的错误
    • 很多专家使用“取巧”的办法来解决这个问题:把句柄类作为简单数值(常称为值类型)来传递
      • Matrix operator+(const Matrix&, const Matrix&);
        • 通过 const 引用把 Matrix 传递给函数,一直是传统而高效的做法。
        • 如何以传值来返回 Matrix 而不用拷贝所有的元素
      • 引入移动构造
      • && 表示构造函数是一个移动构造函数
      • Matrix&& 被称为右值引用
      • 当用于模板参数时,右值引用的符号 && 被叫做转发引用
  • 移动语义蕴含着性能上的重大好处:它消除了代价高昂的临时变量
    • Matrix mx = m1+m2+m3; // 不需要临时变量
  • 移动语义是 C++ 资源管理模型的重要基石
  • 成“智能指针”的工厂函数示例:
    template <class T, class A1>
    std::shared_ptr<T> factory(A1&& a1)
    {
        return std::shared_ptr<T>(new T(std::forward<A1>(a1)));
    }
    
    • forward 告诉编译器将实参视为右值引用

unique_ptr 和 shared_ptr

  • shared_ptr
    • 代表共享所有权
    • 由于计数,有额外开销,尤其是多线程开发中
  • unique_ptr
    • 代表独占所有权(取代 C++98 中的 auto_ptr)
    • 不引入额外开销
  • 显式使用 new 和 delete 的旧方式容易出错,在现代 C++ 中已经不推荐使用
  • 智能指针用于表示资源所有权的主要用途是面向对象编程,其中指针(或引用)用于访问对象

统一初始化

  • 从 C 语言中,C++ 继承了三种初始化形式,并添加了第四种形式:
    • int x; // 默认初始化(仅适用于静态变量)
    • int x = 7; // 值初始化
    • int a[] = {7,8}; // 聚合初始化
    • string s; // 由默认构造函数初始化
    • vector v(10); // 由构造函数初始化
  • 统一初始化方法
    • 基于使用花括号的列表写法,花括号初始化器列表之前的 = 也是可选的
      • int a = {5}; // 内建类型
      • int a[] {7,8}; // 数组
      • vector v = {7,8}; // 具有构造函数的用户定义的类型
        int f(vector<int>);
        int i = f({1,2,3});  // 函数参数
        
        struct X {
            vector<int> v;
            int a[];
            X() : v{1,2}, a{3,4} {}  // 成员初始化器
            X(int);
            // ...
        }
        
        vector<int>* p = new vector<int>{1,2,3,4};  // new 表达式
        X x {};  // 默认初始化
        
        template<typename T> int foo(T);
        int z = foo(X{1});  // 显式构造
        

nullptr 略

constexpr 函数

  • 允许在常量表达式中使用以 constexpr 为前缀的函数
  • 允许在常量表达式中使用简单用户定义类型–字面量类型
  • 字面量类型基本上是一种所有运算都是 constexpr 的类型
  • 传统的解决方案要么需要更多的运行时间,要么需要程序员在草稿纸上算好值
  • constexpr 函数可以在编译期进行求值,因此它无法访问非本地对象(它们在编译时还不存在)
  • 在某些地方,C++ 需要常量表达式(例如,数组边界和 case 标签)
  • constexpr 函数才成为元编程的主要支柱
  • C++14 允许在 constexpr 函数中使用局部变量,从而支持了循环
  • C++20 允许将字面类型用作值模板参数类型
  • C++20 将非常接近最初的目标
    • 可以使用内建类型的地方也都可以使用用户定义的类型

用户定义字面量

  • operator"" 作为字面量运算符
    • ""x 是后面跟着后缀 x 的字面量
      • constexpr Imaginary operator""i(long double x) { return Imaginary(x); }
      • 3.4i 是一个 Imaginary

原始字符串字面量

  • 原始字符串字面量 R"(…)"

属性 attribute

  • 供了一种将本质上任意的信息与程序中的实体相关联的方法
    • [[noreturn]] void forever() {...}
    • 属性 [[noreturn]] 通知编译器或其他工具 forever() 永远不会返回,这样它就可以抑制关于缺少返回的警告
  • 属性用 [[…]] 括起来
  • C++11 增加了标准属性 [[noreturn]] 和 [[carries_dependency]]
  • C++17 增加了 [[fallthrough]]、[[nodiscard]] 和 [[maybe_unused]]
  • C++20 增加了 [[likely]]、[[unlikely]]、[[deprecated(message)]]、[[no_unique_address]] 和 [[using: …]]
  • 忽略属性,编译器不会有任何危害

C++11:改进对泛型编程的支持

  • 泛型编程好处
    • 超越以 C 风格或面向对象风格所可能获得的灵活性
    • 更清晰的代码
    • 更细的静态类型检查粒度
    • 效率(主要来自内联、让编译器同时查看多处的源代码,以及更好的类型检查)
  • C++11 中支持泛型编程的主要新特性有
    • lambda 表达式
    • 变参模板 Variadic templates
    • template 别名 template aliases
    • tuple
    • 统一初始化 uniform initialization

lambda 表达式

  • C++ 不允许在函数内部定义函数,而是依赖于在类内部定义的函数
    • 这使得函数的上下文可以表示为类成员
    • 因而函数对象变得非常流行
    • 函数对象
      • 只是一个带有调用运算符(operator()())的类
        struct Less_than {
            int& i;
            Less_than(int& ii) :i(ii) {}  // 绑定到 i
            bool operator()(int x) { return x<i; }  // 跟参数比较
        }
        
  • lambda 表达式示例
    void test()
    {
        string s;
        // ... 为 s 计算一个合适的值 ...
        w.foo_callback([&s](int i){ do_foo(i,s); });
        w.bar_callback([=s](double d){ return do_bar(d,s); });
    }
    
    • 默认情况下,lambda 表达式不能引用在本地环境的名字
    • 可以指定 lambda 表达式应该从它的环境中“捕获”一些或所有的变量
    • [&s] 传递引用
      • 一个 [&] 捕获列表意味着“lambda 表达式可以通过引用指代所有局部变量
    • [=s] 传值
      • 一个 [=] 捕获列表意味着“将所有局部变量复制到 lambda 表达式中”
  • lambda 表达式的返回类型可以从它的返回语句推导出来
    • 如果没有 return 语句,lambda 表达式就不会返回任何东西
  • C++14 添加了泛型 lambda 表达式和移动捕获
  • lambda 表达式 和 函数对象可以互转的,个人理解,尽量使用 lambda 表达式作为内部函数
  • lambda 表达式就是函数对象

变参模板

  • 解决两个问题
    • 不能实例化包含任意长度参数列表的类模板和函数模板
    • 不能以类型安全的方式传递任意个参数给某个函数
  • 思路
    • 递归构造一个参数包,然后在另一个递归过程来使用它
  • 用法
    • template
  • 缺点
    • 容易导致代码膨胀,因为 N 个参数意味着模板的 N 次实例化

别名 template aliases

  • 示例

    • typedef double (*analysis_fp)(const vector&);
    • using analysis_fp = double (*)(const vector&);
  • 类型和模板别名是某些最有效的零开销抽象及模块化技巧的关键

    • 别名让用户能够使用一套标准的名字而同时让各种实现使用各自(不同)的实现技巧和名字
    • 这样就可以在拥有零开销抽象的同时保持方便的用户接口
  • 比较好用的模板示例

    template<InputTransport Transport, MesssageDecoder MessageAdapter>
    class InputChannel {
    public:
        template<typename... TransportArgs>
            InputChannel(TransportArgs&&... transportArgs)
                : _transport {forward<TransportArgs>(transportArgs)... }
            {}
        // ...
        Transport _transport;
    }
    

tuple

  • 用法
    • 作为返回类型,用于需要超过一个返回类型的函数
    • 编组相关的类型或对象(如参数列表中的各条目)成为单个条目
    • 同时赋多个值
  • 当撰写泛型代码时,把多个值打包到一个元组中作为一个实体进行处理往往能简化实现
  • tuple 对于不值得命名、不值得设计类的一些中间情况特别有用
  • make_tuple()
    • 是标准库函数,可以从参数中推导元素类型来构造 tuple
  • tie()
    • 是标准库函数,可以把 tuple 的成员赋给有名字的变量
  • 使用 C++17 的结构化绑定
    auto SVD(const Matrix& A) -> tuple<Matrix, Vector, Matrix>
    {
        Matrix U, V;
        Vector S;
        // ...
        return {U,S,V};
    };
    
    void use()
    {
        Matrix A;
        // ...
        auto [U,S,V] = SVD(A); // 使用元组形式和结构化绑定
    }
    

C++11:提高静态类型安全

  • 依赖静态类型安全有两大好处
    • 明确意图
      • 帮助程序员直接表达想法
      • 帮助编译器捕获更多错
    • 帮助编译器生成更好的代码
  • int 或者 string 几乎可以表达任何东西
  • C++11 中与类型安全直接相关的改进有
    • 对于线程和锁的类型安全接口
      • 避免 POSIX 和 Windows 在并发代码中对 void** 及宏的依赖
    • range-for
      • 避免错误地指定范围
    • 移动语义
      • 解决指针的过度使用问题
    • 资源管理指针
      • unique_ptr 和 shared_ptr
    • 统一初始化
      • 让初始化更通用,更一致,更安全
    • constexpr
      • 消除多处(无类型和无作用域的)宏的使用
    • 用户定义的字面量
      • 让用户定义类型更像内建类型
    • enum class
      • 消除一些涉及整型常量的弱类型做法
    • std::array
      • 避免内建数组不安全地“退化”成指针

C++11:支持对库的开发

实现技巧

  • enable_if 元函数由 Boost 开创并成为 C++11 的一部分
  • is_copy_assignable 是一个 type trait
  • is_nothrow_constructible<> 是 C++11 标准库的类型特征(type traits)

元编程支持 Metaprogramming Support

  • 尝试采用了两条(至少理论上)互补的路径
    • Language
      • concepts
      • compile-time functions
      • lambdas
      • template aliases
      • 及更精确的模板实例化规范
    • Standard library
      • tuples
      • type traits
      • enable_if

noexcept 规约

  • 在 C++11 中,异常规约被废弃
  • C++17,移除了异常规约这个特性
  • noexcept
    • void do_something(int n) noexcept {...}
      • 如果 do_something() 抛异常,程序会被终止
      • 这样操作恰好非常接近零开销
        • 因为它简单地短路了通常的异常传播机制

C++11:Standard-Library Components

  • thread
    • 基于线程和锁的并发
  • regex
    • 正则表达式
  • chrono
    • 时间
    • C++20,chrono 得到进一步增强,加入了处理日期和时区的功能
  • random
    • 随机数产生器和分布

C++ 14: 完成 C++11

C++14 的主要语言特性

  • 二进制字面量 Binary literals
    • 例如 0b1001000011110011
  • 数字分隔符 Digit separators
    • 为了可读性,例如 0b1001’0000’1111’0011
  • 变量模板 Variable templates
    • 参数化的常量和变量
  • 函数返回类型推导 Function return type deduction
  • 泛型 lambda 表达式 Generic lambdas
  • constexpr 函数中的局部变量
  • 移动捕获 Move capture
    • 例如 [p = move(ptr)] {/* ... */}; 将值移入 lambda 表达式
  • 按类型访问元组 Accessing a tuple by type
    • 例如 x = get(t);
  • 标准库中的用户定义字面量
    • 例如:10i,"Hello"s,10s,3ms,55us,17ns

数字分隔符

  • 人们正在使用下划线作为用户定义字面量后缀的一部分
    • auto a = 1_234_567_s; // 1234567 秒
  • 库小组为标准库保留了不以下划线开头的后缀
  • 使用单引号作为用户定义字面量分隔符
    • auto a = 1'234'567; // 1234567(整数)
    • auto b = 1'234'567s; // 1234567 秒

变量模板

template<typename T>
constexpr T pi = T(3.1415926535897932385);

template<typename T>
T circular_area(T r)
{
    return pi<T> * r * r;
}
  • C++20 标准库提供了一组定义为变量模板的数学常数

    template<typename T> constexpr T pi_v = unspecified;
    constexpr double pi = pi_v<double>;
    

函数返回类型推导

  • C++11 引入了从 lambda 表达式的 return 语句来推导其返回类型的特性。C++14 将该特性扩展到了函数

    template<typename T>
    auto size(const T& a) { return a.size(); }
    
    • 此类函数不能提供稳定的接口,因为它的类型现在取决于它的实现
    • 而且在编译到使用这个函数的代码时,函数实现必须是可见的

泛型 lambda 表达式

  • 语法中省略类型
    • auto get_size = [](auto& m){ return m.size(); }

constexpr 函数中的局部变量

  • 允许使用局部变量和 for 循环

    constexpr int min(std::initializer_list<int> xs)
    {
      int low = std::numeric_limits<int>::max();
      for (int x : xs)
        if (x < low)
          low = x;
      return low;
    }
    
    constexpr int m = min({1,3,2,4});
    
    • 这个 min() 函数可以在编译时进行求值

C++17:大海迷航

C++17 的主要语言特性

特性总结

  • C++17 有大约 21 个新的语言特性:

    • 构造函数模板参数推导 Constructor template argument deduction
      • 简化对象定义
    • 推导指引 Deduction guides
      • 解决构造函数模板参数推导歧义的显式标注
    • 结构化绑定 Structured bindings
      • 简化标注,并消除一种未初始化变量的来源
    • inline 变量 inline variables
      • 简化了那些仅有头文件的库实现中的静态分配变量的使用
    • 折叠表达式 Fold expressions
      • 简化变参模板的一些用法
    • 条件中的显式测试 Explicit test in conditions
      • 有点像 for 语句中的条件
    • 保证的复制消除 Guaranteed copy elision
      • 去除了很多不必要的拷贝操作
    • 更严格的表达式求值顺序 Stricter expression evaluation order
      • 防止了一些细微的求值顺序错误
    • auto 当作模板参数类型 auto as a template argument type
      • 值模板参数的类型推导
    • 捕捉常见错误的标准属性 Standard attributes to catch common mistakes
      • [[maybe_unused]]、[[nodiscard]] 和 [[fallthrough]]
    • 十六进制浮点字面量 Hexadecimal floating-point literals
    • 常量表达式 if Constant expression if ś
      • 简化编译期求值的代码
    • 等等 。。。

标准库组件

  • C++17 标准库中增加了大约 13 个新特性
    • optional、any 和 variant
      • 用于表达“可选”的标准库类型
    • hared_mutex 和 shared_lock(读写锁)和 scoped_lock
    • 并行 STL
      • 标准库算法的多线程及矢量化版本
    • 文件系统
      • 可移植地操作文件系统路径和目录的能力
    • string_view
      • 对不可变字符序列的非所有权引用
    • 数学特殊函数 Mathematical special functions
      • 包括拉盖尔和勒让德多项式、贝塔函数、黎曼泽塔函数

具体内容

构造函数模板参数推导

  • 简化了类型的使用
    • 例如 pair 和 tuple,还有当编写并行的代码时用到的锁和互斥锁
    • shared_lock lck {m}; // 不需要显式写出锁类型

结构化绑定

  • 简化写法和消除剩余变量

    template<typename T, typename U>
    void print(vector<pair<T,U>>& v)
    {
        for (auto [x,y] : v)
            cout << '{' << x << ' ' << y << "}\n";
    }
    
    auto [x,y,z] = f(); // 调用语法,引入别名
    

variant、optional 和 any

optional<int> var1 = 7;
variant<int,string> var2 = 7;
any var3 = 7;

auto x1 = *var1 ;               // 对 optional 解引用
auto x2 = get<int>(var2);       // 像访问 tuple 一样访问 variant
auto x3 = any_cast<int>(var3);  // 转换 any
  • 可选类型可以用 union 表示,没有运行期开销

  • overloaded 实现

    using var_t = std::variant<int, long, double, std::string>; // variant 类型
    
    // 简单访问的样板:
    template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
    template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
    
    void use()
    {
        std::vector<var_t> vec = {10, 20L, 30.40, "hello"};
    
        for (auto& var : vec) {
            std::visit (overloaded {
                [](auto arg) { cout << arg << '\n'; },    // 处理整数类型
                [](double arg) { cout << "double : " << arg << '\n'; },
                [](const std::string& arg) { cout << "\"" << arg << "\"\n"; },
            }, var);
        }
    }
    

并发

  • 在 C++17 中,以下类型的加入极大地简化了锁的使用
    • scoped_lock
      • 获取任意数量的锁,而不会造成死锁
      • shared_mutex 和 shared_lock
        • 实现读写锁
  • 多个读线程可以“共享”该锁(即同时进入临界区),而写线程则需要独占访问。

并行 STL

  • 基本的想法是,为每个标准库算法提供一个额外参数,允许用户请求向量化和/或多线程
    • sort(par_unseq, begin(v), end(v)); // 考虑并行和向量化
  • C++17 的并行算法也支持向量化。
  • 在 C++20 中,能用范围库来避免显式使用容器的元素序列
    • sort(v);
  • 并行版本的范围在 C++20 中没有及时完成,因此我们只能等到 C++23 才能这么写
    • sort(par_unseq, v); // 使用并行和向量化来对 v 进行排序
    • 可以自己实现
      template<typename T>
      concept execution_policy = std::is_execution_policy<T>::value;
      
      void sort(execution_policy auto&& ex, std::random_access_range auto& r)
      {
          sort(ex, begin(r), end(r));  // 使用执行策略 ex 来排序
      }
      

文件系统

  • 提供的关键类型是 path,对字符集和文件系统的不同写法进行了抽象
void do_something(const string& name)
{
    path p {name};  // name 可能是俄语或阿拉伯语
                    // name 可能使用 Windows 或 Linux 文件写法
    try {
        if (exists(p)) {
            if (is_regular_file(p))
                cout << p << " regular file, size: " << file_size(p) << '\n';
            else if (is_directory(p)) {
                cout << p << " directory, containing:\n";
                for (auto& x : directory_iterator(p))
                    cout << "    " << x.path() << '\n';
            }
            else
                cout << p << " exists\n";
        }
        else
            cout << p << " does not exist\n";
    }
    catch (const filesystem_error& ex) {
        cerr << ex.what() << '\n';
        throw;
    }
    // ... 使用 p ...
}
  • 捕捉异常可以防止罕见的错误,比如在 exists§ 检查后、执行详细检索前删除了文件。文件系统接口同时为罕见(异常)和常见(预期)错误提供了支持

条件的显式测试

  • 考虑为条件增加显式测试的能力
if (auto p = f(y); p->m>0) {
    // ...
}
  • 注意空指针问题
  • 为了通用,显式测试也可以用在 switch 和 while 条件中。
  • 在 C++20 中,这一机制被进一步扩展到可以在范围 for 语句中包含初始化

C++20:方向之争

C++20 的主要语言特性

特性总结

  • 概念 Concepts
    • 对泛型代码的要求进行明确规定
  • 模块 Modules
    • 支持代码的模块化,使代码更卫生并改善编译时间
  • 协程 Coroutines
    • 无栈协程
  • 编译期计算支持 : Compile-time computation support
  • <=>
    • 三向比较运算符
  • 范围 Ranges
    • 提供灵活的范围抽象的库
  • 日期 Date
    • 提供日期类型、日历和时区的库
  • 跨度 Span
    • 提供对数组进行高效和安全访问的库
  • 格式化 Format
    • 提供类型安全的类似于 printf 的输出的库
  • 并发改进 Concurrency improvements
    • 例如作用域线程和停止令牌
  • 很多次要特性 Many minor features
    • 例如 C99 风格的指派初始化器和使用字符串字面量作为模板参数

次要特性

  • C99 风格的指派初始化器
  • 对 lambda 捕获的改进
  • 泛型 lambda 表达式的模板参数列表
  • 范围 for 中初始化一个额外的变量
  • 不求值语境中的 lambda 表达式
  • lambda 捕获中的包展开
  • 在一些情况下移除对 typename 的需要
  • 更多属性:[[likely]] 和 [[unlikely]]
  • 在不使用宏的情况下,source_location 给出一段代码中的源码位置
  • 功能测试宏
  • 条件 explicit
  • 有符号整数保证是 2 的补码
  • 数学上的常数
    • 比如 pi 和 sqrt2
  • 位的操作,比如轮转和统计 1 的个数

具体内容

模块

  • #include 中的任何内容都会影响所有后续的 #include
  • 模块示例
    export module map_printer;  // 定义一个模块
    
    import iostream;       // 使用 iostream
    import containers;     // 使用我自己的 containers
    using namespace std;
    
    export                 // 让 print_map() 对 map_printer 的用户可用
    template<Sequence S>
        requires Printable<Key_type<S>> && Printable<Value_type<S>>
    void print_map(const S& m) {
        for (const auto& [key,val] : m)  // 分离“键”和“值”
            cout << key << " -> " << val << '\n';
    }
    
    • 这段代码定义了一个模块 map_printer
    • 该模块提供函数 print_map 作为其用户接口
    • 使用了从模块 iostream 和 containers 导入的功能来实现该函数
    • 关键思想
      • export 指令使实体可以被 import 到另一个模块中
      • import 指令使从另一个模块 export 出来的实体能够被使用
      • import 的实体不会被隐式地再 export 出去
      • import 不会将实体添加到上下文中;它只会使实体能被使用(因此,未使用的 import 基本上是无开销的)
  • 兼容 #include 的写法示例
    export module map_printer;  // 定义一个模块
    
    import       // 使用 iostream 头文件
    import "containers"    // 使用我自己的 containers 头文件
    using namespace std;
    
    export                 // 让 print_map() 对 map_printer 的用户可用
    template<Sequence S>
        requires Printable<Key_type<S>> && Printable<Value_type<S>>
    void print_map(const S& m) {
        for (const auto& [key,val] : m)  // 分离“键”和“值”
            cout << key << " -> " << val << '\n';
    }
    
    • 编译器确保 import 导入的“旧头文件”不具有相互依赖关系
    • 头文件的 import 是顺序无关的

协程

  • 演化工作组引入了关键字 co_return、co_yield 和 co_await,用于协程中的三个关键操作

  • TS 协程的一个重要且可能致命的问题是,它依赖于自由存储区(动态内存、堆)上的分配

  • 协程示例

    generator<int> fibonacci()  // 生成 0,1,1,2,3,5,8,13 ...
    {
        int a = 0;    // 初值
        int b = 1;
    
        while (true) {
            int next = a+b;
            co_yield a;    // 返回下一个斐波那契数
            a = b;         // 更新值
            b = next;
        }
    }
    
    int main()
    {
        for (auto v : fibonacci())
            cout << v << '\n';
    }
    
    • 使用 co_yield 使 fibonacci() 成为一个协程
    • 对协程返回类型的标准库支持仍然不完整,不过库就应该在生产环境的使用中成熟

    编译期计算支持

    • 模板元编程主要旨在将计算从运行期转移到编译期
    • 在早期的 C++ 中,对重载的依赖以及虚函数表的使用都可以看作是通过将计算从运行期转移到编译期来获得性能
    • 编译期计算一直是 C++ 的关键部分
    • 一旦引入模板并发现了模板元编程,模板元编程就被广泛用于在编译期计算值和类型上
    • C++11 里的 constexpr 函数,它是现代编译期编程的基础
    • C++14 推广了 constexpr 函数
    • C++20 增加了好几个相关的特性
      • consteval
        • 保证在编译期进行求值的 constexpr 函数
      • constinit
        • 保证在编译期初始化的声明修饰符
      • 允许在 constexpr 函数中使用成对的 new 和 delete
      • constexpr string 和 constexpr vector
      • 使用 virtual 函数
      • 使用 unions、异常、dynamic_cast 和 typeid
      • 使用用户定义类型作为值模板参数
        • 最终允许在任何可以用内置类型的地方使用用户定义类型
      • is_constant_evaluated() 谓词
        • 使库实现者能够在优化代码时大大减少平台相关的内部函数的使用
    • 最终目的是为了让 C++23 或更高版本支持静态反射
    • C++ 程序员必须学会限制编译期计算和元编程的使用,只有在值得为了代码紧凑性和运行期性能而引入它们的地方才使用

<=> 飞船运算符 spaceship operator

  • <=> 生成的 == 则必须读取足够的字符串以确定它们的词典顺序,那开销就会大得多了
  • 对于字符串,== 通常通过首先比较大小来优化
  • 个人理解,对于字符串的比较,还是使用 == 性能更好

范围 Ranges

  • C++20 标准库为整个容器的操作提供了更简单的表示方法
    • sort(vs);
  • range 本身是一种 concept
    • 所有 C++20 标准库算法现在都使用 concept 进行了精确规定
  • 这种推广允许我们把算法如管道般连接起来
    vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    auto even = [](int i){ return i%2 == 0; }
    
    for (int i : vec | view::filter(even)
                     | view::transform( [](int i) { return i*i; } )
                     | view::take(5))
        cout << i << '\n';    // 打印前 5 个偶整数的平方
    
  • pipeline operator | 管道运算
    • 将其左操作数的输出作为输入传递到其右操作数
      • 例如 A|B 表示 B(A)

日期和时区

  • 和旧的时间工具一起放在 中

  • 表达时间点(time_point)

    constexpr auto tp = 2016y/May/29d + 7h + 30min + 6s + 153ms;
    cout << tp << '\n';    // 2016-05-29 07:30:06.153
    
    • 使用用户定义的字面量
    • 当需要时,日期会在编译期映射到标准时间线(system_time)上的某个点
    • 因此它极其快速,也可以在常量表达式中使用
      • static_assert(2016y/May/29==Thursday); // 编译期检查
  • 默认情况下,时区是 UTC(又称 Unix 时间),但转换为不同的时区很容易

    zoned_time zt = {"Asia/Tokyo", tp};
    cout << zt << '\n';          // 2016-05-29 16:30:06.153 JST
    

格式化 Format

  • 示例
    • s 在 s.size() 前
      • cout << format("The string '{0}' has {1} characters",s,s.size());
    • s.size() 在 s 前
      • cout << format("The string '{1}' has {0} characters",s.size(),s);
    • 时间
      • string s1 = format("{}", birthday);
      • string s2 = format("{0:>15%Y-%m-%d}", birthday);
        • “y-m-d”是默认格式
        • 15 意味着使用 15 个字符和右对齐文本

    • 可以用来处理时区和区域
      • std::format(std::locale{"fi_FI"}, "{}", zt);
      • 给出芬兰的当地时间
      • 默认情况下,格式化不依赖于区域, 可以选择是否根据区域来格式化
      • 默认区域无关的格式化大大提升了性能

跨度 Span

  • 越界访问,有时也称为缓冲区溢出,从 C 的时代以来就一直是一个严重的问题

  • span 类模板

    • 示例
      void f(span<int> a)  // span 包含一根指针和一条大小信息
      {
          for (int& x : a)
              x = 7;  // 可以
      }
      
    • 范围 for 从跨度中提取范围,并准确地遍历正确数量的元素(无需代价高昂的范围检查)
    • 这个例子说明了一个适当的抽象可以同时简化写法并提升性能
  • 如果有必要的话,可以显式地指定一个大小(比如操作一个子范围)

    • 但这样的话,需要承担风险
      int x = 100;
      int a[100];
      f(a);        // 模板参数推导:f(span{a, 100})
      f({a,x/2});  // 可以:a 的前半部分
      f({a,x+1});  // 灾难
      
    • span::size() 被定义返回一个有符号整数

    并发

    • 通用并发模型(“执行器”)在 C++20 中还没有准备好
    • 完成的内容
      • jthread 和 停止令牌 stop tokens
      • atomic
      • 经典的信号量 classic semaphores
      • 屏障和锁存器 barriers and latches
      • 小的内存模型的修复和改进
    • jthread(“joining thread”的缩写)
      • 是一个遵守 RAII 的线程
      • 也就是说,如果 jthread 超出作用域了,它的析构函数将合并线程而不是终止程序
      • 示例
        void some_fct()
        {
            thread t1;
            jthread t2;
            // ...
        }
        
        • 在作用域的最后,t1 的析构函数会终止程序,除非 t1 的任务已经完成,已经 join 或 detach
        • 而 t2 的析构函数将会等待其任务完成
    • stop tokens
      • 基本思想是使用协作式的线程取消方式

你可能感兴趣的:(笔记,c++,开发语言)