Implementations

Item 26: Postpone variable definitions as long as possible

为了尽可能减少变量构造和析构成本,我们应该直到变量被使用的前一刻再定义它。

1
2
3
4
5
6
7
8
9
10
std::string encryptPassword(const std::string& password)
{
std::string encrypted;
if (password.length() < 8) {
throw std::logic_error("Password is too short");
}

encrypted = doSomething(password);
return encrypted;
}

在这个例子里,如果我们在第4行处抛出异常,已经被构造出的excrypted需要被析构,尽管它从来没有被使用过。如果我们把encrypted的定义往后挪,就能避免这种情况。

1
2
3
4
5
6
7
8
9
std::string encryptPassword(const std::string& password)
{
if (password.length() < 8) {
throw std::logic_error("Password is too short");
}

std::string encrypted = doSomething(password);
return encrypted;
}

这可能不符合(ANSI)C语言中把变量声明全部放在函数体头部的习惯,但这是C++的best practice。

对于循环变量:

1
2
3
4
5
6
7
8
9
10
11
12
// A
Widget w;
for (int i = 0; i < n; ++i) {
w = doSomething(i);
...
}

// B
for (int i = 0; i < n; ++i) {
Widget w = doSomething(i);
...
}

A的开销是1次构造+1次析构+n次赋值,B的开销是N次构造+n次析构,因此使用哪种需要根据具体情况下相关函数的开销而定。

Item 27: Minimize casting

类型转换不仅仅是告诉编译器将一种类型视作另一种类型,还可能产生额外代码,因此是有代价的。例如整型数转浮点数:

1
2
int i = 42;
double d = static_cast<double>(i);
1
2
3
pxor    xmm0, xmm0
cvtsi2sd xmm0, DWORD PTR [rbp-4]
movsd QWORD PTR [rbp-16], xmm0

不要试图通过将派生类对象转成基类来调用基类函数(谁会这么写?)

以下代码的输出是0 1,因为Derive::foo()在一个临时的Base对象上调用了Base::foo()。直接用Base::foo()调用就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base {
public:
bool base_foo_called{false};
virtual void foo() {
base_foo_called = true;
}
};

class Derive: public Base {
public:
bool derive_foo_called{false};
virtual void foo() {
static_cast<Base>(*this).foo();
derive_foo_called = true;
}
};

int main() {
Derive d;
d.foo();
cout << d.base_foo_called << ' ' << d.derive_foo_called << endl;
}

dynamic_cast性能开销很大,如果有一个元素为基类指针Base*的容器,不要试图做这些事:

  1. 遍历每个元素,用dynamic_cast判断它指向的是否为派生类Derived,是的话调用Derived::foo()

    这样会做n次dynamic_cast,不如把foo()变成虚函数,给Base::foo()一个空定义,然后直接对每个元素调用foo()

  2. 用一堆if else逐一判断每个元素指向对象的类型。

    性能极差,而且每次继承体系被修改也得跟着改。应该改成基于虚函数的实现。

Item 28: Avoid returning "handles" to object internals

如果我们有一个Point类和一个Rectangle类,Rectangle提供upperleft()成员函数,返回左上角Point的引用。这是一个const成员函数,因为该函数不修改矩形的private变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point {
public:
Point(int x = 0, int y = 0) : x_(x), y_(y) {}
void setx(int x) { x_ = x; }
void sety(int y) { y_ = y; }
private:
int x_;
int y_;
};

class Rectangle {
public:
Point &upperleft() const { return upperleft_; }
Point &lowerright() const { return lowerright_; }
private:
Point upperleft_;
Point lowerright_;
};

以上代码无法通过编译,因为upperleft()的返回类型没有用const修饰,提醒我们要加上这个const

但是,如果我们的Rectangle是基于pimpl实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Rectangle {
public:
Rectangle(const Point &upperleft, const Point &lowerright) {
pImpl_ = std::make_shared<RectangleImpl>();
pImpl_->upperleft_ = upperleft;
pImpl_->lowerright_ = lowerright;
}
Point &upperleft() const { return pImpl_->upperleft_; }
Point &lowerright() const { return pImpl_->lowerright_; }
private:
struct RectangleImpl {
Point upperleft_;
Point lowerright_;
};
std::shared_ptr<RectangleImpl> pImpl_;
};

尽管我们忘记给upperleft()的返回类型加const,这段代码仍然能通过编译,因为该函数确实没有修改pImpl,编译器不管嵌套类RectangleImpl里面的事情(对编译器来说,这是一个与Rectangle无关的类)。因此,可以通过如下代码修改一个const对象。

1
2
const Rectangle rec(Point(0, 0), Point(10, 10));
rec.upperleft().setx(5);

因此,返回一个类内部成分的handle(包括指针,引用,迭代器)是危险的,这里的内部成分是逻辑上的,不一定是对象的成员,语法上可以是对象以外的一个变量(如本例)。

即使我们这里给Rectangle::upperleft()加了const,虽然不能改对象了,还是有悬垂handle的问题:

1
2
3
4
5
6
7
8
9
10
Rectangle foo() {
return Rectangle(Point(0, 0), Point(10, 10));
}

int main() {
const Point &upperleft = foo().upperleft();

cout << upperleft.getx() << endl;
cout << upperleft.gety() << endl;
}

这里的upperleft是对foo()返回的临时对象内部成分的引用,因此它在第6行结束后成为一个悬垂引用,对其的访问是非法的。

当然,不是任何情况都不能返回指向类内部成分的handle,operator[]就是一个反例,它不得不这么做。

Item 29: Strive for exception-safe code

