Effective Cpp

一、习惯C++

条款01:视C++为一个语言联邦

C++已经是个多重范型编程语言(multiparadigm programminglanguage),一个同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming)的语言。

C++的重要组成:C、Object-Oriented C++、Template C++、STL。

C++是包含四种次语言的一体多面语言,关键看你怎么用。

比如,只在C语言部分,pass-by-value通常比pass-by-reference高效,但在面向对象部分,正好相反,pass-by-reference-to-const是相对更好的选择。

而在STL中,迭代器和函数对象是在C pointer之上,所以pass-by-value更高效。

条款02:尽量以const,enum,inline替换#define

“宁可让编译器替换预处理器”。

  • 对于单纯常量,最好以const对象或enums替换 #define。
  • 对于形似函数的宏(macros),最好改用inline函数替换 #define,避免出错。
  • #ifdef / #ifndef 继续扮演控制编译的重要角色。
1
2
3
const double Ratio = 1.5;

#define RATIO 1.5

在编译器错误处理时,#define不会告诉你 RATIO 的出现信息,而是被替换的1.5。

enum 可以作为一种in class常量初值设定的方式。这样就取不到成员变量的地址。

1
2
3
4
5
class Player{
private:
enum {NumTurns = 5;};
...
};

条款03:尽可能使用const

如果关键字 const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。

这一类成员函数之所以重要,基于两个理由。第一,它使 class 接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。第二,它使“操作const对象”成为可能。

两个成员函数如果只是常量性(constness)不同,也可以被重载。比如 const T& getXXX() const;T& getXXX();

在const成员函数需要被修改的变量,使用mutable修饰。mutable释放掉non-static成员变量的bitwise constness约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CBook{
...
public:
std::size_t len() const;

private:
mutable std::size_t length;
mutable bool isValid;
};

std::size_t len() const{
if (!isValid){
length = std::strlen(text);
isValid = true;
}
return length;
};

利用const_cast将常量性移除,可以运用const成员函数实现出其non-const孪生兄弟。当 const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

1
2
3
4
5
6
7
8
9
10
11
12
class CBook{
public:
const char& operator[] (std::size_t pos) const{
...
return text[pos];
}

char& operator[] (std::size_t pos) {
return const_cast<char&>(
static_cast<const CBook*>(*this)[pos]);
}
};

另外,将某些东西声明为 const 可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

条款04:确定对象被使用前已先被初始化

永远在使用对象之前先将它初始化。确保每一个构造函数都将对象的每一个成员初始化。应该尽量使用initialization list。

C++有着十分固定的“成员初始化次序”。base classes更早于其derived classes被初始化(见条款12),而class的成员变量总是以其声明次序被初始化。

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。initialization list列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-localstatic对象,确保在使用对象前,初始化对象。Singleton模式的一个常见实现手法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FileSys{...};

FileSys& tfs(){
static FileSys fs;
return fs;
}

class Dir{...};

Dir::Dir(params){
...
std::size_t disks = tfs().numDisks();
...
}

Dir& tmpDir(){
static Dir td;
return td;
}

二、构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数

编译器就会为它声明(编译器版本的)一个copy构造函数、一个copy assignment操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个default构造函数。所有这些函数都是public且inline (见条款30)。

copy构造函数被用来“以同型对象初始化自我对象”,copy assignment操作符被用来“从另一个同型对象中拷贝其值到自我对象”。copy构造函数是一个尤其重要的函数,因为它定义一个对象如何passed by value 。

编译器可自动为class创建default构造函数、copy构造函数、copyassignment 操作符,以及析构函数。

1
2
3
4
5
6
7
8
class Empty{
public:
Empty() {...};
Empty(const Empty& rhs) {...};
~Empty() {...};

Empty& operator=(const Empty& rhs) {...};
}

条款06:不用默认构造函数时,需要明确即拒绝

明确声明一个成员函数,可以替代编译器默认版本。

或者拒绝编译器默认版本,可将相应的成员函数声明为private并且不予实现。

或者使用delete关键字,明确不使用。

条款07:为多态基类声明virtual析构函数

当derived class经由一个base class指针被删除时,base class若是non-virtual析构函数,则不会执行derived class的析构函数,导致内存泄露。

