Skip to content

Latest commit

 

History

History
588 lines (453 loc) · 19.8 KB

cppguide.md

File metadata and controls

588 lines (453 loc) · 19.8 KB

目录

初始化

  • 尽可能延后对象的声明直到使用的前一刻,并一律马上初始化

  • 循环中使用的对象,一般在循环体中定义而非在循环外,因为 前者执行 n 次构造+n 次析构,后者执行 1 次构造+1 次析构+n 次赋值。 所以除非 1 次赋值比 1 次构造+1 次析构更高效,否则定义再循环内

    一般来讲,长 string、容器与流这类构造开销大的类都应该定义在循环外

  • 注意多个翻译单元(TU)中,non-local static 对象的初始化顺序是未定义的,此时需要使用reference-return 技术 该技术优点如下:

    • 解决多 TU 中 static 对象的初始化顺序问题
    • 在调用get_a()之前,不会有构造a的开销

auto 初始化

  • 优点
    • 书写更加简洁
    • 防止人工判断类型出错
    • 推断出编译器才知晓的类型,如 lambda
    • 易于重构
  • 缺点
    • 初始化器为代理类时,可能会出错
    • auto init = {1, 2},auto 为initializer_list<int>

列表初始化

  • 优点
    • initializer_list实现类聚合式初始化
    • 调用构造函数
    • 隐式类型转换
    • 防止窄化
  • 缺点
    • 上述优点 1,同时也是缺点。因为编译器会优先调用 initializer*list 形参的构造函数, 即使需要类型转换;同时因为防止窄化,从而导致这种情况vector<bool> v{10, true};, 这段代码会报错,因为它调用了构造函数vector(initializer_list<bool>),同时 10 转为 bool 被视作窄化。 而其实我们本意是构造***含 10 个元素,且均为 true 的vector<bool>_**

默认初始化

  • 默认初始化与值初始化

    • 对于内置类型与聚合类,int a[5];叫默认初始化,int a[5]{}叫值初始化,

      • 默认初始化静态变量为全零,初始化动态变量为未定义;
      • 而值初始化会将对象初始化为全零
    • 对于其它类类型,string s;string s{};都叫默认初始化,行为都一样,即调用其默认构造函数, 类类型的默认初始化应该显式的使用后者

  • 构造函数的初始化列表

    • 在进入函数体前会初始化所有 non-static 数据成员

    • 对未出现在初始化列表中的成员执行默认初始化;若成员为内置类型且该类非聚合类,则值初始化该成员

    • 注意成员的构造顺序与声明顺序一致,且基类本身先构造;对于多重继承的基类构造顺序,则是从左到右、从上到下 [注]

reference-return

 当在A文件中定义a,在B文件中定义bb依赖于a, 但是a不一定就在b之前构造 [注], 这就可能导致致命错误。  解决办法是将 non-local static 转换为 local-static,即在函数内定义 static 对象,然后返回该对象的引用

例子
/*
 * 参考自 Effective C++ 3e
 */

// A文件的全局作用域
ObjA& get_a()       // reference-returning函数
{
    static Obj a{}; // 在首次调用该函数期间,或首次遇到该对象定义式时构造该对象
    return a;
}

// B文件的全局作用域
ObjB b{get_a()};    // 这样在构造b之前,a必定是已构造的

指针与引用

  • 一般使用引用,除非需要NULL 语义更改指向
  • 使用裸指针做形参意味着函数内部无需在意其生命周期,其有效性与销毁由调用方保证

static 变量

* 可以使用 nonlocal-static 变量的构造函数来初始化程序状态
* 尽量使用 reference-return 技术代替 nonlocal-static 变量

泛型编程

  • 抽离代码
    • 模板类型无关
    • 算数类型
    • 指针类型

STL

  • 优先使用可读性更好的重载operator
  • Stream 在循环中使用一定不要忘记调用clear()
  • Stream 仅持有 StreamBuf 的指针而非完整对象,不保证其生命周期
  • Container 使用emplace函数代替push|insert函数
  • Container 增删元素时,注意 range-based-for、引用、指针、迭代器的有效性

性能优化

  • 减少分支
  • 缓存机制
  • 异步预取
  • 延迟获取
  • 异步与并发

函数参数

  • 形参修饰

    对比

类型 特性
T 拷贝
T& 引用、左值(非常量)
T&& 引用、右值
const T& 引用、左值、右值、类型转换、只读
temp T& 引用、左值、泛型
temp const T& 引用、左值、右值、泛型、只读
temp T&& 引用、左值、右值、泛型、转发

