C++ 标准笔记

本文始于 2018/6/27,用于汇聚关于 C++ 的实用知识和碰到的坑,温故而知新。

2022/1/2 【更改】标题改为《C++ 标准笔记》
2022/1/2 【新增】StandardLayout
2022/1/2 【新增】bit field

更新预告:

  1. CRTP 原理,目的和应用
  2. CRTP 应用之表达式模板

union 共用体

  • union可匿名,常在结构定义中。
  • 用于节省内存,尤其在嵌入式系统中。
  • union的定义形式与struct相同。

enum 枚举

  • enum A {a,b,c}; 首项默认为0,默认后项比前项大一。
    A被看做一种类型,甚至可以省略;a,c,b被看做常量。
    a,b,c可以自动提升为int, 但int不能自动转换为枚举类型,除非强制转换A(1)。
    每个enum根据其常量的最大最小值确定其上下限。
  • enum创建的常量是静态的, 可以用作静态类成员常量. 运行时所有对象不会包含枚举
  • 更强的安全性--类作用域内枚举(C++11)
    • enum class name : type {...};
    • enum struct name {...};
    • class或struct二选一, :type可选.
    • 作用域内枚举不允许隐式地转换为整型
    • 默认底层为int
    • 调用格式为name::x
  • :type 放在枚举名后以指定底层, 否则将随实现而异.

char 字符型

  • char类型被输入输出流区别对待。cout << (int*) st << endl;
  • char类型数组的初始化被C++区别对待:
    1
    2
    3
    char s1[5] = "abcd"; 合法,"abcd"被转换为{'a','b','c','d'}, 可供修改
    char s2[5] = s1; 错误,即使s1是const
    char* s3 = "abcd"; 警告, "abcd"是常量,不能修改. 参数char s3[]="abcd"亦等价警告.
  • const char []所定义的字符串被保护,有内存位置隔离,不允许跟踪(但允许查看)地址。
  • char是否带符号取决于系统。
  • wchar_t可以表示系统使用的最大扩展字符集,输入输出用wcin,wcout
  • raw r"(a/b\c回车defg*)", 标识符 "( )" 可变为 "*@( )*@" 等等,保持平移对称

<cstring>常用函数

  • char* strcpy(char* dest, const char* src)
    • 以src覆盖dest, 直到遇到src的终止符.返回dest.
  • char* strncpy(char* dest, const char* src, size_t count)
    • 以src的前n位覆盖dest, 若遇到src的终止符则用0填充剩余位.返回dest.
  • char* strcat(char *dest, char *src)
    • 将src复制到dest的末尾.返回dest.
  • int strcmp(char *str1, char *str2)
    • 按字典序比较, 返回-1,0,1.
  • int stricmp(char *str1, char *str2)
    • 按字典序但对大小写不敏感比较.
  • int strncmp(char *str1, char *str2, size_t count)
    • 按字典序比较前n位
  • int strnicmp(char *str1, char *str2, size_t count)
    • 按字典序但对大小写不敏感比较前n位

vector与array 模板类

  • vector<typename> arr(n_elem); 也可以不指定长度
  • array<typename, n_elem> arr; 定长数组。 等长数组可以直接复制。
  • 下标可越界,欲防止越界用 arr.at(x)

读入行

  • cin.getline(arr, arsize) 空行不设置failbit
  • cin.get(arr, arsize) 行读取结束后不丢弃换行符(无法跨越)
  • cin.get() 读取一个任意字符,返回char值
  • cin.get(char)返回cin
  • string类读行 getline(cin, str);

fstream 文件流(以fin, fout为例)

  • 打开文件: .open("filename");
  • 关闭文件: .close();
  • 检测最后一次读入遇到EOF: fin.eof();
  • 检测最后一次读入遇到类型不匹配(包括EOF): fin.fail();
  • 检测最后一次读入文件损坏、故障: fin.bad();
  • 检测最后一次读入完全正常: fin.good(); 等价于 (bool) fin >> value;
  • 清空错误标记,准许读入: .clear();

switch 结构控制

1
2
3
4
5
6
7
8
9
10
switch(num)
{
case constValue1 : statement1
break;(optional)

case constValue2 : statement2
break;(optional)

default : statement3
}