消除这个问题的做法很简单:给base class一个virtual析构函数。此后删除derived class 对象就会如你想要的那般。

对象需要在运行期决定哪一个virtual函数该被调用。由一个所谓vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。

析构函数的运作方式是,最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个derived class的析构函数被调用。

  • 如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。

  • Class 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphical),就不该声明virtual析构函数。

条款08:别让异常逃离析构函数

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后不传递或结束程序。否则可能出现不可预知的风险。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DBConn{
public:
...
void close(){
db.close(); // 可能出错
closed=true;
}

~DBConn{
if (!closed){
try{
db.closed();
}catch (...){
...
}
}
}
private:
DBConnection db;
bool closed = false;
}

条款09:绝不在构造和析构过程中调用virtual函数

derived class对象内的base class会在derived class自身被构造之前先构造。所以调用virtual 函数,derived class并为被完全初始化,导致出现参数未初始化错误。

在derived class对象的base class构造期间,对象的类型是 base class 而不是 derived class。不只 virtual 函数会被编译器解析至(resolve to)base class,若使用运行期类型信息(runtime typeinformation,例如dynamic_cast(见条款27)和typeid),也会把对象视为base class类型。

唯一能够避免此问题的做法就是:确定你的构造函数和析构函数都没有(在对象被创建和被销毁期间)调用 virtual 函数。

在构造函数或者析构函数中调用virtual 函数,不会调用到 derived class 层级的函数(只是 base class 那层)。

**条款10:令operator=返回一个 reference to *this**

为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。令赋值(assignment)操作符返回一个reference to *this。

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
class CC{
public:
CC& operator=(const CC& rhs){
...
return *this;
}
CC& operator+=(const CC& rhs){
...
return *this;
}
CC& operator=(int rhs){
...
return *this;
}
CC& operator++() {
...
return *this;
}
// 后置++,带参,且返回值
const CC operator++(int) {
CC tmp = *this;
this->operator++();
return tmp;
}
}

条款11:在operator=中处理“自我赋值”

欲阻止这种错误,传统做法是藉由operator=最前面的一个“证同测试(identity test)”达到“自我赋值”的检验目的。

在operator=函数内确保代码不但“异常安全”而且“自我赋值安全”的一个替代方案是,使用所谓的copy and swap技术。不仅解决了代码复用,还保证了赋值操作的安全性。

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
27
template <typename T>
class Matrix {
...
friend void swap(Matrix &a, Matrix &b) noexcept {
using std::swap; // 这一步允许编译器基于ADL寻找合适的swap函数
swap(a.x, b.x);
swap(a.y, b.y);
swap(a.data, b.data);
}
...
};

Matrix<T>& Matrix<T>::operator=(const Matrix &rhs){
// 检测自赋值
if (&rhs == this) {
return *this;
}
Matrix tmp = rhs; // copy
swap(tmp, *this); // swap
return *this;
}
// 甚至于 move and swap
Matrix<T>& Matrix<T>::operator=(Matrix2 &&rhs) noexcept {
Matrix2 tmp{std::forward<Matrix2>(rhs)};
swap(*this, tmp);
return *this;
}

条款12:复制对象时勿忘其每一个成分

如果你为class添加一个成员变量,你必须同时修改copy函数。你也需要修改class的所有构造函数(见条款4和条款45)以及任何非标准形式的operator=。

derived class必须复制其base class成分。那些成分往往是private(见条款22),所以你无法直接访问它们,应该让derived class的copy函数调用相应的base class函数。

当你编写一个copy函数,请确保复制所有 local 成员变量,调用所有 base classes 内的适当的copy函数。

注意,copy构造函数和copy assignment操作符,可以提取公共操作,但是不能互相嵌套使用。

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
27
28
29
class Base{
private:
string name;
}

class Derived: public Base{
public:
...
Derived(const Derived& rhs);
Derived& operator=(const Derived& rhs);
...

private:
int priority;
}

Derived::Derived(const Derived& rhs)
:Base(rhs), priority(rhs.priority){
...
}

// copy
Derived&
Derived::operator=(const Derived& rhs){
...
Base::operator=(rhs);
priority = rhs.priority;
return *this;
}

三、资源管理

