- 目录
- 初始化
- reference-return
- 指针与引用
- static 变量
- 泛型编程
- STL
- 性能优化
- 函数参数
- 生命周期安全
- 面向对象
- 异常
- 并发
- 效率优化
- 底层 const 优点
- 类的封装
- 异常安全
- 内存引用优化
- set_new_handle
- 莫滥用 inline 函数
- 标签分派
- 模板类型限制
- const 函数重载技巧
-
尽可能延后对象的声明直到使用的前一刻,并一律马上初始化
-
循环中使用的对象,一般在循环体中定义而非在循环外,因为 前者执行 n 次构造+n 次析构,后者执行 1 次构造+1 次析构+n 次赋值。 所以除非 1 次赋值比 1 次构造+1 次析构更高效,否则定义再循环内
一般来讲,长 string、容器与流这类构造开销大的类都应该定义在循环外
-
注意多个翻译单元(TU)中,non-local static 对象的初始化顺序是未定义的,此时需要使用reference-return 技术 该技术优点如下:
- 解决多 TU 中 static 对象的初始化顺序问题
- 在调用
get_a()
之前,不会有构造a
的开销
- 优点
- 书写更加简洁
- 防止人工判断类型出错
- 推断出编译器才知晓的类型,如 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>
_**
- 上述优点 1,同时也是缺点。因为编译器会优先调用 initializer*list 形参的构造函数,
即使需要类型转换;同时因为防止窄化,从而导致这种情况
-
默认初始化与值初始化
-
对于内置类型与聚合类,
int a[5];
叫默认初始化,int a[5]{}
叫值初始化,- 默认初始化静态变量为全零,初始化动态变量为未定义;
- 而值初始化会将对象初始化为全零
-
对于其它类类型,
string s;
与string s{};
都叫默认初始化,行为都一样,即调用其默认构造函数, 类类型的默认初始化应该显式的使用后者
-
-
构造函数的初始化列表
-
在进入函数体前会初始化所有 non-static 数据成员
-
对未出现在初始化列表中的成员执行默认初始化;若成员为内置类型且该类非聚合类,则值初始化该成员
-
注意成员的构造顺序与声明顺序一致,且基类本身先构造;对于多重继承的基类构造顺序,则是从左到右、从上到下 [注]
-
当在A
文件中定义a
,在B
文件中定义b
,b
依赖于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 语义或更改指向
- 使用裸指针做形参意味着函数内部无需在意其生命周期,其有效性与销毁由调用方保证
* 可以使用 nonlocal-static 变量的构造函数来初始化程序状态
* 尽量使用 reference-return 技术代替 nonlocal-static 变量
- 抽离代码
- 模板类型无关
- 算数类型
- 指针类型
- 优先使用可读性更好的重载
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技巧
来在调用函数时拒绝某个类型转换
- 不要忘记考虑限定成员函数的
-
右值、转发与移动:
某些情况,函数可能会返回已销毁变量的引用且难以察觉:
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 优化基类尺寸
- D is-a B:B 的所有特性与操作 D 都支持。例如,"student is a person"
-
继承基类时的接口含义
- 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,堆内存申请,等各种系统调用
- Caching
- 可以让编译器帮助我们查找违反本意(即修改了不该修改的数据)的代码
- 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
- 为不同的类设计 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 函数应该同时满足
-
规模短小
-
流程直接
-
调用频繁
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 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))); }