杂项

  • exit(EXIT_FAILURE); : cstdlib, 同操作系统通信的参数值
  • nullptr : C++11 关键字, 空指针值.

指针

  • 应当将空指针写为nullptr. deletedelete [] 都可以作用于空指针.
  • const 指针 将受保护,非const 指针 不能复制其值(除非利用强制类型转换),不允许通过const 指针 修改所指向值。
    为了防止欺骗const检查,不允许令const 二级指针指向非const 一级指针

函数指针

  • 声明:typename (*pointer name)(parameter list...);
  • 获得某函数的地址:pointer = function name;
  • 使用函数指针:(*pointer)(parameter list...);pointer(parameter list...);
  • 举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    原函数:

    const double* f1(const double [], int);

    指向f1的指针:

    const double* (*p1)(const double [], int) = f1; 或 auto p1 = f1;

    由f1,f2,f3组成数组://[]优先级高于*

    const double* (*ar[3])(const double [], int) = {f1, f2, f3}; 不允许auto

    指向ar[3]的指针:

    const double* (*(*arp)[3])(const double[], int) = &ar; 或 auto arp = &ar;

    还可以用typedef简化声明:

    typedef const double* (*p_fun)const(const double [], int);

    p_fun ar[3] = {f1, f2, f3};
  • 分析方法: 从变量名开始, 往右往左结合, 逐步解释结合体.
    • 当遇到一个指针, 总是关心它指向什么类型
    • 当遇到一个数组, 总是关心它的元素的类型
    • 当遇到一个函数, 总是关心它的返回值类型

参数传递

  • 参量(argument)是实参,参数(parameter)是形参。

函数重载(函数多态)

  • 同名函数共存要求:函数特征标(函数参数列表)不同。
  • 当需要自动类型转换但选项不唯一时,编译不通过。
  • 不区分const和非const变量(包括指针)。
  • 区分指向const的指针和指向非const的指针。
  • 区分指向const的引用和指向非const的引用。(编译器调用最匹配的版本)
  • 如果没有右值引用参数版本,右值参量将赋给const引用参量。

引用

  • 左值引用
    • 非const引用可以引自可修改的左值(防止修改const值和修改临时变量)
    • const引用可以引自任何左值、右值(毕竟数值不会变)
    • 基类引用可以指向派生类对象
  • 指针也能被引用, 例如 int*& x = ptr;

短路运算符

  • ||, &&具有短路作用,结果必然时短路,不计算右边的表达式

inline

  • 在声明时用或在第一次调用前的定义用都行
  • 不能处理递归, 编译器有权拒绝采用.
  • 函数默认值必须放在原型声明中, 不能放在和声明分离的定义中.只能从右到左地提供默认值

decltype(expression)

  • decltype(expression)核对表

    1. 如果expression是一个不带括号的标识符, 则返回相同类型, 包括const,*等限定符
    2. 否则,如果expression是一个函数调用(需要提供参数),则返回相同类型
    3. 否则,如果expression是一个带括号的左值,则返回其引用
    4. 否则,返回expression的类型(例:右值)
  • 可以利用typedef decltype(expression) somename;简化声明

  • decltype 用于函数返回类型声明

    • 声明返回类型时未知参数列表, 所以需要后置声明
      1
      2
      template <typename T1, class T2>
      auto gt(T1 x, T2 y) -> decltype(x + y) // "-> 已知类型" 也行

模板

  • 创建模板

    1
    2
    template <typename T>    //也可以用class代替typename
    void swap(T&, T&);
  • 显式具体化

    1
    2
    template <> void swap(int&, int&);
    template <> void swap<int>(int&, int&);

    为什么需要显示具体化?常规函数不能替代吗?

  • 显式实例化

    • 不使用函数就能依照模板生成实例, 常用在库中
    • template void swap<int>(int&, int&);
    • 使用函数时add<double>((int)a, (double)b); 会强制使用模板.注意引用类型不能隐式类型转换.后续的强制使用仍需加<double>

::变量名

  • 表明采用全局变量而非局部变量

储存空间

  1. 静态储存: 保证初始化为0, 生命周期和程序相同.
  2. 自动储存(栈内存): 不保证初始化;
  3. 动态储存(堆内存): 不保证初始化;