C++程序中最常使用的资源就是动态分配内存,但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述符(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets。

条款13:以对象管理资源

把资源放进对象内, C++的“析构函数自动调用机制”确保资源被释放。

  • 获得资源后立刻放进管理对象(managing object)内。“以对象管理资源”常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII),几乎总是在获得资源后于同一语句内用它初始化某个管理对象。

  • 管理对象(managing object)运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁,其析构函数会被自动调用。

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源

  • 两个常被使用的RAII classes分别是 std::shared_ptr 和 std::auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使 被复制的ptr 指向null。

由于 std::shared_ptr 和 std::auto_ptr 内部析构使用的是 delete 而不是 delete[],所以以下代码是个错误:

1
2
3
std::auto_ptr<std::string> aps(new std::string[10]);

std::shared_ptr<std::string> aps2(new std::string[10]);

别对动态分配而得到的array使用 std::shared_ptr 和 std::auto_ptr。

条款14:复制RAII对象需要注意

复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定RAII对象的 copying 行为。

处理方法根据对象及其资源的特点决定。

  • 禁止复制
1
2
3
class Lock: private Uncopyable {
...
}
  • 对资源进行引用计数。使用 std::shared_ptr(同时可以用 deleter 参数传入 function object,控制计数为0时的行为)。
1
2
3
4
5
6
7
8
9
class Lock{
public:
explicit Lock(Mutex* pm): mutexPtr(pm, unlock){ // unlock 为 deleter
lock(mutexPtr.get());
}

private:
std::shared_ptr<Mutex> mutexPtr;
}
  • 深拷贝资源
  • 转移资源拥有权,比如使用 std::auto_ptr

条款15:在资源管理类中提供对原始资源的访问

APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“访问原始资源”的办法。

对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,隐式转换更灵活。

条款16:成对使用new和delete时要采取相同形式

当你使用 new,有两件事发生。第一,内存被分配出来(通过名为operator new的函数,见条款49和条款51)。第二,此内存区域会有一个或多个构造函数被调用。

当你使用 delete,也有两件事发生:第一,资源内存会有一个或多个析构函数被调用;第二,内存才被释放(通过名为operator delete的函数,见条款51)。

如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

1
2
3
4
5
6
7
8
9
10
string* p1 = new string;
delete p1;

string* p2 = new string[10];
delete[] p2;


typedef string Def[3];
string* p3 = new Def;
delete[] p3;

条款17:以独立语句将newed对象置入智能指针

理由是C++编译器处理事件顺序的不确定性。

比如,process传入Widget的ptr,和一个priority()函数:

1
process(std::shared_ptr<Widget>(new Widget), priority());

执行顺序中,在 new 和 shared_ptr 构造函数执行时,priority()的执行出现异常,那么new的对象可能导致资源泄露。

以独立语句将 newed对象存储于(置入)智能指针内。正确方法:

1
2
3
4
// 1
std::shared_ptr<Widget> pw(new Widget);
// 2
process(pw, priority());

四、设计与声明

条款18:让接口容易被正确使用,不易被误用

  • “促进正确使用”的办法包括:接口的一致性,与内置类型的行为兼容。
  • “阻止误用”的办法包括:建立新类型时限制类型上的不必要操作,不让使用者负责资源管理。
  • std::shared_ptr 支持定制 custom deleter。可被用来自动解除互斥锁(mutexes;见条款14)。

条款19:设计class犹如设计type

设计高效的classes必须了解你面对的问题:

  • 真的需要一个新type吗?如果只是为既有的class添加一些功能,是否单纯定义一或多个non-member函数或templates,就能够达到目的?。

  • 新type的对象应该如何被创建和销毁?即class的构造函数、析构函数、内存分配函数和释放函数(operator new,operator new[],operator delete和operator delete[])的设计。

  • 对象的初始化和对象的赋值有什么样的差别?这决定了构造函数和赋值操作符(operator=)的行为差异。别混淆了“初始化”和“赋值”。

  • 新type的对象如何被passed by value?即如何设计copy constructor。

  • 考虑type成员变量的取值合法范围。

  • 新type 的继承关系如何?是否继承自虚基类,是否会被新子类继承,析构函数是否需要为virtual?。

  • 新type需要什么样的类型转换?若允许类型 T1 被隐式转换为T2,就必须在 class T1 内写一个类型转换操作符(operator T2)或在 class T2 内写一个 non-explicit-one-argument 的构造函数(即,Ctor(int arg1, int arg2=1):m_arg1(arg1),m_arg2(arg2) {})。如果你只允许 explicit 构造函数存在,就得写出专门负责执行转换的构造函数,且不能是类型转换操作符(type conversion operators, 即operator T2)或 non-explicit-one-argument 构造函数。

  • 什么样的操作符和函数对此新 type 而言是合理的?即,需要为class声明哪些member函数,哪些外部全局函数。

  • 哪个成员为 public,哪个为protected,哪个为 private,哪一个 classes 和/或 functions 应该是friends?

  • 新type有是否需要是个class template?。

