C++历史标准的发展及常见的Workaround和最佳实践

C++历史概述

C++是一门历史悠久的语言,原来叫C with classes,自1979年以来已经出现了98/03/11/14/17五个标准,因此C++中的Workaround和Best practice是随着标准演化而推进的。因此这里首先概览一下C++从开始设计以来的特性的引入、废止与更新的过程,以及对C++11及以后引入的重要特性的一些论述。

史前时代

1979年,这门语言基于C(cfront将C++编译到C)实现了对象机制(但还没有虚函数等运行时多态),inline函数(大快人心的大好事)等。1985年,cfront正式面世,现在虚函数、重载、引用、new/delete、const这些我们常见的概念都出现了,而标准库也有了雏形。1989年是另一个重要的年头,多继承、抽象类、static/const成员函数、类限定的指针(指向类成员)/new/delete应运而生,C++的OOP机制进一步增强。
1998年,C++迎来第一个ISO标准。98标准列出了RTTI机制、virtual(cppreference上翻译成了协变返回类型)、各种cast以及自定义的operator T、mutable/bool关键字和模板的一些特性。
2003年,C++03发布了,这是一个次要版本,之后C++0x标准的出台陷入了长久的等待。在这段时间中C++标准库引入了tr和tr1两个扩展包(主要来自Boost和C99的冷饭)。

C++11

2011年,C++11千呼万唤始出来。C++11带来的主要新特性如下,其中斜体表示标准库特性:

  1. auto/decltype、尾随返回类型(trailing)

    1
    2
    3
    4
    template<typename T, typename U>
    auto add(T x, U y) -> decltype(x + y){
    return x + y;
    }

    decltype和完美转发能够非常方便地替换result_typeargument_type之类的手动实现,于是在C++17中,这些机制被去除了。与之同时被去除的还有result_of现在变为invoke_result

  2. 右值/移动语义/完美转发
    详见我的文章C++右值

  3. enum class

  4. =delete=default

  5. final/override

  6. constexpr

  7. user defined literals
    这里指可以支持operator""

  8. 列表初始化、委托/继承构造函数、花括号初始化器
    详见我的文章C++初始化方式

  9. nullptr/nullptr_t/long long/char16_t/char32_t等类型

  10. 类型别名(别名模板)
    现在我们可以用using替代typedef

    1
    2
    template <typename T> using V = vector<T>;
    using VI = vector<int>;
  11. 可变参数模板(参数包)
    详见我的文章C++模板编程

  12. 放松了union的限制

  13. 放松了POD的限制,现在分为trivial和standard_layout
    详见C++初始化方式和我的其他文章

  14. Unicode字面量

  15. 用户自定义字面量和operator ""
    现在支持下面这些很骚的写法

    1
    2
    12_km
    0.5_Pa
  16. 属性
    属性用两个中括号括起来,如[[ noreturn ]],用来标准化像__attribute____declspec这类的用法

  17. lambda表达式

  18. noexcept和新的异常处理机制

  19. alignofalignas
    详见我的文章C++内存对齐与多态

  20. thread_local原子库和线程库
    包括<thread><conditional_variable><mutex><future><atomic>等头文件。
    详见我的文章并发编程重要概念及比较

  21. GC的API

  22. range for

    1
    2
    3
    4
    int ran[3] = {1,2,3};
    for(int & x: ran){
    ...
    }
  23. static assert

  24. emplace_back
    C++11的完美转发和参数包特性支持了emplace_back的实现,我们避免了构造并复制/移动临时对象的开销。

  25. std::initializer_list

  26. std::forward_list

  27. chrono库

  28. ratio库

  29. algorithm库
    引入了新的算法,如all_of系列、is_permutation系列、copy_系列、move系列(注意不是右值引用的那个move)、shuffleis_partioned系列、is_sorted系列、is_heap系列。

  30. 新的allocator:std::scoped_allocator_adaptor

  31. 合并tr1
    tr1主要包含了reference_wrapper、智能指针、mem_fn(之前从来没用过)、result_ofstd::functionstd::bind、各种trait函数、随机数模块、新的数学函数、tuplearray、regex、unordered_系列容器。

  32. 在exception库中合并Boost的部分异常处理机制exception_ptrerror_codeerror_condition

  33. 合并Boost的迭代器std::beginstd::endstd::nextstd::prev

  34. 标准库新增容器unordered_mapunordered_settuplearray(从tr1中引入)

  35. string库
    增加了std::to_string等函数