异常安全(exception-safe)指的是函数抛出异常后,应该仍满足:

  1. 不发生资源泄漏,例如一个拿住的锁没有释放。

  2. 所操作的对象仍处于一个一致(合法)的状态。

    “一致”不局限于函数完全执行,或者完全没有执行两种状态,只要是对象的一个合法状态就可以。

    一个不满足2的例子:

    1
    2
    3
    4
    5
    6
    void PrettyMenu::changeBg(std::istream& imgSrc)
    {
    delete bgImage_;
    ++imageChangeCnt;
    bgImage_ = new Image(imgSrc);
    }

    如果创建Image对象时抛出异常,bgImage_会成为一个空悬指针。

对于1,通过RAII资源管理类可以保证资源不泄漏(如lock_guard之于mutex)。对于2,函数的一致性保证又可以分为不同程度:

  1. 基本保证:异常抛出后,对象仍处于一致状态,但不知道具体是哪一种。
  2. 强保证:异常抛出后,对象回到函数调用之前的状态。
  3. 承诺不抛出异常。

大部分函数很难做到3,例如一个需要分配内存的函数必须在内存不足时抛出bad_alloc

使用copy-and-swap和pimpl idiom是一种提供强保证的方式,即把数据成员放到impl类里,操作在impl的副本上做,最后通过不抛异常的swap交换新旧impl类。这样做(需要拷贝)代价很大,需要权衡。

Item 30: Understand the ins and outs of inlining

inline向编译器发出内联申请。内联消除了函数调用开销,但也导致代码体积变大。

如果成员函数在类的内部定义,它被视为隐式添加了inline

虚函数也可以被声明为inline,但只有当不需要动态绑定(即编译期能确定调用的是哪个版本)时才可能真正被内联。

派生类的构造/析构函数哪怕是空函数,也一般不会被内联。因为编译器会生成一堆代码调用父类的构造/析构函数。

函数模板不一定要是内联的。只有确信任何实例化出来的函数都应该被内联,才应该为函数模板添加inline修饰。

(以上为原书内容)


在Modern Cpp中,inline的意义已经超出了它的字面意思。cppreference指出:

An inline function or inline variable(since C++17) has the following properties:

  • The definition of an inline function or variable(since C++17) must be reachable in the translation unit where it is accessed (not necessarily before the point of access).
  • An inline function or variable(since C++17) with external linkage (e.g. not declared static) has the following additional properties:
    • There may be more than one definition of an inline function or variable(since C++17) in the program as long as each definition appears in a different translation unit and (for non-static inline functions and variables(since C++17)) all definitions are identical. For example, an inline function or an inline variable(since C++17) may be defined in a header file that is included in multiple source files.
    • It must be declared inline in every translation unit.
    • It has the same address in every translation unit.

被声明为inline的函数(C++17后也可以是变量)被允许在每个使用到的translation unit中都有一份定义,非inline的函数和变量在整个程序中只能定义一次(one-definiton rule)。工程上,inline允许我们在头文件中定义函数和变量,并被多个源代码文件包含,同时不违反one-definition rule。这也解释了为什么类的成员函数会被隐式inline,因为这允许我们在头文件中定义类的成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
// header.h
int foo() {
return 1;
}

// 1.c
#include "header.h"
int main() {}

// 2.c
#include "header.h"
void bar() {foo();}

以上代码会导致链接时的多重定义,只有给foo()加上inline才能正确链接。

Item 31: Minimize compilation dependencies between files

编写C++程序时,如果做到将接口与类的实现分离,就可以在修改实现时避免重新编译仅依赖接口的源文件。

一个concrete class的例子,它没有很好地做到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <string>
#include "date.h"
#include "address.h"

class Person {
public:
Person(const std::string &name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName;
Data theBirthDate;
Address theAddress;
};

Person的接口部分和实现部分在一起,同时必须#include与私有成员相关的头文件,引入了依赖关系。一旦这些头文件有修改,依赖了这个Person类的任何文件都需要一起修改。

为了解决这个问题,大概有两种思路:

  1. 基于pimpl idiom,此时Person只提供接口,称为handle class。实现部分放到一个PersonImpl中去:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class PersonImpl;
    class Date;
    class Address;

    class Person {
    public:
    Person(const std::string &name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    private:
    std::shared_ptr<PersonImpl> pImpl;
    };

    PersonImpl的接口与Person完全一样,Person的成员函数调用PersonImpl的同名对应函数。

    Person的实现(PersonImplAddressData)的修改不会导致依赖Person的代码重新编译。

    这里我们只需要声明DateAddress,而不需要定义它们。在更复杂一点的场景中,这个Date的声明可能不是class Date,而是一个更复杂的东西(比如是一个typedef BasicDate<int> Date之类的),我们不知道要怎么声明Date。这种情况下,Date的作者应该提供一个类似datefwd.h的头文件,我们只要include进来就行。一个这样的例子是标准库里的<iosfwd>

    基于handle class将接口与实现分离会带来一定性能损失,因为多了一个指针带来了间接访问。

  2. 另一个思路是将Person实现为interface。

    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
    class Person {
    public:
    virtual ~Person();
    virtual string name() const = 0;
    virtual string birth_date() const = 0;
    virtual string address() const = 0;

    static shared_ptr<Person>
    create(const string& name, const string& birth_date, const string& address) {
    return shared_ptr<Person>(new RealPerson(name, birth_date, address));
    }
    };

    class RealPerson : public Person {
    public:
    RealPerson(const string& name, const string& birth_date, const string& address)
    : theName(name), theBirthDate(birth_date), theAddress(address) {}