条款20:宁以pass by reference to const替换pass by value

默认情况下C++以pass by value方式(一个继承自C的方式)传递对象至函数。默认函数参数都是以实参的副本为初值,而调用端所获得的也是函数返回值的一个副本,由对象的copy 构造函数生成,这可能使得pass by value成为费时的操作。

pass by reference to const这种传递方式,没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。

以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而没有初始化derived class的部分。

  • 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
  • 以上规则并不适用于内置类型,以及 STL 的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。STL的迭代器和函数是基于C指针实现的。

条款21:必须返回对象时,别返回其reference

  1. 绝对不要返回pointer或reference指向一个local stack对象
  2. 绝对不要返回reference指向一个heap-allocated对象

条款22:将成员变量声明为private

从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。

  • 切记将成员变量声明为private。
  • protected并不比public更具封装性。

条款23:宁以non-member、non-friend替换member函数

宁可拿non-member、non-friend函数替换member函数。这样做可以增加封装性、包装弹性(packaging flexibility)和可扩展性。

条款24:若所有参数皆需类型转换,请为此采用non-member函数

如果你需要为某个成员函数的所有参数(包括this指针参数)进行类型转换,那么这个函数必须是个non-member。const T operator*(const T& lhs, const T& rhs)

条款25:考虑写出一个不抛异常的swap函数

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
  • 为“用户定义类型”进行std templates全特化是好的,但千万不要更改std::swap原来的实现。

五、实现

条款26:尽可能延后变量定义式的出现时间

尽可能延后变量定义式的出现,尽可能在使用变量前定义变量,尽可能在变量赋初值时定义变量。这样做可增加程序的清晰度并改善程序效率。

条款27:尽量少做转型动作

  • const_cast 通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C++-style转型操作符。
  • dynamic_cast 主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一可能耗费重大运行成本的转型动作。
  • reinterpret_cast 意图执行低级转型,实际运行情况取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。
  • static_cast 用来强迫隐式转换(implicit conversions),例如将non-const 对象转为 const 对象(条款3),或将 int 转为 double 等等。将 void* 指针转为某类型 typed 指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const——这个只有const_cast才办得到。

请记住:

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它包装成某个函数。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
  • 宁可使用C++-style(新式)转型,不要使用旧式转型。

条款28:避免返回handles指向对象内部成分

如果const成员函数传出一个reference指向成员变量,函数运行结果又被存储于对象外部,那么这个函数的调用者就可以通过reference修改对象的内部成员。

避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生 dangling handles 的可能性降至最低。

条款29:为“异常安全”而努力是值得的

异常安全函数(Exception-safe functions)提供以下三个保证之一:

  1. 基本型保证:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
  2. 强烈型保证:如果异常被抛出,程序状态不改变。如果函数成功,就没有异常出现;如果函数失败,程序会回退到“调用函数之前”的状态。
  3. 不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们其设计的功能。

异常安全码(Exception-safe code)必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。

强烈型异常安全的一种实践:copy and swap。原则很简单:为你打算修改的对象做出一份副本,然后对副本做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将副本和原对象在一个不抛出异常的操作中置换(swap)。

  • 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。
  • “强烈保证”往往能够以 copy-and-swap 实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。

条款30:透彻了解inline的里里外外

  • 将inline限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  • 不要只因为function templates出现在头文件,就将它们声明为inline。
  • !!! important!!!: 现代C、C++编译器,会自动优化代码,程序中 inline 已经只算是一种提示符,并不具备编译层面上的绝对含义。所以,忘了它也无妨。