选择

  • 属性:

    • 不要忘记考虑限定成员函数的this
    • 只要函数不应该抛出异常,就应该限定为noexcept
    • 不打算导出的变量与函数定义于unnamedspace
    • 需要引用外部变量与函数声明为extern
    • 莫滥用 inline 修饰函数(尤其是构造函数于析构函数),inline 函数应符合:规模短小、流程直接、调用频繁
    • 不鼓励在函数中使用 auto 推断实参类型,该特性应仅限于 lambda 中使用
    • 利用=delete技巧来在调用函数时拒绝某个类型转换
  • 右值、转发与移动:

    • 需要创建形参副本时(包括 return),针对右值引用形参使用 move,对万能引用形参使用 forward;并注意应该在最后使用对象时才进行操作
    • 值返回时,若 return 返回函数局部变量且其类型与返回类型不符,则使用 move
    • 解决与万能引用余其他模板的冲突问题

生命周期安全

某些情况,函数可能会返回已销毁变量的引用且难以察觉:

struct T {
  vector<int>& Get() {return v;}
  const vector<int>& Get() const {return v;}
  vector<int> v;
}
vector<int>& v = T{}.Get(); // 这条语句结束后,v指向的vector<int>就被销毁了

// 解决方案
struct T {
  vector<int>& Get() & {return v;}
  vector<int>& Get() const& {return v;}
  vector<int> v;
}
vector<int>& v = T{}.Get(); // compile error
const T& ret(const T& i) {return i;}
const T& val = ret(1);  // 这条语句结束后,val指向的变量就被销毁了

// 解决方案:重载右值引用