    virtual string name() const { return theName; }
    virtual string birth_date() const { return theBirthDate; }
    virtual string address() const { return theAddress; }
    private:
    string theName;
    string theBirthDate;
    string theAddress;
    };

    int main() {
    shared_ptr<Person> p = Person::create("John Doe", "1970-01-01", "1234 56th Street");
    cout << p->name() << endl;
    cout << p->birth_date() << endl;
    cout << p->address() << endl;
    }

    Person作为抽象基类定义了接口,RealPerson继承接口提供了实现。

    基于interface将接口与实现分离会带来一定性能损失,因为所有成员函数都是虚函数,带来了查虚函数表的开销。

Inheritance and Object-oriented Design

Item 32: Make sure public inheritance models "is-a"

public继承代表"is-a"的关系。如果Derivedpublic形式继承Base,说明Derived对象同时也是一个Base对象。

"is-a"意味着任何一个能对Base对象进行的操作也能对Derived对象进行,任何一个需要Base对象的地方也可以接受一个Derived对象,而不能简单依靠常识判断,例如:

  1. 如果Bird类支持fly()函数,那企鹅就不是鸟,因为企鹅不会飞。
  2. 如果Rectangle支持修改宽度,并且让宽度不等于长度,那Square就不是Rectangle,因为对正方形不能这么做。

Item 33: Avoid hiding inherited names

不要让派生类中的名称遮挡基类中的名称:

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
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int) { cout << "Base::mf1(int)" << endl; }
virtual void mf2() { cout << "Base::mf2()" << endl; }
void mf3() { cout << "Base::mf3()" << endl; }
void mf3(double) { cout << "Base::mf3(double)" << endl; }
};

class Derived : public Base {
public:
virtual void mf1() { cout << "Derived::mf1()" << endl; }
void mf3() { cout << "Derived::mf3()" << endl; }
};

int main() {
Derived d;
int x;

d.mf1();
d.mf1(x); // Wrong
d.mf2();
d.mf3();
d.mf3(x); // Wrong
}

Base中的mf1()mf3()两个名字被遮挡了,相当于派生类没有继承带参数版本的mf1(int)mf3(double)。对于public继承而言这是不允许的。

Item 34: Differentiate between inheritance of interface and inheritance of implementation

当派生类继承基类时,它一定继承了基类各成员函数的接口。但是否继承成员函数的实现,取决于不同成员函数的声明方式:

  • 纯虚函数:只继承接口,不继承实现。因为派生类必须给出纯虚函数的实现。

    语法上,纯虚函数是可以有实现的。

  • 普通函数(非虚函数):继承接口和实现,并且不允许实现被修改。

  • 虚函数:继承接口和默认实现。继承虚函数存在一种潜在的危险:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class AirPlane {
    public:
    virtual void fly() {
    cout << "AirPlane::fly()" << endl;
    };
    };

    class ModelA : public AirPlane {};
    class ModelB : public AirPlane {};

    ModelAModelB都可以使用通用的fly(),它们直接继承了基类实现并且未作修改。

    但假如有一种新式飞机ModelC,它并不能使用AirPlane::fly()。如果类的编写者忘记重载fly(),就会导致错误。我们希望发生这种情况时能报出一个编译错误。

    一种做法是将接口继承和实现继承分离开来,把AirPlane的虚函数fly()拆成一个纯虚函数fly()和一个非虚函数defaultFly()。派生类必须重载fly()并显式调用defaultFly()来表明它们不仅希望继承接口,还希望继承实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class AirPlane {
    public:
    virtual void fly() = 0;
    protected:
    void defaultFly() {
    cout << "AirPlane::fly()" << endl;
    }
    };

    class ModelA : public AirPlane {
    public:
    void fly() override { defaultFly(); }
    };
    class ModelB : public AirPlane {
    public:
    void fly() override { defaultFly(); }
    };

    这样做的坏处是多引入了一个函数,不利于代码可读性。另一种方案是依赖纯虚函数的定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class AirPlane {
    public:
    virtual void fly() = 0;
    };

    void AirPlane::fly() {
    cout << "AirPlane::fly()" << endl;
    }

    class ModelA : public AirPlane {
    public:
    void fly() override { AirPlane::fly(); }
    };
    class ModelB : public AirPlane {
    public:
    void fly() override { AirPlane::fly(); }
    };

    效果和之前一样,派生类必须重载fly()

Item 35: Consider alternatives to virtual function

以下定义一个GameCharacter类,并定义了一个healthValue()虚函数,因为不同游戏人物可能有不同的计算血量方式。

1
2
3
4
class GameCharacter {
public:
virtual int healthValue() const;
};

本章给出了实现这个需求的几种其他方式。

Non-Virtual Interface(NVI)

该方式实现了Template Method设计模式,healthValue()是一个模板方法,它不允许被派生类重载。派生类只可以重载其中的一个步骤,即doHealthValue()。这样一来,healthValue()中的一些通用步骤(日志,拿锁,验证)就可以在整个继承体系中保留。

1
2
3
4
5
6
7
8
9
10
11
class GameCharacter {
public:
int healthValue() const {
// do-something
int ret = doHealthValue();
// do-something
return ret;
}
private:
virtual int doHealthValue() const;
};

基于函数指针

该方式实现了Strategy设计模式。healthValue()的实现不再是GameCharacter的一部分,而是拆分出去,通过一个函数指针被GameCharacter调用。

1
2
3
4
5
6
7
8
9
10
11
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf) {}
int healthValue() const { return healthFunc(*this); }
private:
HealthCalcFunc healthFunc;
};

这样做的好处是:

  • 同一类型的不同对象可以拥有不同的函数指针,也就是不同的血量计算方式。
  • 血量计算方式在运行时可以变更。