条款31:将文件间的编译依存关系降至最低

  • 支持“编译依存性最小化”的一般构想是:依赖声明,不要依赖定义。
  • 程序库头文件应该以“完全且仅有声明式”(full and declaration-onlyforms)的形式存在。这种做法不论是否涉及templates都适用。

六、继承与面向对象设计

条款32:确定你的public继承塑模出is-a关系

public inheritance 意味 "is-a" 的关系。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

条款33:避免覆盖继承而来的名称

为了让被遮掩的名称再见天日,可使用 using 声明式或转交函数(forwarding functions)。

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
27
28
29
30
31
32
33
34
35
class Base {
int x;
public:
virtual void f1() = 0;
virtual void f1(int);
virtual void f2();
void f3();
void f3(double);
virtual void f5();
...
};

class Derievd: public Base {
public:
using Base::f1;
using Base::f3;
virtual void f1();
void f3();
void f4();
// fowarding function
virtual void f5() {
Base::f5();
}
...
};

Derived d;
int x = 1;
d.f1(); // Derived::f1
d.f1(x); // Base::f1
d.f2(); // Base::f2
d.f3(); // Derived::f3
d.f3(x); // Base::f3
d.f4(); // Derived::f4
d.f5(); // Derived::f5, 转到Base::f5()

条款34:区分接口继承和实现继承

函数接口(function interfaces)继承和函数实现(functionimplementations)继承。

  • 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
  • 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现,必要情况下,缺省实现可以单独设计为一个成员函数,而接口设计为pure virtual函数,防止缺省实现被误用。
  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现,比如设计每个对象都相同且必要的ID生成方法。

条款35:考虑virtual函数以外的其他选择

  • virtual函数的替代方案包括 NVI(Non-Virtual Interface) 手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  NVI(Non-Virtual Interface) 
class A {
public:
int score() const { // non-virtual, 子类不重载
...
int val = doScore();
...
return val;
}
private:
virtual int doScore() const { // 子类重载
...
}
};
  • 将功能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
  • std::function 对象的行为就像一般函数指针。这样的对象可接纳,函数签名(target signature)一致的所有可调用对象(callable entities)。
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
27
28
29
30
31
32
// declare
class Person;
int defaultLearnStrategy(const Person& pp);

// defination
class Person {
public:
typedef std::function<int(const Person&)> learnStrat;
explicit Person(learnStrat lst=defaultLearnStrategy):
stratFunc(lst) {
...
}
int score() const {
return stratFunc(*this);
}
private:
learnStrat stratFunc;
};

int readStrategy(const Person&) {
...
};
int writeStrategy(const Person&) {
...
};

...
Person p1(readStrategy);
Person p2(writeStrategy);

int score1 = p1.score();
int score2 = p2.score();

条款36:绝不重新定义继承而来的non-virtual函数

任何情况下都不该重新定义一个继承而来的non-virtual函数。

条款37:绝不重新定义继承而来的缺省参数值

绝对不要重新定义一个继承而来的virtual成员函数缺省参数值,因为缺省参数值都是静态绑定(statically bound),而virtual函数——你唯一应该覆写的东西——却是动态绑定(dynamically bound)。

静态绑定的问题,当父类指针指向子类对象,其静态类型就为父类。动态绑定,则会根据所指对象,解析动态类型为子类。

若子类重新定义一个继承而来的virtual成员函数缺省参数值,会静态解析为父类中的缺省参数值,而不是子类中重新定义的值。可能会导致一些不易排查的错误。

一种解决方法,是使用non-virtual实现父类的带缺省参数值的成员函数,调用一个virtual的功能成员,传入缺省参数值。子类只需要重载virtual的功能成员。

条款38:通过复合塑模出has-a关系

复合(composition)是类型之间的一种关系,指某种类型的对象内含其他类型的对象。

1
2
3
4
5
class A {
B component1;
C component2;
...
};

当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。

应用域指逻辑上的关联,比如电脑由存储系统、IO系统、计算系统等组成。实现域指一个类的实现中使用了buffer、mutex、binary search tree等技术手段。

条款39:明智而审慎地使用private继承

Private 继承意味 implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了使用class B内某些特性和方法,不是因为B对象和D对象存在有任何观念上的关系。

private继承纯粹只是一种实现技术(继承自一个private base class的每样东西在你的class内都是private,因为它们都只是实现的细枝末节而已)。