面向对象

  • 取消友元
  • 数据成员
    • private
    • pImpl
    • 构造顺序
    • 结构对齐
  • 构造
    • default?
    • explicit?
    • non-inline
    • never-call-virtual
  • 析构
    • virutal & definition
    • noexcept & .destroy()
    • non-inline
    • never-call-virtual
  • copy? & move?
  • operator
    • 单成
    • 算赋
    • 前后
    • explicit bool 1
  • 先声明再定义,注意定义依赖顺序

  • 思考是否可提供接口成员函数以取消友元函数的 friend 属性以加强封装

  • 类型成员应该声明在前

  • 数据成员

    • private 封装
    • handle 与 interface 封装
    • 构造顺序与对齐问题
    • 若有 const 与引用成员,则无法合成 default 构造与赋值操作
  • 构造函数

    • 思考 default 构造是否有意义
    • 单参构造最好explicit
    • 不要声明为inline
    • 不要调用虚函数,因为调用的虚函数是静态版本而违反直觉
  • 析构函数

    • 多态基类的析构函数应该声明为virtual,并需要提供定义(即使是pure virtual
    • 不要让异常逃离析构函数,同时可以提供.destroy()接口给用户来处理异常的机会
    • 不要声明为inline
    • 不要调用虚函数,因为调用的虚函数是静态版本而违反直觉
  • copy 与 move

    • 思考是否需要 copy 或 move 操作
    • 可以提供virtual unique_ptr<T> clone() const接口来进行 copy 而不会发生切割(截断)
  • 类型转换

    • 不要在两个类之间定义相同方向的转换,例如,在 A 中定义由 B 到 A 的转换,又在 B 中定义从 B 到 A 的转换
    • bool外,最多只定义一个与内置类型有关的类型转换
    • 所有转换最好explicit
  • 重载 operator

    • 不要重载&&||,
    • 一般只有单目运算符和赋值运算符设计为成员
    • 若定义了算术运算符,也应定义相应的复合赋值
    • 后置自增减运算符应该调用前置版本以降低维护难度
    • 对于接收左值的操作符,限定 this 为左值
  • 接口成员函数

  • 类之间的关系及其设计

    • D is-a B:B 的所有特性与操作 D 都支持。例如,"student is a person"

      设计:

      • D public 继承 B
      • 若 B 的某些特性,D 并不支持,则需要从 B 中抽出更基础更抽象的基类特性,再进行继承
      • 实现一种可供多种类使用的特性,若需要使用 static 数据成员,则该基类应该设计为类模板, 并让派生类将自己的类型作为模板参数来继承该基类,否则基类的 static 成员对整个程序中所有继承自它的派生类都唯一
    • D has-a B:B 是 D 的一个组分或一种性质。例如,"person has a name and ID"

      设计:

      • D 复合 B,即 B 作为 D 的数据成员
    • D is-implement-in-terms-of B:B 为 D 提供一些用户不可见的实现细节

      设计:

      • D 复合 B
      • D priveta 继承 B,满足以下情况才选择此设计:
        • D 需要访问 B 的 protect 成员
        • D 需要修改 B 的 virtual 函数
        • 利用 EBO 优化基类尺寸
  • 继承基类时的接口含义

    • pure virtual:只声明接口,强制派生类覆写定义
    • non-pure virtual:声明接口,并提供默认定义
    • non-virtual:声明接口,并提供不应该被修改的定义
  • 继承体系中,non-leaf 类应该设计为抽象基类

异常

  • 异常的易错点
    • catch 语句按顺序进行捕获,所以派生类放前面,基类放后面
    • 以引用捕获,而非值捕获或指针捕获
  • 异常安全技巧:
    • 留心可能发生异常的代码

      大部分标准库异常见这儿

    • 限制从不受信任来源接收的数据的数量和格式
    • 代码前移以减少当异常发生时做的无用功
    • 使用 unique_ptr 管理资源
    • 使用 copy-and-sawp 技巧
    • 使用 move_if_noexcept

并发

  • 同步单个变量用std::atomic,同步多个变量用std::mutex

  • volatile用于特种内存(值的改变由程序之外的条件影响)

  • 保证 const 成员函数的线程安全性,因为 const 成员函数一般视为只读,如此在多线程环境应该是无需用户手动同步的; 但是若它更改了 mutable 数据成员,为了维护上述线程安全性,需要在 const 成员函数中进行同步

效率优化

  • 延迟评估:

    • 写时复制
      • Handle 类持有 Implement 类的指针
      • Implement 类 public 继承 RCSupport 类,并管理数据
      • RCSipport 类维护引用计数
      • Handle 类对象执行写入操作时,才通过 new 表达式构造新的 Implement 对象,否则与拷贝目标共享
    • 区分读写
      • 利用 proxy 类作 operator[]这些返回左值引用的操作
      • proxy 类持有 Handle 类的指针以及所代理对象的下标
      • 若对 proxy 对象执行需要左值的操作,则视作写入
      • 其它情况可以利用转换函数解决
    • 延迟获取(按需加载)

      类的数据成员设为mutable optional<T>,需要时再获取值

  • 超前预算:

    • Caching

      缓存之前获取的结果

    • Prefetching

      预先进行一次大动作,代替多次小动作 如:磁盘 I/O,堆内存申请,等各种系统调用

底层 const 优点

  • 可以让编译器帮助我们查找违反本意(即修改了不该修改的数据)的代码
  • const T&相比T&,前者可以接收更多类型,见下表

类的封装

  • non-member non-friend 函数比 member 具有更好的函数封装性,且可以前者可以分离编译

  • 数据成员应该封装为 private, 为了更好的封装数据成员以降低编译依赖甚至可以使用handle 类interface 类

    主要是重构数据成员时,所有调用该类的文件都要重新编译,因为数据成员包含在类的定义中被一起放在类头文件, 用以编译器判断类对象所需栈内存大小。解决办法:

    • Handle 类:Handle 类作为接口类给用户使用,其内存储指针指向实现类(其中包含数据成员),如此一来实现类便只需声明式, 再将实现类的定义接口类的接口函数的定义放在实现源文件中,如此实现分离而降低编译依赖

      例子
      // 接口头文件
      #pragma once
      #include <memory>
      
      class Handle
      {
      public:
          /* ... */
          ~Handle();
      private:
          struct Impl;
          unique_ptr<Impl> p2Imp_m;
      };
      
      // 实现源文件
      #include <string>
      #include "myheader.hpp"
      
      struct Handle::Impl
      {
          std::string str_m;
          mine::MyType t_m;
      };
      
      /* ... */
      
      Handle::~Handle() = default // unique_ptr<T>的析构需要获取T的完整定义,并且注意三五原则
    • Interface 类:Interface 类作为接口给用户使用,其本质是一个抽象基类,其中定义了用户接口(没有数据成员), 并提供 static 成员函数来构造实现类(即派生类)并获取其指针或引用(具体来说是 unique_ptr)。最后将该static 函数实现类(派生类) 定义在实现源文件

      使用 unique_ptr 的目的是防止 static 函数返回的指针忘记被 delete,而抽象基类是无法定义具体对象的, 包括其引用

      例子
// 接口头文件
#include <string>
#include <memory>

struct Human
{
    virtual ~Human() {  };
    virtual const std::string& name() const =0;
    virtual std::size_t id() const =0;
    static std::unique_ptr<Human> make(const std::string& name = "", std::size_t id = 0);
};

// 实现源文件
#include "human.hpp"
#include <memory>
#include <string>

class HumanInte : public Human
{
public:
    HumanInte(const std::string& name, std::size_t id):
        name_m{name}, id_m{id} {  }

    const std::string& name() const override
    {
        return name_m;
    }

    std::size_t id() const override
    {
        return id_m;
    }

private:
    std::string name_m;
    std::size_t id_m;
};

std::unique_ptr<Human> Human::make(const std::string& name, std::size_t id)
{
    return std::make_unique<HumanInte>(name, id);
}
    </details>

异常安全

  • 异常安全性

    • 不泄漏任何资源
    • 不允许数据败坏
  • 异常安全等级

    由等级最低者决定

    • 基本承诺 :若异常发生,保证资源不泄漏,保证数据有效
    • 强烈保证 :若异常发生,保证返回状态与调用前状态相同
    • 不抛出异常:顾名思义,但若还是抛出异常则终止程序

内存引用优化

读取某个内存引用,若在作用域内不可能被某些操作写入,重复引用时可以被编译器优化, 而避免手动将其载入寄存器的麻烦,以下操作便会妨碍优化:

  • 传引用参数:函数有多个传引用形参,试图读取其中一个实参,又要写入另一个参数(调用任何有非底层 const 传引用参数的函数都视为写入)

    因为传入的多个引用可能指向同一个对象,所以对于所有处理多引用的函数应该能够处理该情况

  • 类成员:试图读取一个类的数据成员,但又要调用同一个类对象的 non-const 成员函数

  • 全局变量:试图读取全局对象,但又要调用任何函数

    任何函数都可能修改全局变量,包括 lambda

set_new_handle

  • 为不同的类设计 new 失败时的处理函数
// 此例子中为简便而并未将实现分离
// 让需要自定义new_handle的类public继承该类即可,例如`class Test: public NewHandlerSupport<Test>`
template <typename T> // 模板的目的是为了让不同的类继承不同的实例,从而产生不同的static new_handler对象
class NewHandlerSupport
{
public:
    static std::new_handler
    set_new_handler(std::new_handler newHandle) noexcept
    {
        auto oldHandle = CurHandle;
        CurHandle = newHandle;
        return oldHandle;
    }

    static void*
    operator new(std::size_t size);
    {
        NewHandlerHolder hd{std::set_new_handler(CurHandle)};
        return ::operator new(size);
    }
private:
    struct NewHandlerHolder
    {
        std::new_handler holder_m;
        NewHandlerHolder(std::new_handler hd): holder_m{hd} {}
        ~NewHandlerHolder() { std::set_new_hanlder(holder_m); }
    }
    static inline std::new_handler CurHandle{nullptr};
}

莫滥用 inline 函数

注意:莫要滥用 inline 函数,特别是构造函数与析构函数,因为

  • 构造函数的会捕获成员构造时所抛出的异常并析构已构造的成员(不会调用类的析构函数), 则会有大量的重复调用成员的析构函数,特别是第一个构造的成员的析构函数

  • 构造函数会调用其数据成员的构造函数,当类的复合嵌套或继承愈来愈深,构造函数的代码重复便会愈来愈严重

    一句话总结就是:inline 函数应该同时满足

  • 规模短小

  • 流程直接

  • 调用频繁

标签分派

template <typename T>
void func_tool(T&& t, false_type);

void func_tool(int i, true_type);

template <typename T>
void func(T&& t)
{
    // 若decay_t<T>为int,则则调用针对int的重载函数
    func_tool(forward<T>(t), is_same_v<int, decay_t<T> >);
}

模板类型限制

// 限制对`int&` `const int&` `int&&`的实例化
template <typename T, typename = enable_if_t<
    !is_same_v<decay_t<T>, int>
>
void func(T&&);

const 函数重载技巧

  • const T&重载T&时,non-const 版本调用 const 版本以避免代码重复

    const T& func(const T& t);
    
    T& func(T& t)
    {
        return const_cast<T&>(func(static_cast<const T&>(t)));
    }