但是,如果healthFunc函数需要通过GameCharacter的私有成员计算血量,这个做法就行不通了。

基于std::function

和上例一样,只是把第5行定义的函数指针改为std::function

1
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

这允许GameCharacter使用任何callable计算血量,例如函数对象,甚至其他类的成员函数(通过std::bind)。

两个继承体系

HealthCalcFunc形成一个自己的继承体系,GameCharacter包含一个指向基类HealthCalcFunc的指针。这是实现Strategy设计模式的经典方式。

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
class GameCharacter;
class HealthCalcFunc {
public:
virtual int calc(const GameCharacter& gc) const {
...
}
};

class SlowHealthCalcFunc : public HealthCalcFunc {
public:
virtual int calc(const GameCharacter& gc) const {
...
}
};

HealthCalcFunc defaultHealthCalc;

class GameCharacter {
explicit GameCharacter(HealthCalcFunc* hcf = &defaultHealthCalc)
: healthFunc(hcf) {}
int healthValue() const {
return healthFunc->calc(*this);
}
private:
HealthCalcFunc* healthFunc;
};

Item 36: Never redefine an inherited non-virtual function

不要重新定义继承来的非虚函数,因为非虚函数不会触发动态绑定。

Item 37: Never redefine a function's inherited default parameter value

虚函数默认参数的值是静态绑定的,在编译时确定。

以下例子会打印出x = 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
virtual void func(int x = 10) {
std::cout << "Base::func with x = " << x << std::endl;
}
};

class Derived : public Base {
public:
virtual void func(int x = 20) override {
std::cout << "Derived::func with x = " << x << std::endl;
}
};

int main() {
Base* b = new Derived();
b->func();

delete b;
return 0;
}

因此不要改变继承来的虚函数的默认参数的值。事实上,虚函数带有默认参数不是一个好的设计,最好通过Non-Virtual Interface的方式写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
void func(int x = 10) {
dofunc(x);
}

private:
// 私有的虚函数,派生类将重写这个函数
virtual void dofunc(int x) {
std::cout << "Base::dofunc with x = " << x << std::endl;
}
};

class Derived : public Base {
private:
// 重写私有的虚函数
virtual void dofunc(int x) override {
std::cout << "Derived::dofunc with x = " << x << std::endl;
}
};

Item 38: Model "has-a" or "is-implemented-in-terms-of" through composition

复合(composition)指一个类包含了另一个类,它可以表示一种"has-a"的关系,如Person之于AddressPhoneNumber

1
2
3
4
5
6
7
8
9
10
class Address {};
class PhoneNumber {};
class Person {
public:
...
private:
string theName;
Address address;
PhoneNumber number;
};

复合还可以表示一种"is-implemented-in-terms-of"的关系,指A是利用B来实现的。这纯粹是一种实现手段,和类之间的逻辑关系无关。如下面的例子,我们基于std::list实现了集合MySet

1
2
3
4
5
6
7
8
9
10
template<class T>
class MySet {
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
size_t size() const;
private:
std::list<T> rep;
};

Item 39: Use private inheritance judiciously

我们知道,public继承代表一种"is-a"的关系,而private继承其实代表了一种"is-implemented-in-terms-of"的关系。如果Derived私有继承了Base,代表Derive只要Base的实现,不要Base的接口。这纯粹是实现细节,和类之间的逻辑关系设计无关。一个例子:

1
2
3
4
5
6
7
8
9
10
class Timer {
public:
explicit Timer(int tick);
virtual void onTick();
};

class Widget: private Timer {
private:
void onTick() override;
};

已有的Timer类每隔tick就会触发一次onTick()。我们的Widget也希望每隔一定秒数触发一次动作,因此我们private继承了Timer,并重新定义了虚函数onTick()。逻辑上,Widget类和Timer类没有任何关系,Widget并不是一个Timer

事实上,私有继承并不是必须的,这个例子也可以通过上一节提到的复合改写成这样:

1
2
3
4
5
6
7
8
9
class Widget {
private:
class WidgetTimer : public Timer {
public:
virtual void onTick() override;
...
};
WidgetTimer timer;
};

这样写甚至比私有继承更好,因为可以把WidgetTimer的定义放在其他地方,这样消除了WidgetTimer类之间的编译依赖。

作者给出了一个私有继承比复合更好的场景:私有继承一个没有non-static成员变量的empty class没有空间开销,但是把这个empty class复合进来至少需要占1个字节:

1
2
3
4
5
6
7
8
9
10
class Empty {};

class Widget1: private Empty {
int a;
};

class Widget2 {
int a;
Empty e;
};

Widget1的大小应该等于int的大小,Widget2则会比int更大。

Item 40: Use multiple inheritance judiciously

使用多重继承会带来一些问题,例如:

  • 从多个父类继承到相同的名字会导致歧义,必须显式通过类名指定在使用哪个名字。

  • 出现菱形继承时,共同父类的成员会被继承两遍。此时两个中层的派生类应该虚继承它们的共同基类。然而:

    1. 虚继承本身有性能开销。

    2. 虚继承出现时,虚基类的初始化由most-derived class负责,如果most-derived 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
      35
      class Base {
      public:
      Base(int value) : value_(value) {
      std::cout << "Base constructor called with value " << value_ << std::endl;
      }

      private:
      int value_;
      };

      class Derived1 : virtual public Base {
      public:
      Derived1(int value) : Base(value) {
      std::cout << "Derived1 constructor called" << std::endl;
      }
      };

      class Derived2 : virtual public Base {
      public:
      Derived2(int value) : Base(value) {
      std::cout << "Derived2 constructor called" << std::endl;
      }
      };

      class MostDerived : public Derived1, public Derived2 {
      public:
      MostDerived(int value) : Base(value), Derived1(value), Derived2(value) {
      std::cout << "MostDerived constructor called" << std::endl;
      }
      };

      int main() {
      MostDerived obj(10);
      return 0;
      }

      这段代码的输出是:

      1
      2
      3
      4
      Base constructor called with value 10
      Derived1 constructor called
      Derived2 constructor called
      MostDerived constructor called

      Derived1Derived2Base构造函数的调用都被忽略,构造函数的实际调用顺序由MostDerived给出。