Private继承在软件设计层面上没有意义,其意义只在于软件实现层面。

  • Private继承意味is-implemented-in-terms of(根据某物实现出)。它通常比复合(composition)的级别低。
  • 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

因为 empty class 始终会占用 1 字节的空间。若使用复合,那么加上alignment的影响,类的空间会存在一些浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Defs {
typedef ...
...
};

// sizeof(A) == 8
class A {
int x;
Defs df;
};

// sizeof(B) == 4
class B: private Defs {
int x;
};

条款40:明智而审慎地使用多重继承

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
  • 多重继承有其用途。比如,同时“public继承某个interface class”和“private继承某个协助实现的class”。

七、模板与泛型编程

条款41:了解隐式接口和编译期多态

面向对象编程世界总是以显式接口(explicit interface)和运行期多态(runtime polymorphism)解决问题。

Templates 及泛型编程的世界,与面向对象有根本上的不同。泛型编程中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interfaces)和编译期多态(compile-time polymorphism)得到重视。

  • class 和 template 都支持接口(interfaces)和多态(polymorphism)。
  • 对 class 而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
  • 对 template 参数而言,接口是隐式的(implicit),它取决于T的具现化类型及其实现。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

条款42:了解typename的双重意义

template内出现的名称如果依赖某个template参数,称之为从属名称(dependent names)。

如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你明确指明它是个类型。

任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在它前面加上关键字 typename。

typename 不可以出现在 base classes list 内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。

  • 声明template参数时,前缀关键字class和typename可互换。
  • 标识嵌套从属类型名称时,请使用关键字typename;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
1
2
3
4
5
6
7
8
9
template<typename T>
class Derived: public Base<T>::Nested { // base class list 不允许typename
public:
// member initialization list 不允许typename
explicit Derived(int x): Base<T>::Nested(x) {
typename Base<T>::Nested temp; // dependent names
}
...
};

条款43:学习处理模板化基类内的名称

可在derived class templates内通过 "this->;" 指涉base class template内的成员,而不只是在特化的class template中寻找成员。例如:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
template<typename Company>
class MsgSender {
public:
void send(const MsgInfo& info) {...}
void encryptedSend(const MsgInfo& info) {...}
};

template<>
class MsgSender<CompanyK> {
public:
// 没有send
void encryptedSend(const MsgInfo& info) {...}
};

template<typename Company>
class LogSender: public MsgSender<Company> {
public:
void sendLog(const MsgInfo& info) {
...
// 当 Company 为 CompanyK,出错
// 因为没有send,只是在MsgSender<CompanyK>中找成员
send(info);
...
}
};

// Solution
template<typename Company>
class LogSender: public MsgSender<Company> {
public:
void sendLog(const MsgInfo& info) {
...
// 可在 template<typename Company> class MsgSender 中找成员
this->send(info);
...
}
};
// 或者
template<typename Company>
class LogSender: public MsgSender<Company> {
public:
// 告诉编译器
using MsgSender<Company>::send;
void sendLog(const MsgInfo& info) {
...
send(info);
...
}
};
// 或者
template<typename Company>
class LogSender: public MsgSender<Company> {
public:
void sendLog(const MsgInfo& info) {
...
// 明确指出
MsgSender<Company>::send(info);
...
}
}

条款44:将与参数无关的代码抽离

  • Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生依赖关系。
  • 因非类型模板参数(non-type template parameters,比如 n)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
1
2
3
4
5
6
template<typename T, std::size_t n>
class Matrix {
public:
void invert();
...
};

使用以上Matrix,其 Matrix<int, 5> 和 Matrix<int, 10> 会产生两套处理n不同,其他都类似的 invert 代码,造成代码膨胀。

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
27
28
29
template<typename T>
class MatrixBase {
protected:
MatrixBase(std::size_t n, T* pMem): msize(n), pData(pMem) {
...
}
void setDataPtr(T* ptr) {pData = ptr;}
void invert(std::size_t fsize) {
...
}
...
private:
std::size_t msize;
T* pData;
};

