读《深入理解C++11》

2019-08-08
7 min read

《深入理解C++11》

Tips

  1. constexpr && const

    constexpr 关键字的引入是为了解决 const 对 ROM 支持的不友好。const 变量只是不可修改,并不是编译时常量,无法应用于 ROM 等小型设备

  2. lambda && functor

    lambda 在内部被实现为仿函数

  3. char && wchar_t

    C++11 后 char 与 wchar_t 进行连接时编译器将自动转化为 wchar_t

  4. long long && 123LL

  5. C++11 规定类的析构函数默认为 noexcept(true)

  6. C++11 的就地初始化比初始化列表先执行

  7. C++11: sizeof(C::mem);C++98:sizeof((C*)(0)->mem)

  8. C++11: friend T ;C++98:friend class T

  9. C++11:typeid(type).hash_code()

  10. C++11:常量表达式只能有一行 return

  11. nullptr && nullptr_t

  12. C++11 提供 unicode 支持

  13. C++11 SFINAE

    表达式中没有出现“ 外部于表达式本身” 的元素,比如说发生一些模板的实例化,或者隐式地产生一些 拷贝构造函数的话,这样的模板推导都不会产生 SFINAE 失败

  14. extern template void fun<int>(int) ,少用

  15. 内联名字空间 inline namespace

  16. 构造函数的默认参数不会被继承。继承来的构造函数是隐式声明的

  17. 构造函数不能同时使用委派和初始化列表;委派构造函数可以捕获异常

  18. auto && cv-qualifer

    声明为引用类型 auto 会保持原有变量的属性

  19. 三元运算符的性能缺陷:#define Max(a,b) ((a)>(b)):(a):(b)

  20. 纯右值 && 将亡值

    将亡值是被强制转化为右值的返回值

  21. 万能的 cont T&

    C++ 98 开始就允许 const T& 绑定到右值,并可以延长右值的生命周期与引用值相同。这种优化减少了拷贝与析构的损耗。常量左值引用是“万能”的,如果重载没有实现右值引用的接口,则编译器自动调用常量左值引用接口

  22. 构造时为成员变量手动调用 move

    编写右值接口时要显式的调用 move 函数将成员变量转化为右值以触发成员变量的移动语意

  23. 非受限联合体 / 类内匿名联合体 / placement new

    任何非引用类型都可以成为联合体的数据成员,这样的联合体即所谓的非受限联合体( Unrestricted Union)

    非受限联合体默认会删除非 POD 变量的构造函数,所以需要手动调用 placement new 和对应的析构

  24. Big5 的自动生成机制

    以在 C++11 中,拷贝构造/赋值和移动构造/赋值函数必须同时提供,或者同时不提供,程序员才能保证类同时具有拷贝和移动语义。只声明其中一种的话,类都仅能实现一种语义

新概念

forward

所谓完美转发(perfectforwarding),是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数

完美转发的引入是为了解决右值属性传递的一些问题,按照右值的一般理解:没有名字不能取地址的值被称为右值,但右值参数在函数内是左值。比如函数 f(C &&c)c 在函数f 内部是当作左值看待的。如果在函数 f 内调用了另一个函数 g(C &&c)/g(const C &c),那么编译器默认会选择 g(const C &c) ,示例如下:

#include <utility>
#include <iostream>

class A{ };
void tt(A &&a)      { std::cout << "const A &&a" << std::endl; }
void tt(const A &a) { std::cout << "const A &a"  << std::endl; }

void bb(A &&a) { tt(a); } 
// void cc(A &&a) { tt(std::forward(a)); } // invalid usage of forward
template<typename T> 
void dd(T &&a) { tt(std::forward<T>(a)); } // 只有左值引用 forward 不需要模板参数,如下文定义

int main() {
  tt(A()); // const A &&a
  bb(A()); // const A &a
  dd(A()); // const A &&a

  A a;
  tt(a); // const A &a
  // bb(a); // invalid call
  dd(a); // const A &a
}