​ 作者建议少使用虚继承,用的时候最好虚继承一个没有non-static成员的基类,避免初始化带来的心智负担。

作者指出,一种合理的使用多重继承的场景是:一个类C需要public继承一个接口类I,以体现"has-a"的关系;同时,实现C时又需要基于一个CInfo类,因此C需要(当然不是必须,见Item 39)private继承CInfo,体现"is-implemented-in-terms-of"的关系。

Templates and Generic Programming

Item 41: Understand implicit interfaces and compile-time polymorphism

类型和函数模板都是在定义程序的接口,也都提供了多态支持。

  • 类型提供了显式接口,体现在成员函数的签名上;函数模板提供了隐式接口,体现在对泛型对象执行的操作中。例如对泛型对象T& w执行了操作w.size(),就意味着模板实例化时的类型T必须支持size()这一接口。
  • 类型提供了运行期多态,即对虚函数的调用是在运行时动态绑定的;函数模板提供了编译期多态,通过模板实例化确定调用的函数是哪一个版本。函数重载也属于编译期多态。

Item 42: Understand the two meanings of typename

声明模板时可以使用template <typename T>,或者template <class T>,这两种写法完全等价。

typename还有另一个用途:当我们在模板使用一个nested dependent name时,编译器在parse阶段无法确定这个名称究竟是一个类型名还是变量名,它默认认为这是一个变量名。我们通过在名称前加上typename提升编译器这其实是一个类型名。

dependent name: 模板中一个依赖模板参数的名称,如B<T>, T::A

nested name:一个位于某作用域内部(即::后面)的名称,如std::cout

1
2
3
4
5
6
7
int p = 1;

template<typename T>
void foo(const T& container)
{
/* typename */ T::const_iterator * p;
}

这个例子里,编译器在模板实例化之前无法确定const_iterator是一个类型名(声明一个指向迭代器的指针p),还是一个变量名(将该变量与变量p相乘),而默认的行为是后者。如果我们希望的行为是前者,就必须加上typename

一个反例是当基类的名称是dependent name,且出现在base-class list或者构造函数成员初始化列的时候不能加typename,此时名称默认被当作类型名。但该名称出现在其他地方时,仍然需要加typename

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class Base {
public:
class Dummy {
Dummy(int x) {}
};
typedef Dummy Nested;
};

template<typename T>
class Derived: public Base<T>::Nested { // 不能加typename
public:
Derived(int x): Base<T>::Nested(x) { // 不能加typename
typename Base<T>::Nested tmp; // 必须加typename
}
};

除了typename以外,template也被用作一个去除歧义的disambiguator。这是cppreference中的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T>
struct S
{
template<typename U>
void foo() {}
};

template<typename T>
void bar()
{
S<T> s;
s.foo<T>(); // error: < parsed as less than operator
s.template foo<T>(); // OK
}

由于bar是一个模板函数,bar中定义的变量s的类型S<T>是一个dependent name,因此编译器(parser)无法判断foo这个名称的类型。它可能是一个模板,也可能是一个s的成员,正在与T进行小于比较(虽然在这个例子里不合法)。此时必须用template修饰foo消除歧义。

cppreference中介绍了dependent name和disambiguator的更多细节。

Item 43: Know how to access names in templatized base classes

在模板类的派生类中,编译器不会自动在(模板)基类的作用域中查找继承来的名称,如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
class Base {
public:
void foo() {
cout << "Base::foo\n";
}
};

template<typename T>
class Derived : Base<T> {
public:
void bar() {
foo();
}
};

13行对foo()的调用会失败,编译器不会在Base<T>中寻找foo函数。这么做的逻辑是模板实例化之前,不能确保Base<T>中一定还存在着foo()这个名称,如果有这么一个全特化模板,它没有定义foo(),那么Derived<int>就继承不到foo()这个函数:

1
2
template<>
class Base<int> {};

因此,需要显式告知编译器我们需要在模板基类中寻找函数名称(这一告知也同时保证该名称存在,否则会出现编译错误)。一种做法是通过this->调用foo()

1
2
3
4
5
6
7
template<typename T>
class Derived : Base<T> {
public:
void bar() {
this->foo();
}
};

或者通过using告知编译器foo()确实存在于Base<T>中:

1
2
3
4
5
6
7
8
template<typename T>
class Derived : Base<T> {
public:
using Base<T>::foo;
void bar() {
foo();
}
};

Item 44: Factor parameter-independent code out of templates

使用模板函数可能会导致代码膨胀。在下面的例子中,由于我们为SquareMatrix提供了两种实例化,invert()函数也会有两个实例,尽管它们唯一的区别只是维度n的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T, std::size_t n>
class SquareMatrix {
public:
void invert() {}
};

int main() {
auto sm1 = SquareMatrix<double, 2>();
auto sm2 = SquareMatrix<double, 3>();

sm1.invert();
sm2.invert();
}