储存说明符

  • static用于变量 编译阶段尽量初始化, 运行时直接分配空间,程序结束时销毁, 首次遇到时再保证初始化
    • 静态外部链接性变量: 直接在非被包括区域定义, 在其他单元中可用extern引用声明(不允许再次初始化)(ODR单定义规则)
    • 静态内部链接性变量: 在非被包括区域加static, 限定在本单元内可以访问
    • 静态无链接性变量:在被包括区域加static, 限定在本包括区域内可以访问
  • static用于函数 覆盖函数的默认外部链接性为内部链接性,必须同时用于原型和定义中.
  • thread_local 变量持续性与所属线程的持续性相同, 可与static, extern结合使用(其他声明中不能使用多个储存说明符)
  • mutable 使得const对象中的属性能被修改,而不受const限制
  • register C++11之前是寄存器变量,不能取地址; 之后是显式指明自动变量(无实际作用,避免旧代码非法)

显式指出语言链接性以帮助链接程序寻找匹配函数:

extern "C" void spiff(int);

cv-限定符

- const全局变量带有隐式static使得变量链接性为内部, 导致多文件同时include一个头文件时不会发生定义冲突 *可以使用extern覆盖隐式static使变量链接性为外部. 在其他单元中仍需用extern来引用它.*
- volatile 提示编译器该变量会在程序之外被修改, 不要进行寄存器缓存优化. 多见于驱动程序.

new运算符

  • 原函数:
    1
    2
    3
    4
    void * operator new(std::size_t);
    void * operator new[] (std::size_T); // new int[40] --> new(40*sizeof(int))
    void operator delete(void *);
    void operator delete[] (void *);
  • new, delete函数可以替换.
  • new在堆中查找满足要求的内存块
  • 定位new运算符
    • typename* p = new (MemAddress) typename
    • 返回(void *)指定的内存地址
    • 允许重载
    • 注意非堆内存不可delete
    • 若内存中存放了对象,则需要手动调用析构函数,再由MemAddress释放内存,且需注意new []与delete []对应
  • 元素的创建与销毁应遵循FILO顺序