C++11 中 forward 的实现方式如下(注意第二种实现方法必须提供模板参数):

template <class T>
inline T&& forward(typename std::remove_reference<T>::type& t) noexcept {
    return static_cast<T&&>(t);
}

template <class T>
inline T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
    static_assert(
          !std::is_lvalue_reference<T>::value, "Can not forward an rvalue as an lvalue.");
    return static_cast<T&&>(t);
}

为实现上述功能,C++ 11 在模板中引入了引用折叠的概念:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

initializer_list

#include <iostream>
#include <vector>
using namespace std;
class Mydata
{
public:
    Mydata &operator[](initializer_list<int> l) {
        for (auto i = l.begin(); i != l.end(); ++i) idx.push_back(*i);
        return *this;
    }
    Mydata &operator=(int v) {
        if (idx.empty() != true) {
            for (auto i = idx.begin(); i != idx.end(); ++i) {
                d.resize((*i > d.size()) ? *i : d.size());
                d[*i - 1] = v;
            }
            idx.clear();
        }
        return *this;
    }
    void Print() {
        for (auto i = d.begin(); i != d.end(); ++i)
            cout << *i << " ";
        cout << endl;
    }

private:
    vector<int> idx; // 辅助数组, 用于记录 index
    vector<int> d;
};
int main() {
    Mydata d;
    d[{2, 3, 5}] = 7;
    d[{1, 4, 5, 8}] = 4;
    d.Print(); // 4 7 7 4 4 0 0 4
}

自定义字面值

#include <iostream>

struct Watt{ unsigned int v; }; 
Watt operator "" _w( unsigned long long v) { 
    return {(unsigned int) v}; 
} 

int main() { 
    Watt capacity = 1024_w; 
}

decltype

编译时行为

decltype 的使用比较繁琐,非库代码一般较少使用

usingsize_t    = decltype(sizeof(0));
usingptrdiff_t = decltype((int*)0(int*)0);
usingnullptr_t = decltype(nullptr);

std::result_of

template <class> struct result_of;

template <class F, class... ArgTypes>
struct result_of<F(ArgTypes...)> {
  typedef decltype(std::declval<F>()(std::declval<ArgTypes>()...)) type;
};

返回值推导

template<typename T, typename U>
auto sum(T t, U u) -> decltype(t+u) { return t+u; }

变长模板

template <typename... Elements> class tuple; // 变长模板的声明 
template< typename Head, typename... Tail>   // 递归的偏特化定义 
class tuple<Head, Tail...> : private tuple< Tail...>  { Head head; }; 
template<> class tuple<> {}; // 边界条件

原子变量与内存序

default && delete && explicit

default 和 POD 类型息息相关

default 和默认 Big5 的自动生成规则息息相关

不要同时使用 explicit && default 或者 explicit && delete

class ConvType { 
    public: ConvType( int i) {}; 
    ConvType( char c) = delete; // 删除 char 版本 
};

alignas/alignof

因为硬件等原因 ,比如cache line (64B),总线宽度(64bit) 等,数据是否对齐会影响系统的性能,某些指令场景下未对齐的数据会造成系统的崩溃

以 64位 CPU 为例,cpu 以 64bit 为单位(8字节)从内存中读取数据,如果 int 类型的变量对齐到内存 1 字节地址边界,那么 cpu 在读取这个变量时很可能需要读两次,每次读 int 的一部分;如果 int 对齐到 4 字节边界,那么 CPU 一次肯定可以获取整个 int 变量(可以用反正法证明)

下面代码片段展示了 alignas/alignof 的使用方法。x64 cpu 支持矢量指令,一次可以操作多个变量,默认情况下以 8 字节为边界进行对齐,这不利于 CPU 取值(32 字节是扩展对齐方式,标准对这类行为没有定义)