如果是非模板的场景,我们很可能只实现一次invert(),并给它一个参数n。对于模板场景,我们也希望这么做。我们可以令SquareMatrix继承一个尺寸无关的基类:

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
template<typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase(size_t n, T* pMem) : size(n), pData(pMem) {}
void setDataPtr(T* ptr) { pData = ptr; }
void invert(size_t matrixSize) {
cout << "SquareMatrixBase::invert" << endl;
}
private:
size_t size;
T* pData;
};

template<typename T, size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
private:
using SquareMatrixBase<T>::invert;
public:
SquareMatrix() : SquareMatrixBase<T>(n, data) {}
void invert() {
invert(n);
}
private:
T data[n * n];
};

这里:

  • 由于矩阵数据保存在SquareMatrixSquareMatrixBase需要维护一个指针指向矩阵数据,供矩阵操作函数(如invert(),这里没有体现)使用。
  • SquareMatrix私有继承了SquareMatrixBase,因为这里体现的是"is-implemented-in-terms-of"的关系。
  • SquareMatrix需要显式using基类中的invert()函数(见Item 43)。

Item 45: Use member function templates to accept "all compatible types"

如果我们在实现一个智能指针类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
class mySmartPtr {
public:
mySmartPtr() : ptr_(nullptr) {}
explicit mySmartPtr(T* ptr) : ptr_(ptr) {}
private:
T* ptr_;
};

class Base {};
class Derived : public Base {};

int main() {
Base *b;
Derived *d;
b = d;

mySmartPtr<Derived> spd;
mySmartPtr<Base> spb = spd; // Wrong
}

我们希望像裸指针一样,将指向派生类Derived的指针转换为指向基类Base的指针。但是mySmartPtr<Base>mySmartPtr<Derived>是完全无关的两个类,不存在类型转换,因此第20行的赋值无法编译。

我们不可能穷尽智能指针所可能指向的所有类型,来写出各种构造函数,用于类型转换。因此,我们必须定义一个成员函数模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class mySmartPtr {
public:
mySmartPtr() : ptr_(nullptr) {}
explicit mySmartPtr(T* ptr) : ptr_(ptr) {}

template<typename U>
mySmartPtr(const mySmartPtr<U>& other) : ptr_(other.get()) {
cout << "template copy constructor" << endl;
}

T* get() const { return ptr_; }

private:
T* ptr_;
};

这里,我们通过ptr_(other.get())将底层的裸指针互相转换,它能保证不合法的指针转换(如Base*Derived*)无法编译,使mySmartPtr也表现出相同的效果。

值得注意的是,尽管这里定义的拷贝构造函数模板看似包括了mySmartPtr的默认拷贝构造函数(当TU是同种类型时),但它并不是默认拷贝构造函数,编译器仍然会生成默认拷贝构造函数,因此:

1
2
mySmartPtr<Base> spb1;
mySmartPtr<Base> spb2 = spb1;

这里的构造将会匹配到默认拷贝构造函数(因为这是一个更特化的匹配),而不是这里的拷贝构造函数模板,因此不会打印出输出。

Item 46: Define non-member functions inside templates when type conversions are desired

在Item 24中,我们通过定义一个non-member的operator *,允许Rationalint变量相乘。现在,我们给出一个模板化的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
class Rational {
public:
Rational(const T& n = 0, const T& d = 1) : n_(n), d_(d) {}
T numerator() const { return n_; }
T denominator() const { return d_; }
private:
T n_, d_;
};

template<typename T>
Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

int main() {
Rational<int> r1(3, 5);
Rational<int> r2(4, 7);
Rational<int> r3 = r1 * 2;
}

然而,这段代码(19行)无法编译,因为编译器无法找到一个可供调用的operator *。具体来说,编译器在推导模板实参时不会考虑通过构造函数发生的隐式类型转换,因此无法将int转换为Rational<int>,从而推导出TRational

我们可以在Rational内声明operator*为一个友元函数。

1
2
3
4
5
template<typename T>
class Rational {
...
friend Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
};

现在,代码可以通过编译了。编译器完成operator*调用的逻辑是:

  1. 定义r1时,Rational<int>这个类就已经被实例化了,其中Tint
  2. Rational<int>中声明了一个含有两个Rational<int>形参的函数operator*,这个函数没有用template修饰,不是模板函数。
  3. 调用非模板函数时,编译器会考虑隐式类型转换,并且int确实可以隐式转换成Rational<int>,调用成功。

然而,这段代码现在无法链接,因为operator*只有声明没有定义。

Q:11-14行不是定义了operator*吗?

A:那只是一个从来没有被实例化过的函数模板罢了,我们现在需要的operator*是一个非模板函数,它们根本就不是同一个函数。

我们可以通过以下方式修正这个程序:

  1. 定义一个Rational<int>版本的operator*,这显然不是一个通用的方案,丧失了模板的优势。这里只是用来证明为operator*提供一个(当前实例化下的)定义确实可以成功链接。

    1
    2
    3
    Rational<int> operator*(const Rational<int>& lhs, const Rational<int>& rhs) {
    return Rational<int>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
  2. 直接在声明友元函数时给出定义。

    1
    2
    3
    4
    5
    6
    7
    template<typename T>
    class Rational {
    ...
    friend Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
    return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
    };

    这显然是一个更好的方案。把友元函数在类的内部定义产生的副作用是它会被隐式inline,如果我们担心operator*的函数体过长而不适合inline,可以把实现部分定义在类的外面,友元函数只是调用实现函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    template<typename T>
    class Rational {
    public:
    Rational(const T& n = 0, const T& d = 1) : n_(n), d_(d) {}
    T numerator() const { return n_; }
    T denominator() const { return d_; }

    friend Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
    return do_multiply(lhs, rhs);
    }
    private:
    T n_, d_;
    };

    template<typename T>
    Rational<T> do_multiply(const Rational<T>& lhs, const Rational<T>& rhs) {
    Rational<T> ret;
    // balabala
    return ret;
    }