template<typename T, std::size_t n>
class Matrix: private MatrixBase<T> {
using MatrixBase<T>::invert; // 使用Base的invert
public:
Matrix(): MatrixBase<T>(n, nullptr),
pData(new T[n * n]) {
this->setDataPtr(pData.get());
}
void invert() { this->invert(n); }
...
private:
boost::scoped_array<T> pData;
};

以上就只有一份 invert 代码,是一种解决方式。并且使用指针传递数据地址,进一步与 n 参数分离。

  • 因类型参数(type parameters,比如 int、long等)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。比如,STL中,vector、list等,在实现操作强类型指针 T* 的成员函数时,都调用了一个操作 void* 的成员函数,由后者完成实际工作,避免代码膨胀。

条款45:运用成员函数模板接受所有兼容类型

  • 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
  • 如果你声明 member templates 用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。因为泛化copy构造并不会阻止编译器生成默认的copy构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T>
class shared_ptr {
public:
// copy constructor
shared_ptr(const shared_ptr& rhs);
// templated copy constructor
template<class Y>
shared_ptr(const shared_ptr<Y>& rhs);
// copy assignment
shared_ptr& operator=(const shared_ptr& rhs);
// templated copy assignment
template<class Y>
shared_ptr& operator=(const shared_ptr<Y>& rhs);
...
}

条款46:需要类型转换时请为模板定义非成员函数

当我们编写一个class template,实现一个外部函数 function template,其所有参数需要进行class template的隐式类型转换时,请将这个外部函数定义为 class template 内部的 friend 函数。

因为 function template 在对实参进行类型推导时,从不考虑通过构造函数进行的隐式类型转换。

这里 friend 的作用不再是为了外部函数访问 class 的 non-public 部分,而是创建一个 non-member function ,以此来完成实参的类型的隐式转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class T>
const Rational<T> doMultiply(const Rational<T>& lhs,
const Rational<T>& rhs); // declare

template<class T>
class Rational {
public:
// 完成模板具体化
friend const Rational<T> operator*(const Rational<T>& lhs,
const Rational<T>& rhs) {
return doMultiply(lhs, rhs);
}
};
template<class T>
const Rational<T> doMultiply(const Rational<T>& lhs,
const Rational<T>& rhs) {
...
}

条款47:请使用traits classes表现类型信息

STL中 iterator 的一个示例:

1
2
3
4
5
6
7
8
9
template<typename T>
struct __list_iterator {
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef T* pointer;
typedef T& reference;
typedef ptrdiff_t difference_type;
...
};

trait class常见设计如下:

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
27
28
29
30
31
32
//使用iterator提供的类型信息
template<typename Iterator>
struct iterator_traits
{
typedef typename Iterator::iterator_category iterator_category;
typedef typename Iterator::value_type value_typep;
typedef typename Iterator::difference_type difference_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
};

// 指针偏特化。
template<typename T>
struct iterator_traits<T *>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
};

// const指针偏特化
template<typename T>
struct iterator_traits<const T *>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef const T* pointer;
typedef const T& reference;
};

常见使用:

1
typedef typename iterator_traits<Iterator>::iterator_category category;

iterator_traits 在编译期获取 iterator_category 等信息。

请记住:

  • Traits classes使得“类型相关信息”在编译期可用。它们通过 templates 和 templates特化实现。
  • 整合重载技术(overloading)后,traits classes 有可能在编译期对类型执行if...else测试。

条款48:认识template元编程

Template metaprogramming(TMP,模板元编程)是编写template-based C++程序并执行于编译期的过程。一旦TMP程序结束执行,其输出,也就是从 templates 具体生成的若干C++源码,便会一如往常地被编译。

一个 TMP 递归程序:

1
2
3
4
5
6
7
8
9
10
11
12
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n - 1>::value };
};

template<>
struct Factorial<0> {
enum { value = 1 };
};

...
int f6 = Factorial<6>::value;
  • Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
  • TMP 可被用来生成“基于策略选择组合”(based on combinations of policy choices)的定制代码,可用于实现多种设计模式,也可用来避免生成对某些特殊类型并不适合的代码。

八、定制new和delete

条款49:了解new-handler的行为

  • set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用。
  • nothrow new 是一个颇为局限的工具,因为它只适用于内存分配阶段;后续的构造函数调用还是可能抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
void outOfMem() {
std::cerr << "Out of memory." << std::endl;
std::abort();
}