#include <iostream> 
using namespace std; 
struct ColorVector { 
    double r, g, b, a; 
}; 
// 非标准对齐,使用时要注意
struct alignas(32) AlgColorVector { 
    double r, g, b, a; 
}; 
int main() { 
    // alignof(ColorVector): 8
	// alignof(AlgColorVector): 32
    // alignof(std::max_align_t): 16
    cout << "alignof(ColorVector): "      << alignof(ColorVector)    << endl;
    cout << "alignof(AlgColorVector): "   << alignof(AlgColorVector) << endl;
    cout << "alignof(std::max_align_t): " << alignof(max_align_t)    << endl;
    return 0; 
} 

std::max_align_t

系统支持的对齐数值是有限的,可以通过 alignof(std::max_align_t) 来判断对齐上限,通常是最大标量(long double)长度

使用超过默认对齐字节的对齐长度,标准不保证所有编译器正常

align/aligned_storage/aligned_union

// aligned_storage example
#include <iostream>
#include <type_traits>

struct A {  // non-POD type
  int avg;
  A (int a, int b) : avg((a+b)/2) {}
};
typedef std::aligned_storage<sizeof(A),alignof(A)>::type A_pod;

int main() {
  A_pod a,b;
  new (&a) A (10,20);
  b=a;
  std::cout << reinterpret_cast<A&>(b).avg << std::endl;

  return 0;
}

通用属性

如下程序片段,程序中 const 属性告诉编译器,该函数返回值只依赖于输入,不会改变函数外的数据,因此编译器可以对 area(3) 进行优化处理,只对函数调用一次,后续将 area(3) 视为常量进行操作,将大大提升程序性能

extern int area(int n) __attribute__ ((const))

int main()
{
    int areas=0;
    for(int i=0;i<10;++i)
    {
        areas+=area(3)+i;
    }
}

自C++11开始,C++ 拥有统一形式的通用属性申明方式,语法格式如下:

[[ carries_dependency ]] func();

C++ 11/14/17 以后常用属性有:

[[noreturn]]、[[carries_dependency]]、[[deprecated]]、[[deprecated(“reason”)]]

POD

参考1:https://mariusbancila.ro/blog/2020/08/10/no-more-plain-old-data/

参考2:https://docs.microsoft.com/en-us/cpp/cpp/trivial-standard-layout-and-pod-types?view=msvc-160

参考3:https://en.cppreference.com/w/cpp/language/initialization

A POD type is a type that is both trivial and standard-layout. This definition must hold recursively for all its non-static data members.

POD 在 C++ 20 以后被 trivial 和 standard layout 这两个概念替换。简单一些可以认为 POD 类型是 C++ 中与 C 兼容的内存布局,没有 C 不理解的内存成分且拷贝没副作用(比如引用增加等)。以C++中的继承为例,因为 C 中没有继承等概念,所以 C++ 中有继承与多态而与 C 不兼容的变量都不是 POD,某些 OB 对象是 POD 但 OO 对象一定不是POD。可以通过 std::is_pod 判断是否 POD: std::is_pod = std::is_trivaial + std::is_standard_layoutis_pod 慢慢被弃用,应该使用后两个方法

  1. trival ,std::is_trivial && =default

    没有自定义的 BIG5 和析构函数,且没有虚函数和虚基类

    可以从一个例子来说明 trivial 的意义:对象的内存可以通过 malloc 创建然后通过 free 删除

    default 关键字的存在可以将自定义了构造函数的类恢复为 POD。一般定义了自己的构造函数后默认的构造函数就不再生成,但使用 default 可以强制编译器生成默认构造等方法

  2. standard layout,std::is_standard_layout ,标准布局的对象内存是可预测的。不同编译器对 C++ 继承对象的实现方式不同,所以包含继承关系的对象一般不是标准布局,因为其内存布局强依赖编译器

POD 的优势

  1. 字节赋值,比如直接使用底层的 memory copy 进行拷贝
  2. 提供与 C 兼容的内存布局
  3. 保证静态初始化的安全有效