Item 47: Use traits classes for information about types

C++ 中的trait类是一种用于提取类型信息或执行编译时判定的机制。一个例子是iterator_traits类,它使用户可以在运行时确定迭代器的类型、value_type等信息。

以迭代器类型(input output forward bidirectional random_access)为例,iterator_traits的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename IterT, typename DistT>
void doAdvance(std::input_iterator_tag, IterT& iter, DistT d) {
cout << "input_iterator_tag" << endl;
}

template<typename IterT, typename DistT>
void doAdvance(std::random_access_iterator_tag, IterT& iter, DistT d) {
cout << "random_access_iterator_tag" << endl;
}

template<typename IterT, typename DistT>
void doAdvance(std::bidirectional_iterator_tag, IterT& iter, DistT d) {
cout << "bidirectional_iterator_tag" << endl;
}

template<typename IterT, typename DistT>
void myAdvance(IterT& iter, DistT d) {
doAdvance(typename std::iterator_traits<IterT>::iterator_category(), iter, d);
}

myAdvance需要移动迭代器,但是不同类型迭代器支持的操作不同,myAdvance的实现也应该不同。于是,myAdvance通过iterator_traits在编译期获得迭代器的类型,并根据该类型,通过函数重载(编译期确定)调用对应版本的doAdvance函数。

iterator_traits的实现类似于:

1
2
3
4
5
6
7
8
9
template<typename IterT>
struct my_iterator_traits {
typedef typename IterT::iterator_category iterator_category;
};

template<typename IterT>
struct my_iterator_traits<IterT*> {
typedef typename std::random_access_iterator_tag iterator_category;
};

迭代器的实现者需要在类型定义时typedefiterator_category这个类型名,iterator_traits会直接使用这个类型。由于指针也是一种迭代器,而基本类型里没有办法typedefiterator_traits还有一个偏特化的版本用于处理指针,并将指针标为随机访问迭代器。

另一个通过偏特化实现的traits例子是is_same

1
2
3
4
5
6
7
8
9
template <typename T, typename U>
struct my_is_same {
static constexpr bool value = false;
};

template <typename T>
struct my_is_same<T, T> {
static constexpr bool value = true;
};

Item 48: Be aware of template metaprogramming

本节介绍了模板元编程的基本概念,给出了一个计算阶乘的Hello-world示例:

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

template<>
struct Factorial<0> {
static const unsigned value = 1;
};

int main() {
cout << Factorial<5>::value << endl; // 120
}

Customizing new and delete

Item 49: Understand the behavior of the new-handler

operator new有一个nothrow版本,签名如下:

1
void* operator new  ( std::size_t count, const std::nothrow_t& tag );

用户通过以下方式调用nothrow版本的new,它不会抛出bad_alloc异常,而是在分配失败时返回空指针:

1
int *p = new (std::nothrow) int;

但是,对于非基本类型,new和构造函数往往是一起被调用的,就算分配失败时new不抛异常,构造函数也很可能抛异常。所以不要因为用了nothrow版本的new就忽略异常处理。

operator new无法满足内存分配请求时,它会调用一个new_handler函数,然后再次尝试分配,重复此过程直到分配成功或抛出异常为止。

<new>头文件中定义了new_handler相关的基础设施:

1
2
3
4
5
6
7
8
9
10
11
12
  /** If you write your own error handler to be called by @c new, it must
* be of this type. */
typedef void (*new_handler)();

/// Takes a replacement handler as the argument, returns the
/// previous handler.
new_handler set_new_handler(new_handler) throw();

#if __cplusplus >= 201103L
/// Return the current new handler.
new_handler get_new_handler() noexcept;
#endif

set_new_handler设置一个自定义的new_handler并返回旧的那个,get_new_handler返回当前的new_handler

如果我们希望在分配不同类型的对象时,使用不同的new_handler处理失败,可以采取以下的方式:

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
class NewHandlerHolder {
public:
explicit NewHandlerHolder(new_handler nh) : handler(nh) {}
~NewHandlerHolder() { std::set_new_handler(handler); }
private:
new_handler handler;
NewHandlerHolder(const NewHandlerHolder&) = delete;
NewHandlerHolder& operator=(const NewHandlerHolder&) = delete;
};

class Widget {
public:
static new_handler set_new_handler(new_handler p) {
new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
static void* operator new(size_t size) {
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
private:
static new_handler currentHandler;
};

std::new_handler Widget::currentHandler = 0;

这里NewHandlerHolder是一个RAII类,它管理的资源是global版本的new_handler

用户通过Widget::set_handler指定专供new Widget使用的new_handler,这个局部new_handler会在Widget::new每次被调用时被设置,并在调用结束后恢复成全局new_handler

1
2
3
4
5
6
7
8
void outOfMem() {}

int main() {
Widget::set_new_handler(outOfMem);
Widget* pw1 = new Widget; // calls outOfMem if memory request fails
std::string* ps = new std::string; // uses std::set_new_handler
Widget* pw2 = new Widget; // calls outOfMem if memory request fails
}

我们注意到,这种为不同类型提供自定义new_handler的方式是通用的,对Widget是这样,对其他类也是这样。因此,我们可以通过模板复用这个方式:

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
class NewHandlerHolder {
public:
explicit NewHandlerHolder(new_handler nh) : handler(nh) {}
~NewHandlerHolder() { std::set_new_handler(handler); }
private:
new_handler handler;
NewHandlerHolder(const NewHandlerHolder&) = delete;
NewHandlerHolder& operator=(const NewHandlerHolder&) = delete;
};

template <typename T>
class NewHandlerSupport {
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size) noexcept(false);
private:
static std::new_handler currentHandler;
};

template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) noexcept {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}