int main() {
std::set_new_handler(outOfMem);
// 失败返回 0
int *noArr = new(std::nothrow) int[100000000000L];
// outOfMem() 触发
int *arr = new int[100000000000L];
}

条款50:了解new和delete的合理替换时机

合理替换时机:

  • 用来检测运用上的错误。如果我们自行定义一个 operator new,在申请的内存中写入特定的签名signatures。operator delete 检查上述签名是否原封不动,若否就表示在分配的内存区域中发生了 overrun 或 underrun,并记录log信息。
  • 为了强化效能。对某些应用程序而言,将编译器自带的new和delete替换为定制版本,是提升效率的办法之一。
  • 为了收集使用上的统计数据。
  • 为了优化内存空间的分配、内存对齐优化等。
  • 为了将关联数据结构尽量保存在连续的更少的内存页上,减少page fault。

条款51:编写new和delete时需固守常规

  • operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new_handler。它也应该有能力处理0 bytes申请。Class专属版本则还应该处理申请内存大小和class大小不匹配的情况,这通常是 derived class 没有实现 operator new 而调用了 base class 的 operator new 的情况。
  • operator delete 应该在收到null指针时不做任何事。Class专属版本则还应该处理申请内存大小和class大小不匹配的情况。
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
27
28
29
30
31
32
33
34
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void* rawMem, std::size_t size) throw();
...
};

void* Base::operator new(std::size_t size) throw(std::bad_alloc) {
if (size != sizeof(Base))
return ::operator new(size); //使用std中标准new
if (size == 0)
size = 1; // 一种处理方法,始终返回合法指针
while (true) {
分配内存;
if 分配成功
return 指针
// 以下只是为了取得 new_handler 函数指针
std::new_handler gHandler = std::set_new_handler(0);
std::set_new_handler(gHandler);

if (gHandler) (*gHandler)();
else throw std::bad_alloc();
}
}

void Base::operator delete(void* rawMem, std::size_t size) throw() {
if (rawMem == 0) return;
if (size != sizeof(Base)) {
::operator delete(rawMem);
return;
}
回收内存;
return;
}

条款52:写了placement new也要写placement delete

如果operator new接受的参数不止size_t,那就是 placement new。众多 placement new 版本中特别有用的一个是“接受一个指针指向对象该被构造之处”。

一个带额外参数的 operator new,需要带相同额外参数的对应版operator delete。

要防止内存泄漏,必须同时提供一个正常的operator delete,用于构造期间无任何异常被抛出,和一个 placement delete 版本,用于构造期间有异常被抛出。后者的额外参数必须和operator new一样。

  • 当你写一个 placement operator new,也需要写对应的 placement operator delete。如果没有,你的程序可能会发生内存泄漏。
  • 当你声明 placement new 和placement delete,考虑是否有必要覆盖它们的正常(全局默认)版本。
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
27
28
29
30
31
32
33
34
35
class StandardNewDelete {
public:
// 使用全局默认的 new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc) {
return ::operator new(size);
}
static void operator delete(void* pMem) throw() {
::operator delete(pMem);
}
// 使用全局默认的 placement new/placement delete
static void* operator new(std::size_t size, void* ptr) throw() {
return ::operator new(size, ptr);
}
static void operator delete(void* pMem, void* ptr) throw() {
::operator delete(pMem, ptr);
}
// 使用全局默认的 nothrow new/nothrow delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) throw() {
return ::operator new(size, nt);
}
static void operator delete(void* pMem, const std::nothrow_t& nt) throw() {
::operator delete(pMem);
}
}

// 增加自定义形式
class Derived: public StandardNewDelete {
public:
// 防止标准 new/delete 被覆盖
using StandardNewDelete::operator new;
using StandardNewDelete::operator delete;
// 追加自定义 placement new/placement delete
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void* pMem, std::ostream& logStream) throw();
}

九、杂项讨论

条款53:不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息。努力在编译器的最高警告级别下争取“无任何警告”。
  • 不同的编译器处理方式并不相同。

条款54:让自己熟悉标准程序库

  • C++标准程序库的主要功能由STL、iostreams、locales组成。
  • 熟悉智能指针、函数指针、hash-based容器、正则表达式(regular expressions)等。

条款55:让自己熟悉Boost

  • Boost致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!