C++14

  1. lambda
    C++14放松了lambda的要求,现在lambda的参数可以使用auto声明了

    1
    2
    3
    4
    // C++11
    auto l = [](int x){return x;};
    // C++14
    auto l = [](auto x){return x;};

    此外,在C++11中lambda表达式是无法捕获右值得到,于是有了诸如Lambda如何捕获转换自临时变量的右值这样的问题。
    现在lambda允许捕获右值了,这是典型的给C++11擦屁股

    1
    2
    3
    4
    5
    Value v = ...;
    // C++14
    auto l = [value = std::move(v)] {return *value;};
    // C++11
    auto l = std::bind([] (const Value & value){return *value;}, std::move(v));
  2. 增强了类型推导(函数返回值)
    对于普通函数,我们现在可以进行如下声明而免去尾随返回类型(trailing)了。但是我们要注意对于有多个return的情形,我们要保证所有的返回值推导到一致的类型,对于函数中出现递归调用的情况,需要在当前行前看到至少一个return定义。如下面的例子中语句2涉及递归调用,那么在它前面必须要出现一个能够推导的return(语句1),如果我们颠倒语句1和2的出现次序,编译将报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    auto fac(int i){
    if(i == 0){
    // 1
    return 1;
    }else{
    // 2
    return fac(i - 1) * i;
    }
    }
  3. 增强了类型推导(decltype(auto)
    C++11引入了两种类型推导方式autodecltype。其中auto始终推导出一个非引用的类型,如同std::decay所做的那样,而auto &&始终推导出一个引用类型。decltype的推导结果则和具体表达式密切相关,例如decltype(*ptr)是带引用的。WIKI上列出了一些demo,我们可以看到decltype区分值和表达式,对于表达式始终是返回引用的。

    1
    2
    3
    4
    5
    6
    7
    8
    int i;
    int&& f();
    auto x3a = i; // x3a的类型是int
    decltype(i) x3d = i; // x3d的类型是int
    auto x4a = (i); // x4a的类型是int
    decltype((i)) x4d = (i); // x4d的类型是int&
    auto x5a = f(); // x5a的类型是int
    decltype(f()) x5d = f(); // x5d的类型是int&&

    C++14通过decltype(auto)auto声明提供了decltype的行为。

  4. 放松了constexpr
    根据文章,这一些列变化主要体现在放松了对constexpr函数体中的限制

  5. 模板变量

  6. 放松了聚合初始化的要求
    C++11允许使用default member initializer初始化构造函数没有初始化的成员。C++14允许在聚合初始化中使用这个特性,下面的代码现在成为可能

    1
    2
    3
    4
    5
    struct CXX14_aggregate {
    int x;
    int y = 42;
    };
    CXX14_aggregate a = {1}; // C++14允许。a.y被初始化为42
  7. 0b引导的2进制数字常量、数字分位符(便于阅读)

  8. 线程库和原子库
    引入<shared_mutex>读写锁的相关实现shared_timed_mutexshared_lock(注意shared_mutex从C++17开始提供)。
    shared_lock配合shared_mutex可以实现读锁。

  9. 标准库提供了一系列_t函数用来简化提取traits书写

  10. 标准库提供了一系列自定义literal来简化书写
    这个特性能够实现例如chrono库中的“时间单位”,shmsus等。这个是通过String Literal Operator,即operator””实现的。特别需要注意的是,由于这项特性的引入,连接字符串字面量和变量的时候必须要在中间空一格空格。例如

    1
    2
    3
    4
    // Not OK
    "%"PRId64
    // OK
    "%" PRId64
  11. 关联容器支持异构查找

  12. 标准库的其他更新

    1. tuple允许通过类型索引(但必须类型在tuple中是唯一的),看起来并没有什么用
    2. 引入了std::make_unique给C++11擦屁股
    3. 整数序列std::integer_sequence
    4. 增强了traits(擦屁股)
      std::integral_constant的const的operator()
      std::is_final
    5. std::exchange函数
    6. 增加了全局的std::cbegin/std::cend(又是擦屁股)
    7. std::quoted函数用来处理IO

C++17

  1. static_assert现在可以省略第二个参数了(这居然也算特性)

  2. Class template argument deduction (CTAD)

  3. template template现在允许typename了,之前只能用class
    他这个typenameclass啦啥的总是有很多人搞不清,上次用那个RapidXML,多加了几个typename

  4. 类型推导
    现在auto可以从braced-init-list推导了,即我们可以按照下面的写法

    1
    2
    3
    4
    5
    auto x1 = { 1, 2 }; // decltype(x1) is std::initializer_list<int>
    auto x2 = { 1, 2.0 }; // error: cannot deduce element type
    auto x3{ 1, 2 }; // error: not a single element
    auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
    auto x5{ 3 }; // decltype(x5) is int
  5. namespace可以用::连接了

  6. Allowing attributes for namespaces and enumerators

  7. 新的attributes
    [[fallthrough]]
    [[maybe_unused]]
    [[nodiscard]]。一个 Discarded value expression表示我们执行它只是为了它的副作用而不是结果。如果我们不使用返回的结果,或者不static_cast<void>(f)(称为 cast to void)时,会产生一个警告

  8. 提供了u8来表示UTF-8编码的char,这是出于兼容性和擦屁股考虑的,因为仍然只有一个字节,所以只能放ASCII

  9. 二进制的浮点表示。。。

  10. 允许所有非类型模板参数的常量计算(?)

  11. Fold expr折叠表达式。。。让你的C++代码越发令人望而生畏

  12. 常量表达式if constexpr

  13. 结构绑定(structured binding)
    这个是非常有用的特性了,可以用来做pattern matching。可以用它实现聚合类的反射。

  14. ifswitch里面也可以定义变量了,这个特性也不错

  15. copy-initialization and direct-initialization of objects of type T from prvalue expressions of type T (ignoring top-level cv-qualifiers) shall result in no copy or move constructors from the prvalue expression. 这一段话总而言之就是guaranteed copy elision的一些规则,可参考我的问题

  16. Some extensions on over-aligned memory allocation

  17. 构造函数可以模板推导了
    下面的代码在C++17是可行的。这个之前在VS2015上写CFortranTranslator的时候就想有,上SoF查了一下发现原来真有,可惜是C++17,所以只能函数模板封装一层,做成make_系函数,估计C++20之后标准库里面这些make_函数都要deprecate吧。

    1
    2
    3
    4
    // C++17
    std::pair(5.0, false);
    // C++14
    std::pair<double,bool>(5.0, false);
  18. Inline variable
    在我的博文中指出了这个特性的作用,以及没有这个特性之前的丑陋的Workaround

  19. __has_include

  20. 标准库新增一些库包括std::string_viewstd::optionalstd::anystd::variant(涉及到合并了一些TS的特性)
    这四个库是很有用的

  21. std::byte

  22. int std::uncaught_exceptions() noexcept替换了bool std::uncaught_exception() noexcept,所以我们不只学会了英语的复数

  23. 容器
    map系容器添加了两个方法:extract 和 merge。
    extract 通常被用来修改一个 map 的 key,但避免 reallocation

    1
    2
    3
    4
    5
    std::map<int, std::string> m{{1, "mango"}, {2, "papaya"}, {3, "guava"}};
    auto nh = m.extract(2);
    nh.key() = 4;
    m.insert(std::move(nh));
    // m == {{1, "mango"}, {3, "guava"}, {4, "papaya"}}

    merge 用来将接受的参数中的 key 插入到自己之中(忽略重复的 key)。
    增加了统一的std::sizestd::emptystd::data访问容器

  24. Definition of “contiguous iterators”
    一个 LegacyContiguousIterator 表示逻辑相邻的元素在物理上也是相邻的。

  25. 基于Boost的一个文件系统库

  26. STL算法的并行版本

  27. 新的数学函数
    现在有椭圆积分和贝塞尔曲线了

  28. 逻辑运算std::conjunctionstd::disjunctionstd::negation

  29. std::scoped_lock
    可以看做是 lock_guard 的超集。相比 lock_guard 能接受0/1个 mutex,scoped_lock 可以接受1-n个 mutex。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    std::scoped_lock lock(e1.m, e2.m);

    // Equivalent code 1 (using std::lock and std::lock_guard)
    // std::lock(e1.m, e2.m);
    // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
    // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);

    // Equivalent code 2 (if unique_locks are needed, e.g. for condition variables)
    // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
    // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
    // std::lock(lk1, lk2);

C++20

限制类型特性

concepts前

在concepts前,C++限制类型特性常可以通过static_assert对应traits或直接上SFINAE解决,这里还列出一些特殊的情形。

  1. 创建只能在栈上的对象
    在对象内重载void * operator new (size_t)
  2. 创建只能在堆上的对象
    禁用析构函数
  3. 在栈上new对象
    使用placement new
  4. 不借助final关键字创建final对象
    实际上就是让我们不能定义出一个派生对象,我们知道将构造函数设为私有之后这个这个类就不能实例化了,不过这个就像化疗一样,虽然派生类不能实例化了,但是自己也不能实例化。
  5. 将函数的返回值加入重载决议
    注意返回值不是函数签名的一部分(所以函数重载决议也是不包括返回值的),不被推导。如果希望实现将返回值也加入重载决议类似的效果,可以借助于类型转换操作符operator T::U()实现

concepts后

concepts后的C++发生了翻天覆地的变化,翻身码农把歌唱,过去的地主富农们装逼的套路又少了很多。可惜码农们南望王师又一年,这concepts是迟迟不来啊。

反射

我一直认为C++、反射和优雅之间只能同时存在两个。在C++17标准前,C++实现反射主要有以下的几种思路:

  1. 手动注册
    rttr是一个较为成熟的库,它并不丑,但是免不了需要在程序运行之前手动执行RTTR_REGISTRATION一下。
  2. 基于编译中间结果

内存管理

C++在内存管理方面常出现的问题包括如下的方面:

  1. 缓冲区溢出
    由于现行冯洛伊曼架构,这个是老生常谈的话题了,由此还派生出专门的栈/溢出攻击。在C++层面,我们需要审慎使用sprintf或者strcpy等函数,或者使用能显式指定长度的_s系函数。在系统层面,有一些常见的方法,例如金丝雀值。
  2. 内存泄露
    假设我们的程序是一个批处理程序,那么内存泄露其实影响有限,如果我们能够确保我们的代码逻辑是没有问题的话。
  3. double free
    double free一个悬空指针产生的问题相对内存泄露的后果要严重多,因为它会导致SegmentFault或者Double free or corruption。但一般来说这样的RE甚至是好事情,double free问题通常意味着代码存在严重的逻辑错误,这时候RE至少能dump,总比WA之后穷查log要好吧。
  4. 非法访问
    非法访问包括访问越界地址和访问未初始化(完毕)或者被销毁的的变量。例如我们删除了某个指针,但没有置指针值为nullptr,那么现在这个指针就称为悬空指针,对它进行访问会造成Access Violation等错误。
    C++中对此还有一些更为隐晦的规则,例如C++中在构造函数和析构函数中不能访问虚函数。或者有时候大家会情不自禁用一个被删除的对象来进行复制构造,抑或返回一个自动变量的指针等。
  5. new/delete不配对
    有关new/delete的问题可以查看我的文章C++初始化方式
  6. 内存碎片
    这个是一个复杂的问题。

右值

有关右值可以查看我的文章C++右值

Reference

  1. https://en.cppreference.com/w/cpp/11
  2. https://en.cppreference.com/w/cpp/14
  3. https://en.cppreference.com/w/cpp/17