template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) noexcept(false) {
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}

class Widget : public NewHandlerSupport<Widget> {};

NewHandlerHolder的基础上,我们定义了一个模板类NewHandlerSupport,它定义了自己的set_new_handlernew,从而使我们不需要像之前那样侵入式地修改Widget类。现在,Widget只需要public继承NewHandlerSupport<Widget>获得这两个接口即可。

注意这里NewHandlerSupport虽然是模板类,但内部实现完全与模板参数T无关。这里T的唯一作用就是让NewHandlerSupport的不同派生类拥有各自的基类,即NewHandlerSupport<T>。这也是为什么Widget要继承NewHandlerSupport<Widget>,把自己的类名当作模板参数T。这种继承方式被称为CRTP(Curiously Recurring Template Pattern)。

Item 50: Understand when it makes sense to replace new and delete

本节泛泛而谈了一些需要自定义newdelete的理由:

  • 针对应用的内存分配pattern和使用场景,实现特化的分配器以提升分配性能,或者降低内存分配器的内存占用。
  • 收集内存使用的统计信息。
  • 检测内存分配错误,例如new的时候写入canary值,delete时检查。
  • 实现一些非传统行为,例如用newdelete封装C语言共享内存API。
  • 实现自定义的内存对齐需求。C++17已经提供了接受alignment版本的newdelete

Item 51: Adhere to convention when writing new and delete

实现自定义版本的newdelete时,应该让它们表现出与默认版本类似的行为,例如:

  • 每当分配失败时,调用new_handler(如果存在)并重试,直到分配成功为止。
  • 将申请分配0字节的请求视为分配1字节,因为C++标准通常规定分配器应该返回一个指向足够大的内存块的指针,使得在这个内存块上进行存储是安全的。

一个典型的operator new的逻辑类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void *operator new(size_t size) {
if (size == 0) {
size = 1;
}

while (true) {
void *p = malloc(size);
if (p) {
return p;
}

new_handler globalHandler = get_new_handler();

if (globalHandler) {
(*globalHandler)();
} else {
throw bad_alloc();
}
}
}

有时,我们提供给基类的new就是为基类设计的,不想给派生类使用,此时可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base {
public:
void *operator new(std::size_t size) {
if (size != sizeof(Base)) {
return ::operator new(size);
}
return malloc(size);
}
private:
int a;
};

class Derived : public Base {
private:
int b;
};

int main() {
Derived *d = new Derived; // custom new
Base *b = new Base; // ::operator new
return 0;
}

通过将sizesizeof(Base)比较,得知分配的是否为基类对象。

对于delete,我们应该保证delete一个空指针是安全的:

1
2
3
4
5
void operator delete(void *p) noexcept {
if (p == nullptr) {
return;
...
}

如果像上面的例子一样,我们为基类和派生类调用了不同版本的new函数,delete也要相应处理这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
...
void operator delete(void *p, std::size_t size) {
if (size != sizeof(Base)) {
::operator delete(p);
return;
}
free(p);
}
...
};

Item 52: Write placement delete if you write placement new

如下所示,我们定义了一个Widget类并提供了自定义的new(),它除了size_t外还接受一个string作为参数。如果我们不想让局部定义的名称new遮挡住标准库里的::new的话,就应该同时定义new(size_t size)这个重载,它调用:: operator newdelete()同理。

事实上,标准库里的operator new有多个重载版本,这里只考虑了new(size_t size)这一个版本。我们可以考虑声明一个StandardNewDeleteForms类,把标准库里所有的newdelete都包装一层,然后让Widget public继承StandardNewDeleteFormsusing operator newoperator delete这两个名称,这样就不会有遮挡全局名称的问题了。

现在的问题是:Widget *pw1 = new Widget意味着调用了两个函数:new()Widget的构造函数。如果new()成功了但是构造函数抛出异常,编译器生成的代码需要保证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
class Widget {
public:
static void* operator new(size_t size) {
return ::operator new(size);
}

static void* operator new(size_t size, string str) {
cout << "Widget::operator new " << str << endl;
return ::operator new(size);
}

static void operator delete(void* pMemory) {
::operator delete(pMemory);
}

static void operator delete(void* pMemory, string str) {
cout << "Widget::operator delete " << str << endl;
::operator delete(pMemory);
}

Widget() {
throw runtime_error("rtErr");
}
};

int main() {
try {
Widget *pw1 = new ("HaHa") Widget;
delete pw1;
} catch (const exception& e) {
cout << e.what() << endl;
}
}

但是,编译器怎么知道需要被“撤销”的那个new()对应的delete()函数是哪个呢?答案是,它会寻找额外参数个数和类型都与new()完全相同的那个delete(),并使用与调用new()时相同的额外参数调用它。如果找不到,它就不会调用delete()造成,内存泄漏。

额外参数指的是除了size_t(对于new())或者void *(对于delete())以外的其他参数,拥有额外参数的new()被称为placement new,拥有额外参数的delete()被称为placement delete。

当额外参数是一个void *时,这个new()是一个狭义上的placement new,用于在一个指定的内存位置上创建对象。

因此,当我们自定义了一个placement new时,不能忘记同时定义对应的placement delete,否则就有内存泄漏的风险。

需要注意的是,29行的delete pw1如果被执行,调用的仍然是non-placement版本的delete(因为我们没有提供string参数)。上述的placement new/delete对应规则仅在构造函数抛出异常时触发。