名称空间

  • 声明区域: 可以声明变量的区域, 例如所在的文件, 代码块.
  • 潜在作用域: 从声明点开始到声明区域的结尾. 可能被局部变量隐藏,故称潜在.
  • 自定义名称空间:namespace Somewhat {...}
    • 只能在函数外部定义, 允许在另一个名称空间中嵌套
    • 因此, 默认情况下声明的名称的链接性为外部(除非它引用了常量)
    • 可以在其他合法位置继续添加名称, 提供定义.
  • using namespace Somewhat;编译指令
    • 每个声明区域中都有一条隐式的using namespace 全局名称空间;
    • 若某处使用过using namespace编译指令,则其子声明区域也隐式添加这条语句.
    • 局部变量拥有最高优先权,能隐藏各种using namespace同名变量(因为名称空间都在函数外部定义)
  • using声明
    • 类似于声明了一个局部变量, 在代码块中不允许同级同名.
    • 因此使用using声明比使用using编译指令更安全.
  • 名称空间可以拥有别名, 用于简化代码: namespace MEF = myth::elements::fire;
  • 名称空间可以匿名, 声明之后自动隐式using, 用于避免其他using并替代static全局变量.
  • <.h>文件是不支持名称空间的版本.新版一般将函数搬到std中.
  • 使用建议: 在大型程序/多单元程序使用
    • 少用using namespace
    • 避免在头文件中使用using编译指令,若必须使用,则在所有#include之后使用
    • 避免直接声明外部全局变量和静态全局变量,改用已命名的名称空间
    • using声明首选用在局部而不是全局

  • 构造函数
    • 调用示例:
      1
      2
      3
      4
      5
      6
      7
      8
      Classname object; // 调用空参数构造函数
      Classname object(); // 警告, 正在声明函数
      Classname object(one, two, ...); // 调用对应的构造函数
      Classname object = Classname(one, ...); // 有可能先构造临时对象
      Classname* p = new Classname(...);
      Classname object{...}; Classname object = Classname{...}; // C++11
      Classname* p = new Classname{}; // C++11
      Classname object = value; // 调用一个参数的构造函数, 可用explicit(修饰构造函数)关闭这种特性
    • 重载构造函数中使用的new或new []必须对应,因为析构函数只有一个。
    • 初始化列表
      • 参数列表后由冒号引出的初始化信息。在此初始化对象将使用复制构造函数,而不是空构造函数加赋值运算符,因此效率更高。
      • 初始化的顺序按照成员变量的声明顺序,而与初始化列表顺序无关。
      • 一旦进入花括号,成员变量将完成默认的初始化,对象初步创建完成。因此,成员变量中,非静态常量与引用必须由此列表进行初始化。
      • 类内初始化可等价于默认的列表初始化。列表初始化会覆盖类内初始化。
      • 初始化列表负责调用基类构造函数(基类已在派生类类域中,无需加作用域解析符)
      • 初始化列表负责调用成员对象的非默认构造函数
    • 默认不能继承构造函数(C++11)
  • 析构函数
    • 只能有一个析构函数, 且参数必须为空
    • 注意用delete对应构造函数或其他过程的new
    • 若对象由定位new运算符创建,则需要手动调用析构函数,且遵循FILO顺序。
    • 必须以delete或delete []对应构造函数中的new或new []。
    • 最后会自动调用基类的析构函数
    • 应当将析构函数声明为虚函数
    • 不能继承析构函数
  • 封装是一个编译期的概念
    • 编译期不存在实例,编译器仅针对类型做检查
    • 可以在类方法中访问同类对象的私有成员
  • 同struct : 避免环形构造
    • 编译器禁用简单环形定义, 如 struct A { A a; }; // 使用了未完成的定义
    • 编译器不能辨别复杂环形定义, 如struct A { A(){} A* a = new A(); }
  • 非静态变量在运行时才会创建, 所以如int arr[MAXN]的MAXN必须是静态量,可以是全局const, 类中static const, enum{MAXN=x}.
  • 友元函数
    • 在共有部分声明友元函数原型, 也可以紧接定义以设为内联函数.后置的定义无需friend修饰
    • 友元函数视为类外函数, 但可以访问类私有成员变量.
    • 类的显式二元重载运算符应当用友元, 尽管没有直接修改类私有成员变量
    • 重载<< : std::ostream& operator << (const std::ostream& os, const Classname& obj)以方便输出.
    • 友元函数可方便隐式类型转换, 不必苛求由对象发起函数调用.例如cmp("asd", obj),可以对应原型cmp(string, string);
    • 可以在派生类友元函数中,强制向上转型并使用基类友元函
      数据类型, 否则易有二义性
      函数, 避免隐式转换出错
      ass_name&)`
      始化、按值传递、按值返回、上转型并使用基类友元函数。
  • 转换函数operator typeName()
    • 用途:将类转换成基础数据类型
    • 必须是成员方法
    • 不能指定返回类型
    • 不能有参数
    • 用于cout时应显式标明类型
    • 应当用explicit修饰
  • 复制构造函数Class_name(const Class_name&)
    • 在初始化对象时使用(显示的初始化、按值传递、按值返回、编译器生成临时对象)
    • 新版本C++可用移动构造函数
    • 例:
      1
      2
      3
      4
      5
      Class_name a(b);
      Class_name* pa = new Class_name(b);
      Class_name a = b; // 可能生成临时对象后调用赋值运算符函数,根据实现而异.
      Class_name a = Class_name(b); // 同上
      按值传递函数调用func(b); // 按值传递也初始化了参数
    • 默认的复制构造函数,是在初始化列表中调用所有成员的复制构造函数。默认复制派生类对象的基类部分
    • 应注意深度复制, 即妥善处理指针所指向内容的复制
  • 赋值运算符函数
    • 考虑妥善处理原来已有的,即将被抛弃的数据.
    • 考虑自己赋值给自己的情况, 小心赋值前删除了自身数据.
    • 记住返回自身引用, 即 return *this
    • 不能继承赋值运算符函数
    • 不建议设置为虚方法,为了避免同基类的不同派生类互相赋值
  • 默认方法的潜在危险
    • 某个类在开发初期不需要复制构造函数/赋值运算符重载,但在以后可能需要
    • 可能写出符合常理的代码,但由于未覆盖默认方法而运行时异常
    • 可能在未察觉的情况下调用了默认方法(用重载加法时按值传递、按值返回将调用复制构造函数)
    • 解决办法:
      • 不管默认方法是否需要,总是提供正确的代码
      • 将空方法设为private,并留下错误信息
      • 使用delete(C++11)
  • [ ](取位)运算符: 两个操作数分立于左中括号两侧。只能是成员函数。
  • const对象
    • 只能使用const函数, 若返回引用, 则返回类型为const type&
  • 静态类成员函数
    • 原型含static, 定义不含
    • 和Java不同, 不能通过对象来调用静态成员函数
    • 调用格式 Class_name::s_method();
  • 静态类成员变量
    • 在类中声明, 不可定义
    • 在类外定义并分配内存, 可以不初始化
    • 静态常量在声明同时定义.
  • 派生/继承
    • class newClass : public baseClase
    • 默认继承为private继承。私有继承的向上转型必须显式写出
    • 多重继承的向上转型也应显示写出,以防不同基类的方法冲突
    • 派生类中一旦定义某方法与基类方法同名,则基类所有该名方法被隐藏。与参数列表(特征标)无关。
      • 可用基类名::方法名的途径调用隐藏的方法
      • 重新定义继承的方法,应确保与原来的原型完全相同
      • 若返回类型为基类引用或指针,则可以修改为派生类的引用或指针(返回类型协变)
      • 若基类虚方法被重载了,则应在派生类中重新定义所有的基类版本
    • 若派生类构造函数使用了new,则应提供复制构造函数/赋值运算符(含base::operator=(o);)/虚析构函数的定义
    • 公有继承表达了is-a关系,私有继承/包含表达了has-a关系。通常使用组合包含层次化来建立has-a关系。如果需要访问原有类的保护成员或重新定义虚方法,才使用私有继承。
    • 欲调用基类对象,对(*this)强制向上转型(const Base &)即可
    • 用using改变继承成员权限:在派生类的public处using Base::methodName;(省略using的技术是即将被抛弃的旧技术)
  • 虚方法
    • 若希望通过基类引用或指针调用派生类方法,则需要将基类方法声明为virtual虚方法(一般也将派生类方法声明为virtual)
    • 应当将析构函数定义为虚方法,以确保正确地销毁对象
    • 在类中欲调用基类方法(而不是本类方法),需使用作用域解析符Base::baseMethod();
    • 编译器对虚方法的一般实现:把类的所有虚函数的地址制作成表,在对象中添加隐藏的虚函数表指针,在运行时通过指针检索虚函数。
    • 与Java不同,派生类虚方法的访问权限允许变严格,但由基类引用或指针的多态效果仍然生效。
    • 派生类中一旦定义虚方法,就将隐藏基类所有同名方法,故应在派生类中重新定义所有的基类重载方法
    • 可以令虚方法声明结尾处为=0,使方法成为纯虚方法。纯虚方法可以不提供定义。含有至少一个纯虚方法的类为抽象类,不能声明对象。
  • 访问权限
    • private:仅本类和友元函数有权访问。对数据成员的理想访问控制。
    • protected:本类和派生类有权访问。在派生链中,此权限类似public;在类外部,此权限类似peivate。此权限通常只给成员函数。
    • public:在同一域中就能访问。
  • 引用限定成员函数
    • 函数签名的最后可以标注&&&来做「引用限定 ref-qualified」。
      1. 非限定成员函数可由左值调用,也可由右值调用。此时不能再定义引用限定成员函数。
      2. 左值限定成员函数仅可由左值调用。
      3. 右值限定成员函数仅可由右值调用。

Lambda

Lambda是一种匿名函数,它在普通函数的基础上增加了一些功能。Lambda是通过函数对象实现的,具有在编译器内联的能力,因此性能可以比函数指针更好。

使用举例

1
2
3
4
5
6
7
8
9
10
11
12
[](int q, int p) { return q > p; } // 常见的比较函数
[mask](int x) { return mask & x; } // 判断有无交集
[&flag](int x) { flag[x] = true; } // 修改自动变量flag数组
[&](int x)->int { // 按引用捕获当前作用域的全部自动变量。不是简单地return故要声明类型
if (x > mx) mx = x;
return tot += x;
}
[k, &](int del) { // 按值捕获k、按引用捕获其他变量
sth += del;
cout << k << endl; // k是只读的??!!
}


右值

rvalue,(非严格定义: )匿名的临时值,常出现在等号的右方。最大特征是不可取地址,例如

1
2
int a;
a = 2 + 3; // (2+3) 就是一个右值, &(2+3)没有意义

右值引用

有时候运算过程中会生成一些体积大的临时对象,在完成表达式后这些临时对象会析构,例如

1
2
3
4
5
6
7
8
9
struct MyString {
char* data = nullptr;
//... 使用new和delete的模仿string类,具有拷贝构造函数
};

vector<MyString> arr; arr.reverse(10); // 保留10个string的空间
for (int i=0; i<10; ++i) {
arr.push_back(MyString("Hello_rvalue"));
}

第8行发生如下事件:

  1. MyString("Hello_rvalue") 产生一个右值。
  2. push_back接收一个右值参数,并调用MyString的拷贝构造函数新建一个对象。
  3. MyString("Hello_rvalue")生命周期结束,析构。

发现拷贝构造函数存在资源浪费:既然右值的资源已经没有后续价值,大可以将其资源“偷”过来使用。

于是,MyString类新增移动构造函数(还有移动赋值函数

1
2
3
4
5
6
7
8
9
10
11
12
struct MyString {
// ... 同上
MyString(MyString&& x) {
this->data = x->data;
x->data = nullptr; // 资源被偷走
// ...
}

MyString& operator=(MyString&& x) {
// 类似移动构造函数,不赘述
}
};

如此一来,push_back将自动调用移动构造函数来创建对象,避免大量new内存。

右值引用的存在,就是为了尽量榨干临时对象的价值

要利用右值引用,最重要的是合理地定义移动构造和移动赋值

移动语义

有时候一些左值就像右值一样即将消亡,弃之可惜,例如

1
2
3
4
5
for (int i=0; i<10; ++i) {
string tmp("hello");
tmp += " rvalue?"; // tmp经过一系列复杂的处理
arr.push_back(tmp);
}

如果能像右值引用一般把tmp的资源“偷”走就好了,于是

1
2
3
4
5
for (int i=0; i<10; ++i) {
string tmp("hello");
tmp += " rvalue?"; // tmp经过一系列复杂的处理
arr.push_back(std::move(tmp)); // 强制转换为右值
}

注意,move(tmp)后由于资源已经被偷走,tmp可能像野指针一样危险。被move后对象的行为由程序员负责。

完美转发

完美转发是针对c++模板函数的概念。

我们不得不先介绍c++引用折叠概念:

typename T T& T&&
G G& G&&
G& G& G&
G&& G& G&&

模板函数的右值引用具有欺骗性!在模板实例化时,T& &&T&& &都会被转换为T&。只有T&& &&会成为T&&。利用这一点,我们可只定义T&&的函数行为。

(对于简单的情况,T && 等价于 T&&

但此时也出现了另一个问题:

1
2
3
4
5
6
7
8
9
template <typename T>
void f1(T&& x1) {
f2(x1);
}

template <typename T>
void f2(T&& x2) {
// do something
}

我们希望,若f1接收左值,那么f2也应该接收左值;如果f1接收右值,那么f2也应该接收右值。但c++规定,“右值引用”是一个左值,因为它有名字,还可以取地址。在这里,f2无论如何都会接收左值。怎样才能写出自动接收左右值的模板函数呢?

利用任意表达式都能生成匿名值,最简单的,就像static_cast<T&&>(x1),此时表达式的结果可以转化为右值。再加上引用折叠的特性,就是std::forward的基本原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// T可以是scalar,object,lref。 但不可能是rref!
template <typename T>
void f1(T&& x1) {
// 实现了左值转发为左值,右值转发为右值。
f2(std::forward<T>(x1)); // 不能依赖C++17的自动推导,因为x1总是lref!
}

// 附:std的实现
// FUNCTION TEMPLATE forward
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(
remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg); // 注意这里可能发生引用折叠
}

template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
return static_cast<_Ty&&>(_Arg);
}

// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg); // 注意这里不可能发生引用折叠
}

异常

  • 捕获异常时,从具体到抽象,最后到...
  • 如果异常携带字符串信息,则可能引发std::bad_alloc,惊不惊喜!
  • std定义的异常分类:#include <stdexcept>
    1. logic_error 逻辑出错。
    2. invalid_argument 无效参数。
    3. domain_error 值域错误。(继承自logic_error
    4. length_error 试图超越最大体积。(继承自logic_error
    5. out_of_range 试图越界。(继承自logic_error
    6. runtime_error 仅在运行时可知的错误。
    7. range_error 计算结果无法用目标类型表示。(继承自runtime_error
    8. overflow_error 算术上溢。(继承自runtime_error
    9. underflow_error 算术下溢。(继承自runtime_error
    10. tx_exception 可用于回滚或取消transaction的异常。(Transactional Memory Technical Specification, TM TS)

constexpr if

使用 if constexpr,将在编译期计算分支选择

常常配合 <type_traits> 或变参模板使用。

  1. 条件必须能在编译期计算,并转换为 bool
  2. 被抛弃的分支:
    1. 不参与函数返回值类型推导
    2. 可以使用有声明但没有定义的变量(不属于 ORD-use)
  3. 在一个模板实体中,被抛弃的分支不引起后续的实例化。常用于 if constexpr (sizeof...(rs) > 0) { f(rs...) }
  4. 在一个非模板实体中,被抛弃的分支依然参与语义分析(例如你不能使用一个未声明的变量)。所以 if constexpr 不能替代 #if
  5. 任何时候,被抛弃的分支都不允许 ill-formed(例如 static_assert(false,""))。你可能需要 Concept。

StandardLayout

StandardLayout 的前身是 POD。为了内存对齐,各路厂商的编译器会对结构体的数据成员进行偏移量编排。如果需要字节级粒度的操作(常见于网络),我们需要弄清楚结构体内部的具体布局。

结构体布局

C++标准 对结构体布局仅做了很有限的规定:

  1. 同一访问权限的字段的相对顺序与定义相对顺序相同:先定义的在低地址,后定义的在高地址。
  2. 不同访问权限的字段的相对顺序是未指定的。
  3. 为了内存对齐,允许编译器在字段之间、结构体的末尾添加空字节。

标准局部(原 POD)

  • C++11 以前,PODType 等价于 StandardLayoutType
  • C++11 开始,StandardLayoutType 要求类满足以下条件:
    1. 所有非静态数据成员具有同一个访问权限。
    2. 没有虚函数,没有虚基类。
    3. 任何非静态数据成员都不是引用类型。
    4. 所有非静态数据成员和基类都是 standard layout。
    5. 不存在菱形继承。
    6. bit-fields(位域)和所有非静态数据成员在同一个类内定义。
    7. 其他复杂的条件(参考 cppref)

StandardLayout(标准布局) 是 C++ 标准提出的概念,但没做具体布局。

常见的实现遵循以下原则,可以帮助我们肉眼判断 size 和 alignment:

  1. 结构体自身对齐到最宽的基础类型数据成员。(嵌套 struct 的情况下要展开判断)
  2. 结构体内基础类型字段对齐到自身大小,子 struct 则按照上一条对齐。(在字段之间 padding)
  3. 结构体的 size 必须是对齐字节的整数倍。(在结构体末尾 padding)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
size = 8
alignment = 4
|a___|bbbb
*/
struct A {
char a;
int b;
};

/*
size = 16
alignment = 4
|c___|a___|bbbb|d___
*/
struct B {
char c;
A obj; // obj 要对齐到 4 字节,char c 和 char a 不能紧贴相邻
char d;
}

// 判断是否标准布局
#include <type_traits>
std::is_standard_layout<B>::value;

bit field

bit field(位域)是一种语法糖。它用于极致紧凑地在内存中排列结构体内容,但是需要额外位操作指令。

这里介绍位域的实现原理。

  • C++ 标准没有规定位域应该如何实现,所以,使用位域的 C++ 程序本质上是不可移植的。
  • 要考虑字节内外部的大小端问题。通常内外部的大小端保持一致,如 x86 就是不同字节小端在前(低地址),同一字节内小端在前(低编号)。
  • 编译器会额外添加位操作指令(如 shl, shr, sar, or 等)来保证读写位域的语义正确性。
  • 位域的类型决定了对齐边界。例如 int x : 17 会保证 x 不会越过 4 字节的对齐边界。
  • 相邻的位域,如果对齐长度相同,编译器会尽量将他们装在一起,如果装得下的话。
  • 可以使用匿名位域做人工 padding。长度为 0 的匿名位域会强制下一个位域重新开始对齐。