Effective C++学习笔记

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

C++语言可以看成由四个部分组成:

  1. C
  2. 面向对象部分
  3. 模版,泛型编程
  4. STL

各个部分都有各自的高效编程策略。

请记住:

  • C++高效编程守则视状况而变化,取决于你使用C++的哪一部分

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

宏定义的常量,如果产生编译错误,错误信息只会指向宏的值,不会指向宏的名字,同时符号表中不存在宏,不容易定位问题。

用const替换宏常量需要注意两个问题:一是常量指针问题,由于常量定义一般都放在头文件中,所以指针需要被定义成const,在一个头文件定义个常量char*类型,需要两次const

const char * const authorName = "Scott Meyers";

当然可以用string

const std::string authorName("Scott Meyers"); 

二是类当中的常量问题,为了让这个常量范围限制在类里面,可以定义成一个static变量

class GamePlayer {
private:
static const int NumTurns = 5;// constant declaration
int scores[NumTurns];// use of constant
...
};

当然,更好的办法是定义成enum

class GamePlayer {
private:
enum { NumTurns = 5 };
// “the enum hack” — makes 
// NumTurns a symbolic name for 5
int scores[NumTurns];
// fine
...
};

如果你不想让别人获取到类里某个整数常量值的地址或引用,用enum是一个好的选择,因为const定义的值还是能获取到其地址的,是存在符号表中的。

#define定义的函数可以采用模板内联函数来实现。

请记住:

  1. 对于一些简单的常量,用const或者enum替代#define
  2. 对于函数宏,用inline函数替换#define

条款3:尽可能使用const

char greeting[] = "Hello"; 
char *p = greeting;// non-const pointer, 
                   // non-const data
const char *p = greeting;// non-const pointer,
                         // const data
char * const p = greeting;// const pointer,
                          // non-const data
const char * const p = greeting;// const pointer,
                                // const data

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

const成员函数

将const实施于成员函数的目的,是为了确认该函数可作用于const对象身上。首先,它们使class接口比较容易被理解,可以知道哪个函数可以改动对象而哪个函数不能;其次,它们使操作const对象成为可能。

两个成员函数如果只是常量性不同,可以被重载。

class TextBlock {
public:
...
const char& operator[](std::size_t position) const  // operator[] for
{ return text[position]; }                        // const objects
char& operator[](std::size_t position)            // operator[] for
{ return text[position]; }                        // non-const objects
private:
 std::string text;
};
TextBlock tb("Hello");
std::cout << tb[0];                          // calls non-const
                                              // TextBlock::operator[]
const TextBlock ctb("World");
std::cout << ctb[0];                       // calls const TextBlock::operator[]

std::cout << tb[0];           // fine — reading a 
                              // non-const TextBlock 
tb[0] = ’x’;                  // fine — writing a 
                              // non-const TextBlock
std::cout << ctb[0];          // fine — reading a 
                              // const TextBlock 
ctb[0] = ’x’;                 // error! — writing a 
                              // const TextBlock 

成员函数是const意味着什么,有两种概念:bitwise constness(又称physical constness)和logical constness。

bitwise constness意味着成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。bitwise constness正是C++对常量性的定义,因此const成员函数不可以更改对象内任何非static成员变量。
然而许多函数不具备const性质也能通过bitwise测试:

class CTextBlock {
public:
    ...
    char& operator[](std::size_t position) const   // inappropriate (but bitwise
    { return pText[position]; }                   // const) declaration of
                                                  // operator[]
    private:
    char *pText;
};

这个函数声明为const的本意为不应修改pText,然而其返回一个引用,我们却可以通过其返回的引用修改pText,然而编译器却无法检测出这样的问题

const CTextBlock cctb("Hello");   // declare constant object
char *pc = &cctb[0];              // call the const operator[] to get a
                                  // pointer to cctb’s data 
*pc = ’J’;                       // cctb now has the value “Jello”

这就导出了logical constness,这种主张认为一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端检测不出的情况下才得如此。

class CTextBlock {
public:
    ...
    std::size_t length() const;
    private:
    char *pText;
    std::size_t textLength;// last calculated length of textblock
    bool lengthIsValid;   // whether length is currently valid
};
std::size_t CTextBlock::length() const
{
    if (!lengthIsValid) {
        textLength = std::strlen(pText);// error! can’t assign to textLength
        lengthIsValid = true;           // and lengthIsValid in a const 
    }                                   // member function
    return textLength;
}

length的实现不是bitwise constness,所以上面的代码编译器不过通过,然而这两笔数据的修改对const CTextBlock对象而言却是可以接受的,解决办法就是应用mutable,mutable释放掉non-static成员变量的bitwise constness约束:

class CTextBlock {
public:
    ...
    std::size_t length() const;
    private:
    char *pText;
    mutable std::size_t textLength;// last calculated length of textblock
    mutable bool lengthIsValid;   // whether length is currently valid
};
std::size_t CTextBlock::length() const
{
    if (!lengthIsValid) {
        textLength = std::strlen(pText);// error! can’t assign to textLength
        lengthIsValid = true;           // and lengthIsValid in a const 
    }                                   // member function
    return textLength;
}

在const和non-const成员函数中避免重复

看下面的代码

class TextBlock {
public:
    ...
    const char& operator[](std::size_t position) const
    {
        ...   // do bounds checking
        ...   // log access data
        ...   // verify data integrity
        return text[position];
    }
    char& operator[](std::size_t position)  
    {
        ...  // do bounds checking
        ...  // log access data
        ...  // verify data integrity
        return text[position];
    }
private:
    std::string text;
};

上面的代码实现了operator[]的const和non-const两个版本,然而这两个函数的代码完全一样,这就造成了代码重复的问题;这种情况可以通过类型转换来解决,下面的代码const版本同上,non-const版本实现如下:

char& operator[](std::size_t position)  // now just calls const op[]
{
    return const_cast<char&>(             // cast away const on
                                         // op[]’s return type;
    static_cast<const TextBlock&>(*this) // add const to *this’s type;
    [position]
                                         // call const version of op[]
    );
}

这里,做了两次类型转换,如果才non-cosnt版本内只是单纯的调用operator[],会递归调用自己。为了避免递归,首先将*this从原始的TextBlock&转换为const TextBlock&,这样,它就会调用const版本,第二次转换则是将const版本返回值的const转换为non-const。

请记住:

  • 将某些东西声明为const可帮助编译器检测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体;
  • 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”;
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可以避免代码重复。

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

请记住:

  • 为内置对象进行手工初始化,因为C++不保证初始化它们;
  • 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作,初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同;
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

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

对一个类,如果没有直接的声明,编译器会默认为其生成默认的构造函数,析构函数,拷贝构造函数和copy assignment操作。所有这些函数都是public并且inline。例如定义一个类

class Empty{};

实质上,与下面是一致的

class Empty {
public:
    Empty() { ... }    // default constructor
    Empty(const Empty& rhs) { ... }  // copy constructor
    ~Empty() { ... }   // destructor — see below
                       // for whether it’s virtual
    Empty& operator=(const Empty& rhs) { ... } // copy assignment operator
};

这些函数只有在被调用时才会生成,下面的代码就会导致相应的函数生成

Empty e1;      // default constructor;
               // destructor
Empty e2(e1);  // copy constructor 
e2 = e1;       // copy assignment operator 

编译器生成的析构函数不是虚函数,除非这个类是从定义了的虚析构函数的基类继承而来。

对于编译器默认生成的拷贝构造函数和copy assignment操作,只是简单的将类的非静态成员拷贝到目标对象。例如下面定义的这个类:

template<typename T>
class NamedObject {
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
...
private:
std::string nameValue;
T objectValue;
};

由于这个类已经定义了构造函数,所以编译器不会再为它生成默认的构造函数,但编译器会为其生成默认的拷贝构造函数和copy assignment操作,如下代码就会调用默认拷贝构造函数。

NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1);  // calls copy constructor

同样,编译器也会为其生成类似的copy assignment操作,但是一般而言,只有生出的代码合法且有意义,编译器才会为其生成operator=,否则,编译器会拒绝。考虑下面这个类,其nameValue为一个string的引用,objectValue为const T:

template<typename T>
class NamedObject {
    public:
    // this ctor no longer takes a const name, because nameValue
    // is now a reference-to-non-const string. The char* constructor
    // is gone, because we must have a string to refer to.
    NamedObject(std::string& name, const T& value);
    ...
    // as above, assume no
    // operator= is declared
    private:
    std::string& nameValue; // this is now a reference
    const T objectValue;   // this is now const
};

考虑如下代码会发生什么情况

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);// when I originally wrote this, our
                             // dog Persephone was about to
                             // have her second birthday
NamedObject<int> s(oldDog, 36);  // the family dog Satch (from my
                                  // childhood) would be 36 if she
                                  // were still alive
p = s;                            // what should happen to 
                                  // the data members in p?

赋值之前,p.nameValue和s.nameValue各自指向不同的string对象,赋值之后,p.nameValue应该指向s.nameValue所指的那个string,如果这样,就违背了C++不允许“让引用改指向不同的对象”的原则。因此,编译器就会拒绝这个赋值操作。面对内含const的成员(本例的objectValue),编译器的反应也一样,因为更改const成员是不合法的。还有一种情况,如果某个基类将operator=声明为private,那么编译器将拒绝为其子类生成operator=。

请记住:

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

条款6:若不想使用编译器自动生成的函数,就该明确拒绝

有些情况下,我们不能为一些类的对象做拷贝,例如中介的卖房子系统,待卖的房子作为一个类

class HomeForSale { ... };

由于每一个房子都是独一无二的,因此为HomeForSale做拷贝是毫无道理的,因此我们希望下面的代码中的拷贝编译器能够报错

HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1); // attempt to copy h1 — should 
                    // not compile!
h1 = h2;            // attempt to copy h2 — should
                    // not compile!

解决这个问题的关键在,编译器生成的函数都是public的,为了防止编译器生成这些函数,我们需要自己定义这些函数,但不能声明为public,相反,我们将其声明为private,这样,编译器就不会再生成这些函数,同时,别的地方也不能调用这些函数。

但是这种情况下,类的成员函数和友元函数还是能够调用私有函数,除非,将这些函数定义了但不去实现它们,这样,如果别处有调用的话,在link的时候就会出错。

将成员函数声明为private而且故意不实现它们,这种技巧在C++ iostream库中能够看到被用来阻止copying行为,标准库中ios_base,basic_ios和sentry,其拷贝构造函数和operator=都被声明为private而且没有实现。

class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&);
                                     // declarations only
HomeForSale& operator=(const HomeForSale&);
};

这样定义之后,成员函数或者友元函数在调用该函数时,在link的时候就会出错。但是我们更希望能够在编译的时候出错。要做到这种效果,可以专门设计一个用于阻止copying动作的基类,这个基类声明了private的拷贝构造函数和operator=,然后让我们的类继承自这个基类

class Uncopyable {
protected:
// allow construction
Uncopyable() {}
// and destruction of
~Uncopyable() {}
// derived objects...
private:
Uncopyable(const Uncopyable&);
// ...but prevent copying
Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale: private Uncopyable {
// class no longer
...
// declares copy ctor or
};

这种方法是可行的,因为只要任何地方尝试拷贝HomeForSale对象,编译器便试着生成一个拷贝构造函数和一个operator=,这些函数会尝试调用其基类的对应函数,那些调用编译器会拒绝,因为其基类的拷贝函数都是private。

请记住:

  • 为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像uncopyable这样的baseclass也是一种做法。

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

来看这样一个例子,我们有许多时间记录的方式,所以我们会很自然的设计一个TimeKeeper的基类以及派生于它的一些子类作为不同的计时方法。

class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };

客户端如果需要在程序中使用时间,我们就可以设计工厂函数,返回一个指向计时对象的指针。工厂函数会返回一个基类指针,指向新生成的子类:

TimeKeeper* getTimeKeeper();

为了保持工厂函数的转换机制,工厂函数返回的指针是在堆上,因此为了防止内存泄漏,每个返回的指针都需要delete

TimeKeeper *ptk = getTimeKeeper();
...
delete ptk;

这里引出了一个问题,就是getTimeKeeper返回的指针是指向一个子类对象,而那个对象需要通过一个基类指针来释放,然而这个基类(TimeKeeper)的析构函数不是虚拟析构函数。这是一种灾难,因为C++明确说明了,当一个子类对象通过一个不含虚拟析构函数的基类指针来释放的话,结果是未定义的。实际执行时通常发生的是对象的子类部分没有被销毁。如果getTimeKeeper返回指针指向一个AtomicClock对象,其内的AtomicClock部分很可能没被销毁,而AtomicClock的析构函数也未能执行起来。然而其基类部分通常会被销毁,于是造成一个诡异的“局部销毁”对象。

解决这个问你的方法很简单,给基类一个virtual的析构函数。

class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk;  // now behaves correctly

任何一个拥有virtual函数的类都应该要有一个virtual的析构函数。

一般情况下,如果一个类没有virtual析构函数,那么就意味着这个类没有打算作为一个基类。同样,如果一个类不作为基类,但却定义了一个virtual析构函数,这种情况也会有问题,考虑下面这个表示二维空间的类

class Point {
// a 2D point
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};

如果int占用32bit,那么Point对象可塞入一个64bit的缓存器中。这样一个Point对象也可被当作一个“64bit量”传给其他语言写的函数。然而如果Point的析构函数是virtual,情况就不一样了。

欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期间决定哪一个virtual函数该被调用。这份信息是由一个所谓的vptr(virtual table pointer)指针指出,vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个对应的vtbl,当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。

这样,Point对象的体积就会增加,在32bit计算机中将占用64bits(为了存放两个int)至96bits(两个int加上vptr)。Point对象不再能够塞入一个64bit缓存器。

因此,无端的将所有class的析构函数声明为virtual,就像从未声明他们为virtual一样,都是错误的。

值得注意的是,标准库中string和容器类都不含virtual析构函数,如果在编程中企图去继承string或者标准容器类或任何“带有non-virtual析构函数”的类,都会遇到问题。

“给基类一个virtual析构函数”,这个规则只适用于带多态性质的基类身上,这种基类的设计目的是为了用来“通过基类接口处理子类对象”。

请记住:

  • 带多态性质的基类应该声明一个virtual析构函数,如果class带有任何的virtual函数,它就应该拥有一个virtual析构函数。

  • class的设计目的如果不是作为基类使用,或不是为了具备多态性质,就不该声明为virtual析构函数。

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

C++并不禁止析构函数吐出异常,但它不鼓励你这么做。

如果有异常,有两个办法可以解决这个问题:

(1)如果抛出异常就结束程序,通常通过调用abort完成;

(2)抛出异常,但不做处理

请记住:

  1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

  2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。

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

在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class,若使用运行期类型信息(runtime type information,例如dynamic_type和typeid),也会把对象视为base class类型。对象在derived class构造函数开始执行之前不会成为一个derived class对象。

析构函数也一样,一旦derived class析构函数开始执行,对象内的derived class成员变量便呈未定义值,进入base class析构函数后对象就成为一个base class对象。

请记住:

在构造和析构函数期间不要调用虚函数,因为这类调用从不下降至派生类。

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

对于赋值操作符,我们常常要达到这种类似效果,即连续赋值:

  int x, y, z;
  x = y = z = 15;

为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。

即:

    Widget & operator = (const Widget &rhs)
    {
        ...
        return *this;
    }

所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。

请记住:

令赋值操作符返回一个reference to *this。

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

在实现一个operator=时,要特别注意自己赋值给自己的情况,下面是一份不安全的operator=版本

Widget&
Widget::operator=(const Widget& rhs)  // unsafe impl. of operator=
{
    delete pb;   // stop using current bitmap
    pb = new Bitmap(*rhs.pb);// start using a copy of rhs’s bitmap
    return *this;            // see Item 10
}

如果rhs是自身的话,会导致在分配空间之前就将自身释放了。这个问题可以有几个解决方法:

(1)证同测试

Widget& Widget::operator=(const Widget& rhs)
{
    if (this == &rhs) return *this;// identity test: if a self-assignment,
                                   // do nothing
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}

(2)精心安排语句顺序

Widget& Widget::operator=(const Widget& rhs)
{
    Bitmap *pOrig = pb;// remember original pb
    pb = new Bitmap(*rhs.pb);// point pb to a copy of rhs’s bitmap
    delete pOrig;// delete the original pb
    return *this;
}

(3)copy-and-swap技术

class Widget {
    ...
    void swap(Widget& rhs);// exchange *this’s and rhs’s data;
    ...                   // see Item 29 for details
};
Widget& Widget::operator=(const Widget& rhs)
{
    Widget temp(rhs);// make a copy of rhs’s data
    swap(temp);// swap *this’s data with the copy’s
    return *this;
}

请记住:

  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap;
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

copying构造函数和operator=都输于copying函数

请记住:

  • copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”;
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用

条款13:以对象管理资源

请记住:

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源;
  • 两个常被使用的RAII class分别为shared_ptr和auto_ptr。(C++11中已引入shared_ptr,并建议使用shared_ptr)

条款14:在资源管理类中小心coping行为

有时候,智能指针并不能满足我们的需求,需要我们自己建立资源管理类,比如我们用C API函数处理Mutex的互斥器对象,共有lock和unlock两函数可用:

void lock(Mutex *pm);
void unlock(Mutex *pm);

我们用一个class来管理机锁,这样的class的基本结构需遵循RAII准则,即“资源在构造期间获得,在析构期间释放”。

class Lock {
public:
    explicit Lock(Mutex *pm)
    : mutexPtr(pm)
    { lock(mutexPtr); }
    ~Lock() { unlock(mutexPtr); }
private:
    Mutex *mutexPtr;
};

这样客户端正常调用没有问题,然而如果Lock对象被复制呢

Lock ml1(&m); // lock m
Lock ml2(ml1);

这就面临这一个问题,当一个RAII对象被复制,会发生什么?一般选择以下几种可能:

(1)禁止复制

这时可以按照item6说的,将copying操作声明为private

(2)对底层资源使用“引用计数法”

这时可以使用share_ptr允许指定“删除器”的性质

class Lock {
public:
    explicit Lock(Mutex *pm)   // init shared_ptr with the Mutex
    : mutexPtr(pm, unlock)     // to point to and the unlock func
    {                          // as the deleter
        lock(mutexPtr.get());        // see Item 15 for info on “get” 
    }
private:
    std::tr1::shared_ptr<Mutex> mutexPtr; // use shared_ptr
};                                        // instead of raw pointer

(3)复制底部资源

这中情况下复制资源管理对象,应该同时也复制其所包覆的资源。也就是说,复制资源管理对象时,进行的是“深度拷贝”

(4)转移底部资源的拥有权

某些场合下需要永远只有一个RAII对象指向一个raw resource,即使RAII对象被复制依然如此。此时资源的拥有权会从被复制物转移到目标物。

请记住:

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的coping行为决定RAII对象的copying行为;
  • 普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也可能被实现。

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

请记住:

  • APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法;
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

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

使用了new,就要对应了delete,使用了new[],就必须对应使用delete[]。

当使用new时,有两件事发生:第一,内存被分配出来,第二,针对此内存会有一个(或更多)构造函数被调用。当使用delete,也有两件事发生,针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete的函数)。

请记住:

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

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

考虑如下函数声明和调用

int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

对processWidget函数,在该函数调用前,必须首先核算出被传递的各个实参。其中“std::tr1::shared_ptr(new Widget)”包含了两部分:
(1)执行new Widget

(2)调用shared_ptr构造函数

对processWidget函数,在其调用前,必须先做如下三件事:
(1)调用priority()

(2)执行new Widget

(3)调用shared_ptr构造函数

由于C++以什么样的顺序完成这些事情并没有明确的规定,所以上述的执行顺序有可能会是如下情况:

(1)执行new Widget

(2)调用priority()

(3)调用shared_ptr构造函数

这种情况下,如果在执行priority()过程中发生了异常,new Widget返回的指针将会遗失,因为它还未置入shared_ptr内,就可能引发资源泄漏。

解决办法就是分离语句,在单独语句内存储智能指针,避免在分配资源和构造智能指针的过程中插入其他操作

std::tr1::shared_ptr<Widget> pw(new Widget); // store newed object
                                   // in a smart pointer in a
                                  // standalone statement
processWidget(pw, priority());    // this call won’t leak

请记住:

以独立语句将newed对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

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

欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。

预防客户错误的另一个办法是,限制类型内什么事可做,什么事不可做,常见的限制是加上const。

任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事。

例如这样的接口,

Investment* createInvestment();  

就要求客户自己能去使用智能指针,但是如果客户忘了使用呢,因此接口可以改成返回一个智能指针

std::tr1::shared_ptr<Investment> createInvestment();

假设class设计者期许那些“从createInvestment取得Investment*指针”的客户将该指针传递给一个名为getRidOfInvestment的函数,而不是直接在它身上动刀(delete)。这样一个接口又开启通往另一个客户错误的大门,该错误是“企图使用错误的资源析构机制”(也就是拿delete替换getRidOfInvestment)。createInvestment的设计者可以针对此问题先发制人:返回一个“将getRidOfInvestment绑定为删除器(deleter)”的shared_ptr。

shared_ptr提供了某个构造函数可以设定默认的删除器(deleter)

std::tr1::shared_ptr<Investment>  // create a null shared_ptr with 
pInv( static_cast<Investment*>(0),// getRidOfInvestment as its
getRidOfInvestment);                // deleter; 

std::tr1::shared_ptr<Investment> createInvestment()
{
    std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
    getRidOfInvestment);
    ...
    // make retVal point to the 
    // correct object
    return retVal;
}

shared_ptr还有一个好处,它不存在“cross-DLL problem”,这个问题发生于“对象在DLL中被new创建,却在另一个DLL内被delete销毁”。shared_ptr没有这个问题,因为它缺省的删除器是来自“shared_ptr诞生所在的那个DLL”的delete。

请记住:

  1. 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质;
  2. “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容;
  3. “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任;
  4. shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。

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

设计一个高效的class需要考虑以下问题:

  1. 新type的对象应该如何被创建和销毁?——构造函数和析构函数以及内存分配函数和释放函数的设计;
  2. 对象的初始化和对象的赋值该有什么样的差异?——构造函数和赋值操作符的行为
  3. 新的type的对象如果被passed by value,意味着什么?——copy构造函数
  4. 什么是新type的“合法值”?
  5. 你的新type需要配合某个继承图系吗?
  6. 你的新type需要什么样的转换?
  7. 什么样的操作符和函数对此新type而言是合理的?
  8. 什么样的标准函数应该驳回?——应该声明为private的
  9. 谁该取用新type的成员?
  10. 什么是新type的“未声明接口”?
  11. 你的新type有多么一般化?——是否应该定义为template
  12. 你真的需要一个新type吗?

请记住:

  • Class的设计就是type的设计。在定义一个新的type之前,请确定你已经考虑过本条款覆盖的所有讨论主题

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

采用引用传递对象可以避免大量的构造和析构;C++编译器底层,reference往往以指针实现出来,因此pass by reference通常意味着真正传递的是指针。因此对于内置类型,pass by value会比pass by reference更高效。

请记住:

  1. 尽量以pass-by-reference-to-const替换pass-by-value,前者更加高效,而且还可以避免切割问题;
  2. 以上规则不适用于内置类型,STL迭代器和函数对象,对于他们pass by value更高效。

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

考虑一个用以表现有理数的class,内含一个函数用来计算两个有理数的乘积:

class Rational {
public: 
Rational(int numerator = 0,    // see Item 24 for why this
int denominator = 1);    // ctor isn’t declared explicit
...
private:
int n, d; // numerator and denominator
friend
const Rational                 // see Item 3 for why the
operator*(const Rational& lhs, // return type is const
const Rational& rhs);
};

这个版本的operator*以by value的方式返回其结果;可以将其返回结果改成reference吗?

我们应该记住,所谓reference只是个名称,代表某个既有对象。任何时候看到一个reference声明式,都应该立刻问自己它的另一个名称是什么?因为它一定是某物的另一个名称。

我们当然不可能期望这样一个(内含乘积的)Rational对象在调用operator*之前就存在。因此,如果operator *要返回一个reference指向如此数值,它必须自己创建那个Rational对象。

我们尝试定义一个局部变量

const Rational& operator*(const Rational& lhs,  //warning! bad code!
const Rational& rhs)
{
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
}

这样存在两个问题:首先,采用reference的首要目的是避免构造函数的调用,但是这里的result调用的构造函数;其次,也是最为致命的是,result在这里是一个局部变量,局部变量在函数退出前就被销毁了。

于是,考虑在堆上构造一个对象,并返回reference指向它。

const Rational& operator*(const Rational& lhs,  // warning! more bad
const Rational& rhs)      // code!
{
    Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    return *result;
}

这样还是会有一次构造函数调用,同时又用了new,如何去delete呢,特别是一个语句内调用了两次operator*,也需要两次delete,但是却没有合理的办法让使用者进行那些delete调用,从而导致资源泄漏。

让operator*返回的reference指向一个被定义在函数内部的static Rational对象呢?

const Rational& operator*(const Rational& lhs,  // warning! yet more
const Rational& rhs) // bad code!
{
    static Rational result; // static object to which a 
                            // reference will be returned
    result = ... ;          // multiply lhs by rhs and put the
                            // product inside result
    return result;
}

考虑下面完全合理的客户代码

bool operator==(const Rational& lhs,   // an operator==
const Rational& rhs);
                                       // for Rationals
Rational a, b, c, d;
...
if ((a * b) == (c * d))  {
do whatever’s appropriate when the products are equal;
} else  {
do whatever’s appropriate when they’re not;
}

猜想结果会怎样?(a * b) == (c * d)总是为true,不论a,b,c,d的值是什么!因为它们返回的reference永远指向那个static Rational对象。

一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象。

inline const Rational operator*(const Rational& lhs, const Rational& rhs) 
{
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

虽然这样得承受operator*返回值的构造成本和析构成本,然而长远来看那只是为了获得正确行为而付出的一个小小代价。

请记住:

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。

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

最主要的理由是封装。封装在于通过函数访问成员变量,日后若要以某个计算来替换这个成员变量,而class用户根本不需要知道class的内部实现已经起了变化。

pulic意味着不封装,不封装意味着不可改变,因为如果我们要取消一个public成员变量,所有使用它的客户码都会被破坏;同样protected成员变量,所有使用它的derived classes都会被破坏。因此,protected和public成员变量一样缺乏封装性。

请记住:

  1. 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性;
  2. protected并不比public更具封装性。

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

考虑用一个class来表示网页浏览器

class WebBrowser {
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
...
};

用户想一整个执行所有这些动作,因此WebBrowser也提供这样一个函数:

class WebBrowser {
public:
...
void clearEverything();
// calls clearCache, clearHistory,
// and removeCookies
...
};

当然,这一功能也可以有一个non-member函数调用适当的member函数而提供出来:

void clearBrowser(WebBrowser& wb)
{
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

那么问题来了,是member函数还是non-member函数好呢?

面向对象守则要求数据应该尽可能被封装,如果某些东西被封装,它就不可见,愈多东西被封装,愈少人可以看到它,我们就有愈大的弹性去改变它。

现在考虑对象内的数据。越少代码可以看到数据,越多的数据可被封装,而我们也就越能自由地改变对象数据,例如改变成员变量的数量、类型等等。如何量测“有多少代码可以看到某一块数据”呢?我们计算能够访问该数据的函数数量,作为一种粗糙的量测。越多函数可访问它,数据的封装性就越低。

以上就解释了为什么clearBrowser比clearEverything更受欢迎的原因:它导致WebBrowser class有较大的封装性。

这里有两点需要注意:

  • 从封装角度看,这里的non-member并非仅仅只是non-member,而是non-member non-friend;
  • 封装性让函数成为class的non-member,并不意味着它不可以是另一个class的member。

在C++,可以让clearBrowser成为一个non-member函数并且位于WebBrowser所在的同一个namespace内:

namespace WebBrowserStuff {
class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}

另外,一个像WebBrowser这样的class可能拥有大量便利函数,某些与书签有关,某些与打印有关,还有些与cookie的管理有关……通常大多数客户只对某些感兴趣。没道理一个只对书签相关便利函数感兴趣的客户与一个cookie相关便利函数发生编译相依关系。分离它们的最直接做法就是将书签相关便利函数声明于一个头文件,将cookie相关便利函数声明于另一个头文件,再将打印相关便利函数声明于第三个头文件,以此类推:

// header “webbrowser.h” — header for class WebBrowser itself 
// as well as “core” WebBrowser-related functionality
namespace WebBrowserStuff {
class WebBrowser { ... };
...
// “core” related functionality, e.g.
// non-member functions almost
// all clients need
}
// header “webbrowserbookmarks.h”
namespace WebBrowserStuff {
...
// bookmark-related convenience
}
// functions
// header “webbrowsercookies.h”
namespace WebBrowserStuff {
...
// cookie-related convenience
}
// functions
...

这正是C++标准库的组织方式。

将所有的便利函数放在多个头文件但隶属于同一个命名空间,这样客户也可以轻松扩展这一组函数。他们需要做的就是添加更多的non-member non-friend函数到此命名空间。这是class无法提供的,因为class定义式对客户而言是不能扩展的。

请记住:

  • 宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。

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

class Rational {
public:
Rational(int numerator = 0,
// ctor is deliberately not explicit;
int denominator = 1);
// allows implicit int-to-Rational
// conversions
int numerator() const;
// accessors for numerator and
int denominator() const;
// denominator — see Item 22
private:
...
};

class Rational {
public:
...
const Rational operator*(const Rational& rhs) const;
};

Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;// fine
result = result * oneEighth;// fine

尝试混合相乘

result = oneHalf * 2;// fine
result = 2 * oneHalf; // error!

上面的形式就等同于

result = oneHalf.operator*(2);// fine
result = 2.operator*(oneHalf );// error!

为什么上面第一个是对的,因为发生了隐式转换,但是,只有当参数被列于参数列内,这个参数才是隐式转换的合格持有者。

将operator*声明成一个friend函数呢?这种做法也不好,因为无论何时如果你可以避免friend函数就该避免,因为friend会破坏封装性。

请记住:

  • 如果你需要为某个函数的所有参数进行类型转换,那么这个函数必须是个non-member。

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

针对swap函数,首先,如果swap的缺省实现对你的class或class template提供可接受的效率,你不需要额外做任何事。任何尝试置换(swap)那种对象的人都会取得缺省版本,而那将有良好的运作。其次,如果swap缺省实现版的效率不足,(那几乎意味着你的class或template使用了某种pimpl手法),试着做以下事情:

(1)提供一个public swap成员函数,让它高效的置换你的类型的两个对象值,这个函数绝不该抛出异常;

(2)在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数;

(3)如果你正编写一个class(而非class template),为你的class特化std::swap。并令它调用你的swap成员函数。

最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,直接调用swap。

请记住:

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常;
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非template),也请特化std::swap;
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”;
  • 为“用户定义类型”进行std template全特化也是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

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

变量到真正需要用到时才进行定义,可以提高效率。

请记住:

  • 尽可能延后变量定义式的出现。这样可增加程序的清晰度并改善程序效率。

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

C++提供了四种新式转型

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

各个用法分别如下:

(1)const_cast通常被用来将对象的常量性消除;

(2)dynamic_cast主要用来执行“安全向下转型”,也就是用来据诶的那个某对象是否归属继承体系中的某个类型;

(3)reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为int。

(4)static_cast用来强迫隐式转换,例如将non-const对象转为const对象,或将int转为double等。

请记住:

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast,如果有个设计需要转型动作,试着找出无需转型的替代设计;
  • 如果转型是必要的,试着将它隐藏于某个函数背后;客户随后可以调用该函数,而不需将转型放进它们自己的代码内;
  • 宁可使用C++-style转型,不要使用旧式转型。

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

如果一个函数返回一个数据成员的引用,那么会引起两个问题:
(1)破坏封装性,虽然数据被声明为private,但是返回它们引用的函数为public,其数据成员实际上就成了public;

(2)如果一个const函数返回其内部成员的reference,那么函数调用者就可以直接修改其内部成员。

reference,pointers和iterators都是handles。

即使返回一个const reference也有问题:

class Rectangle {
public:
...
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
...
};   

这样可能会引起悬挂handles问题:handles指向一个不再存在的对象。例如:

class GUIObject { ... };
const Rectangle
// returns a rectangle by
boundingBox(const GUIObject& obj);
// value; see Item 3 for why
//  return type is const

GUIObject *pgo;
// make pgo point to
...
// some GUIObject
const Point *pUpperLeft =
// get a ptr to the upper
 &(boundingBox(*pgo).upperLeft());
// left point of its
// bounding box

上面boundingBox函数会产生一个临时Rectangle对象,pUpperLeft指向了其内部的一个Point对象,然后这句话执行完后,临时的Rectangle就销毁了,这就导致了pUpperLeft指向了一个不再存在的对象。

也有一些例外,例如容器的operator[]函数,但这不是一般情况。

请记住:

  • 避免返回handles(包括reference,pointer,iterator)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生悬挂hangles的可能性降低。

条款29:尽量写出异常安全型代码

当一个异常发生时,一个异常安全函数应该做到:

  1. 不会泄漏资源;
  2. 不会让数据结构销毁。

下面的例子就不满足上面任何一点:

class PrettyMenu {
    public:
    ...
    void changeBackground(std::istream& imgSrc); // change background
    ...                                          // image
    private:
     Mutex mutex;    // mutex for this object 
    Image *bgImage;  // current background image
    int imageChanges; // # of times image has been changed
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    lock(&mutex);  // acquire mutex (as in Item 14)
    delete bgImage; // get rid of old background
    ++imageChanges; // update image change count
    bgImage = new Image(imgSrc);  // install new background
    unlock(&mutex);    // release mutex
}

通过Item13和Item14,我们学会了如何用对象管理资源和类Lock的使用,可以将上面的函数修改如下:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock ml(&mutex);   // from Item 14: acquire mutex and
                       // ensure its later release
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);
}

上面的叙述保证了资源泄漏问题,下面重点放在数据结构如何不被销毁。一个异常安全函数应该满足下面三点之一:

  1. 基本保证:异常抛出后,函数中的任何东西都保持在有效状态下。没有对象和数据结构被破坏,所有的对象和数据结构都处于一种前后一致的状态。
  2. 强烈保证:如果异常发生,程序的状态不会改变。调用这样的函数需要知道,这样的函数一旦执行成功,就是完全成功,如果失败,程序就会回到调用该函数之前的状态。
  3. 不抛异常保证:承诺函数不抛出任何异常。

异常安全代码必须要提供上面至少一种保证,若没有,就不是异常安全代码。至于如何去选择,按实际情况,如果能够提供不抛异常代码就不要写强烈保证的代码,如果能做到强烈保证的代码,就不要写基本保证的代码。但是代码至少要做到基本保证的承诺。

在写异常安全的代码时,有一种copy and swap的策略需要熟悉,就是针对要修改的对象,先做一份copy,然后针对这份copy做修改,最后把这份copy与原始对象swap。这个策略一般都会涉及到“pimpl idiom”,就是将所有要修改的数据单独封装起来,然后在原来的类中用一个指针来操作这些数据。上面的例子可以用如下方法:

struct PMImpl {        // PMImpl = “PrettyMenu
    std::tr1::shared_ptr<Image> bgImage;// Impl.”; see below for
    int imageChanges;          // why it’s a struct
};
class PrettyMenu {
    ...
    private:
    Mutex mutex;
    std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    using std::swap;// see Item 25
    Lock ml(&mutex);// acquire the mutex
    std::tr1::shared_ptr<PMImpl>// copy obj. data
    pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc));   // modify the copy
    ++pNew->imageChanges;
    swap(pImpl, pNew);// swap the new
                     // data into place
}  // release the mutex

通常,一个函数内部可能还会调用别的函数,如果需要这个函数做到“强烈保证”的安全类型,那么它内部调用的每一个函数都必须做到“强烈保证”的安全类型。

请记住:

  • 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都有可实现或具备现实意义。
  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

条款30:理解inline的里里外外

针对inline,如果inline函数的本体很小,编译器针对“函数本体”所产出的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存命中率。相反,过度的inlining会增加目标码的大小,在内存受限系统上会造成程序体积太大,即使拥有虚拟内存,inline造成的代码膨胀也会导致额外的换页行为,降低高速缓存命中率,以及伴随而来的性能损失。

通常,inline函数都放在头文件中,因为大多数编译环境在编译期进行函数的inline操作。

库设计者必须评估inline函数的使用,因为一旦一个inline函数的内容修改了,客户端就必须重新编译代码,而如果该函数不是inline的,那么只需要重新link就可以了。

总之,对于inline,我们需要慎用。

请记住:

  • 将大多数inlining限制在小型、被频繁调用的函数身上,这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化;
  • 不要只因为function templates出现在头文件,就将它们声明为inline。

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

#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;// implementation detail
        Date theBirthDate;// implementation detail
        Address theAddress;// implementation detail
};

上面的代码,Person的定义就与上面的头文件关联了起来,这样,如果当中的任何一个文件有修改,调用Person的客户端代码就都得重新编译。

我们可以采用“将对象实现细节隐藏于一个指针背后”的策略。针对上面的Person,可以把Person分割为两个classes,一个只提供接口,另一个负责实现该接口。

#include <string>  // standard library components
                   // shouldn’t be forward-declared
#include <memory>  // for tr1::shared_ptr; see below
class PersonImpl;  // forward decl of Person impl. class
class Date;        // forward decls of classes used in
class Address;     // Person interface

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:                                 // ptr to implementation;
std::tr1::shared_ptr<PersonImpl> pImpl;  // see Item 13 for info on
};                                       // std::tr1::shared_ptr

这样,就将Person的客户端代码与dates,address和persons的实现细节分离了,这是一种真正的接口与实现的分离。这个分离的关键在于用声明的依赖性来替换定义的依赖性。下面是三条简单的设计策略:

  1. 如果使用object reference或object pointers可以完成任务,那么就不要用object;
  2. 如果能够,尽量以class声明式替换class定义式;
  3. 为声明式和定义式提供不同的头文件。这里需要两个头文件,一个用于声明式,一个用于定义式。如果有个声明式被改变了,两个文件都得改变。C++标准库采用了这样的做法,例如内含iostream各组件的声明式,这类头文件名都含fwd。

像Person这样使用pimpl idiom的classes,被称为Handle classes,这样的class如何工作,有两种办法:
(1)将它们的所有函数转交给相应的实现类并由后者完成实际工作。

#include "Person.h"
#include "PersonImpl.h"

Person::Person(const std::string& name, const Date& birthday,
const Address& addr)
: pImpl(new PersonImpl(name, birthday, addr))
{}
std::string Person::name() const
{
return pImpl->name();
}

(2)让Person成为一种特殊的abstract base class,称为interface class,不带成员变量,没有构造函数,只有一个virtual析构函数以及一组pure virtual函数。一组Person的接口如下所示

class Person {
    public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
    ...
};

对于interface class,通常通过一个工厂函数来获得相应的derived class的指针,而这种函数往往在interface class内被声明为static

class Person {
public:
...
static std::tr1::shared_ptr<Person> // return a tr1::shared_ptr to a new
create(const std::string& name,     // Person initialized with the
const Date& birthday,              // given params; see Item 18 for
const Address& addr);             // why a tr1::shared_ptr is returned
...
};

客户端则像如下调用

std::string name;
Date dateOfBirth;
Address address;
...
// create an object supporting the Person interface
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
std::cout << pp->name()    // use the object via the
<< " was born on "         // Person interface
<< pp->birthDate()
<< " and now lives at "
<< pp->address(); 

Handle class和interface class解除了接口和实现之间的耦合关系,从而降低了文件间的编译依赖性,但是这样也会付出运行期丧失若干速度和若干内存的代价。

请记住:

  • 支持“编译依赖性最小化”的一般构想是:能依赖于声明式,不要依赖于定义式。基于此构想的两个手段是Handle class和Interface class;
  • 程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。

条款32:确定你的public继承属于is-a关系

public继承必须是满足is-a关系的,特别要注意,子类包含了基类的一切行为,在设计时,要特别注意,有些直观上的is-a的东西往往在继承时会出现问题。比如所有的鸟都会飞,企鹅是鸟,但不会飞。对于这些问题,需要视情况而设计,需要记住,所谓的最佳设计,取决于系统需要做什么事,不管是现在还是将来。

请记住:

  • public继承意味着is-a,适用于base class身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也是一个base class对象。

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

C++中,编译器查找某个名称的作用域遵循以下原则:首先查找local作用域,如果没找到,就找其外围作用域,即当前类覆盖的作用域,没找到的话,再找其基类的作用域,依次往上,当前命名空间作用域,最后到全局作用域。

有种情况要特别注意,如下代码:

class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
virtual void mf1();
void mf3();
void mf4();
...
};

Derived d;
int x;
...
d.mf1();  // fine, calls Derived::mf1
d.mf1(x); // error! Derived::mf1 hides Base::mf1

不管该函数是virtual还是非virtual的,就算其参数不一样,derived class的同名函数还是会覆盖base class的函数。

上面可以采用using的方式来处理

class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
using Base::mf1;
// make all things in Base named mf1 and mf3
using Base::mf3;
// visible (and public) in Derived’s scope
virtual void mf1();
void mf3();
void mf4();
...
};

Derived d;
int x;
...
d.mf1();// still fine, still calls Derived::mf1
d.mf1(x);// now okay, calls Base::mf1
d.mf2();// still fine, still calls Base::mf2
d.mf3();// fine, calls Derived::mf3
d.mf3(x);// now okay, calls Base::mf3 (The int x is 
         // implicitly converted to a double so that
         // the call to Base::mf3 is valid.)

还有一种称为转交函数的方法也可以处理,

class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
...
// as before
};
class Derived: private Base {
public:
virtual void mf1()// forwarding function; implicitly
{ Base::mf1(); }// inline — see Item 30. (For info
...
                // on calling a pure virtual
};               // function, see Item 34.)
...
Derived d;
int x;
d.mf1();// fine, calls Derived::mf1
d.mf1(x);// error! Base::mf1() is hidden

请记住:

  • derived class内的名称会覆盖base class内的名称。在public继承下从来没有人希望如此;
  • 为了让被遮掩的名称在见天日,可使用using声明式或转交函数(forwarding function)

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

一个基类如何选择pure virtual函数,simple virtual和non-virtual函数,主要有以下原则:
(1)如果一个函数,derived class仅仅只是继承其接口,那么这个函数就该设计为pure virtual

(2)如果一个函数,derived class不仅仅只是继承其接口,还需要一个默认的实现,那么就设计为simple virtual。

这种情况有时候会产生一个问题,即一个新增的derived class本来是需要重新实现该函数了,但是设计时忘了,导致继承了默认的实现,这在某些系统中可能会造成很大的灾难。有一种方法可以避免这种请况的发生。这种方法在于切断virtual函数接口和其缺省实现之间的连接。

class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
default code for flying an airplane to the given destination
}

class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};

class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
code for flying a ModelC airplane to the given destination
}

这种情况下,ModelC如果没有重新实现fly函数,就不会通过编译。这种方法还不是很安全,因为很存在copy-and-paste代码。这里需注意defaultFly是一个non-virtual函数,因为derived class无需重新实现它。

还有一种方法可以利用“pure virtual函数必须在derived class中重新声明,但他们也可以拥有自己的实现”这一事实来将接口和缺省实现分开。

class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination)  // an implementation of
{
// a pure virtual function
default code for flying an airplane to
the given destination
}
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
code for flying a ModelC airplane to the given destination
}

这个方法主要在于将fly分割为两个基本要素:其声明部分表现的是接口,定义部分则表现出缺省行为。

(3)声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性声明。

请记住:

  • 接口继承和实现继承不同。在public继承之下,derived class总是继承base class的接口;
  • pure virtual函数只具体指定接口继承;
  • simple virtual函数具体指定接口继承及缺省实现继承;
  • non-virtual函数具体指定接口继承以及强制性实现继承。

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

几种替代virtual函数的方法:

(1)借由non-virtual interface手法实现template method模式

这种方法主动virtual函数几乎总是private,由一个public的接口调用private的virtual函数。

class GameCharacter {
public:
    int healthValue() const     // derived classes do not redefine
    {                            // this — see Item 36
    ...                         // do “before” stuff — see below
    int retVal = doHealthValue(); // do the real work 
    ...                           // do “after” stuff — see below
    return retVal;
    }
...
private:
    virtual int doHealthValue() const // derived classes may redefine this
    { 
    ...                               // default algorithm for calculating
    }                                 // character’s health
};

这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface手法。它是所谓的Template Method设计模式的一个独特表现形式。

NVI的优点在于这个public函数可以保证在一个virtual函数被调用之前设定好场景,调用结束之后清理场景。

(2)借由function pointer实现Strategy模式

class GameCharacter;   // forward declaration
// function for the default health calculation algorithm
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;
};

这个做法是常见的Strategy设计模式的简单应用。它提供了某些弹性:

-同一认为人物类型之不同实体可以有不同的健康计算函数。

class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf )
{ ... }
...
};
int loseHealthQuickly(const GameCharacter&);  // health calculation
int loseHealthSlowly(const GameCharacter&);  // funcs with different
                                             // behavior
EvilBadGuy ebg1(loseHealthQuickly);         // same-type charac-
EvilBadGuy ebg2(loseHealthSlowly);          // ters with different
                                           // health-related
                                            // behavior

-某已知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。

(3)由tr1::function完成Strategy模式

class GameCharacter;    // as before
int defaultHealthCalc(const GameCharacter& gc); // as before
class GameCharacter {
public:

// HealthCalcFunc is any callable entity that can be called with
// anything compatible with a GameCharacter and that returns anything
// compatible with an int; see below for details

typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf )
{}
int healthValue() const 
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};

这个设计与之前相比,唯一的不同就是如今的GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针。它具有更大的弹性:

short calcHealth(const GameCharacter&);        // health calculation
                                                 // function; note 
                                              // non-int return type
struct HealthCalculator {                     // class for health
int operator()(const GameCharacter&) const    // calculation function
{ ... }                                       // objects
};
class GameLevel {
public:
float health(const GameCharacter&) const;     // health calculation
...                                           // mem function; note
};                                            // non-int return type
class EvilBadGuy: public GameCharacter {      // as before
...
};

class EyeCandyCharacter: public GameCharacter {   // another character
...                                               // type; assume same
};                                                // constructor as 
                                                   // EvilBadGuy
EvilBadGuy ebg1(calcHealth);                       // character using a
                                                  // health calculation
                                                  // function
EyeCandyCharacter ecc1(HealthCalculator());         // character using a 
                                                  // health calculation
                                                   // function object
GameLevel currentLevel;
...
EvilBadGuy ebg2(                                  // character using a
std::tr1::bind(&GameLevel::health,                // health calculation
currentLevel,                                     // member function;
_1)                                               // see below for details
);

这里,为计算ebg2的健康指数,应该使用GameLevel class的成员函数health。GameLevel::health宣称它自己接受一个参数,但它实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter。如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使他不再接受两个参数。在这个例子中我们必然会想要使用currentLevel作为“ebg2的健康计算函数所需的那个GameLevel对象”,于是将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为:它指出ebg2的健康计算函数应该总是以currentLevel作为GameLevel对象。

本条款摘要

  • 使用non-virtual interface手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性的virtual函数。
  • 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式;
  • 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式;
  • 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。

请记住:

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式;
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法方位class的non-public成员;
  • tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

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

重新定位non-virtual函数,首先破坏了public继承的is-a的关系;其次由于non-virtual函数是静态绑定的,还会导致如下问题

class B {
public:
void mf();
...
};
class D: public B {
public:
void mf();
...
};

D x;

B *pB = &x;
pB->mf();   // calls B::mf

D *pD = &x;
pD->mf();   // calls D::mf

这里本来的意愿是都能调用D的mf(),但是实际情况如上注释所示。

请记住:

  • 绝不重新定义继承而来的non-virtual函数

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

由于non-virtual函数不能重新定义,所以这里只讨论virtual函数。这里的关键在于virtual函数是动态绑定的,但是默认参数值是静态绑定的。

// a class for geometric shapes
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
// all shapes must offer a function to draw themselves
virtual void draw(ShapeColor color = Red) const = 0;
...
};

class Rectangle: public Shape {
public:
// notice the different default parameter value — bad!
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
...
};

Shape *ps;
Shape *pc = new Circle;
Shape *pr = new Rectangle;

ps = pr;
pr->draw();// calls Rectangle::draw(Shape::Red)!

由于默认参数是静态绑定,所以这里的Rectangle的draw函数虽然将默认参数改成了Green,但是调用时其实还是用的Shape的默认参数。

正确做法是:

class Shape {
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const = 0;
...
};

class Rectangle: public Shape {
public:
virtual void draw(ShapeColor color = Red) const;
...
};

但是这样又有一个明显的问题:代码重复。这时我们可以用Item35的NVI手法:

class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const
{
doDraw(color);
}
...
private:
virtual void doDraw(ShapeColor color) const = 0;  
};

class Rectangle: public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;
...
};

请记住:

  • 绝对不重新定义继承而来的缺省参数值,因为默认参数是静态绑定,只有virtual函数才是动态绑定。

条款38:通过复合塑造出has-a或is implemented in terms of

前面说public是一种is-a的关系。复合也有自己的意义,has-a或is implemented in terms of。当复合发生于应用领域时,表现为has-a的关系,当它发生于实现域内则是表现is implemented in terms of的关系。

如下面的Person就是has-a的关系

class Address { ... };

class PhoneNumber { ... };
class Person {
public:
...
private:
std::string name;

Address address;

PhoneNumber voiceNumber;

PhoneNumber faxNumber;

};

最关键的是在要用is implemented in terms of的时候,不能实现成is-a的关系。例如自己想用链表来实现一个set,这个时候就不能让这个set继承自STL的list,因为set并不是一个list,只能是用list来实现。

请记住:

  • 复合的意义与public继承完全不同;
  • 在应用域,复合意味着has-a,而在实现域内,复合意味着is implemented in terms of。

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

首先,private继承意味着:编译器不会将子类转换成基类;同时基类的所有成员都变成了private。

public继承意味着is-a,private继承呢,它意味implemented-in-terms-of。如果class D以private形式继承class B,这时就应该是采用B内已经具备的某些特性,而不是因为B和D存在任何观念上的关系。private继承纯粹是一种实现技术。

private继承意味着implemented-in-terms-of,而前面刚刚说过复合也有implemented-in-terms-of,那这两种该如何选择呢?答案是:尽可能使用复合,必要时才用private继承。何时才是必要?主要是当protected成员或virtual函数牵扯进来的时候。还是一种就是当空间方面的利害关系足以推翻private继承的支柱时。

看这样一个设计:

class Timer {
public:
explicit Timer(int tickFrequency);
 virtual void onTick() const;   // automatically called for each tick
...
};

class Widget: private Timer {
private:
virtual void onTick() const;   // look at Widget usage data, etc.
...
};

这个其实可以采用复合的关系来实现

class Widget {
private:
class WidgetTimer: public Timer {
public:
virtual void onTick() const;
...
};
 WidgetTimer timer;
...
};

另外一种情况涉及empty class

class Empty {};   // has no data, so objects should
                  // use no memory
class HoldsAnInt { // should need only space for an int
private:
int x;
Empty e;// should require no memory
};

这里 sizeof(HoldsAnInt) > sizeof(int); 大多数编译器中sizeof(Empty)获得1,因为面对“大小为零之独立对象”,通常C++会勒令默默安插一个char到空对象内。而由于位对齐,可能不只一个char。但是这种情况不适用于derived class对象内的base class部分,因为它们并非独立的。

class HoldsAnInt: private Empty {
private:
int x;
};

这里几乎可以肯定 sizeof(HoldsAnInt) == sizeof(int)。这就是empty base optimization(EBO)。STL中有许多技术用途的empty class,其中内含有用的成员(通常是typedef),包括base class的unary_function和binary_function。

请记住:

  • private继承意味着implemented-in-terms-of。它比通常的复合的级别低。但是当derived class需要访问protected base class成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的;
  • 和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

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

多重继承需要注意多个基类中相同的名字,会导致子类的歧义,要消除歧义,必须明确指出是哪个基类的名字。

多重继承还有一个问题

class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile: public InputFile,
public OutputFile
{ ... };

这里的IOFile继承自InputFile和OutputFile,那么File的成员在IOFile中到底是一份还是两份呢?C++中的默认的是两份,如果只想要一份,可以让File成为一个virtual base class

class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile,
public OutputFile
{ ... };

使用virtual继承会使代码体积变大,速度变慢,所以需要审慎,不得不用时才用,即便用时也不要在virtual base class内放置数据,这与java中的interface 类似。

请记住:

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

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

在templates和泛型的世界,与面向对象有根本上的不同。在这里,显示接口和运行期多态仍然存在,但是重要性远不及隐式接口和编译期多态。看下面的例子:

template<typename T>
void doProcessing(T& w)
{
    if (w.size() > 10 && w != someNastyWidget) {
    T temp(w);
    temp.normalize();
    temp.swap(w);
    }
}

这里有两点:

(1)w必须支持哪一种接口,系由template中执行于w身上的操作来决定。本例中w的一系列表达式便是T必须支持的一组隐式接口;

(2)凡是涉及w的任何函数调用,有可能造成template的实例化,使这些调用得以成功。这样的实例化行为发生在编译期。“以不同的template参数实例化function template”会导致调用不同的函数,这便是所谓的编译期多态。

通常显示接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。而隐式接口就完全不同了,它不基于函数签名式,而是由有效表达式组成。

上面的例子,w不一定必须提供名为size的函数,确实T必须支持size成员函数,然而这个函数也可能从base class继承而得。这个成员函数甚至不需要返回一个数值类型,它唯一需要做的是返回一个类型为X的对象,而X对象加上一个int(10的类型)必须能够调用一个operator>。这个operator>不需要非得取得一个类型为X的参数不可,因为它也可以取得类型Y的参数,只要存在一个隐式转换能够将类型X的对象转换为Y的对象。

请记住:

  • class和template都支持接口和多态;
  • 对class而言接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;
  • 对template参数而言,接口是隐式的,基于有效表达式。多态则是通过template实例化和函数重载解析发生于编译期。

条款42:理解typename的两种含义

第一种就是模板定义的typename,与class等同

template<class T> class Widget;// uses “class”
template<typename T> class Widget;  // uses “typename”

重点看第二种:主要对于从属名称(template内出现的名称如果依赖于某个template参数),如果从属名称在class内呈嵌套状,我们称他为嵌套从属名称。下面例子中的C::const_iterator就是一个嵌套从属名称。

template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2) {
C::const_iterator iter(container.begin());   // this name is assumed to 
...   // not be a type

对嵌套从属名称的解析,C++有一个规则,就是当看到一个嵌套从属名称时,默认这个名字不是一个名称,除非明确告诉它,所以嵌套从属名称就不是一个类型。因此上面的代码就是错的。如果明确告诉,就是在之前加上typename。

template<typename C>  // this is valid C++
void print2nd(const C& container)
{
    if (container.size() >= 2) {
    typename C::const_iterator iter(container.begin());
    ...
    }
}

也有一种特殊情况下不需要用typename,就是typename不可以出现在base class list内的嵌套从属类型名称之前,也不可在member initialization list中作为base class修饰符。例如:

template<typename T>
class Derived: public Base<T>::Nested {   // base class list: typename not
public:                                  // allowed
explicit Derived(int x)
: Base<T>::Nested(x)                 // base class identifier in mem.
{                                 // init. list: typename not allowed
typename Base<T>::Nested temp;   // use of nested dependent type
...                               // name not in a base class list or
}                                // as a base class identifier in a 
...                              // mem. init. list: typename required
};

请记住:

  • 声明模版参数时,class和typename是等价的;
  • 用typename来识别嵌套从属名称,除非在base class list或者在member initialization list中。

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

如果一个类的基类是一个模板类,然后该类中的函数调用了基类的函数,可能会导致问题。因为编译器在编译期根本不知道该类的基类到底是哪一个类,直到该基类被实例化后才知道,因此编译器也就无法知道这个函数是否存在基类内。针对这个问题,有三种方法可以解决:

(1)在基类函数调用动作之前加上“this->”:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
write "before sending" info to the log;
this->sendClear(info);   // okay, assumes that
                         // sendClear will be inherited
write "after sending" info to the log;
}
...
};

(2)采用using声明

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
    public:
    using MsgSender<Company>::sendClear;// tell compilers to assume
    ...                                 // that sendClear is in the
                                        // base class
    void sendClearMsg(const MsgInfo& info)
    {
    ...
    sendClear(info);                // okay, assumes that
    ...                             // sendClear will be inherited
    }
    ...
};

(3)基类显式调用

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
    public:
    ...
    void sendClearMsg(const MsgInfo& info)
    {
    ...
    MsgSender<Company>::sendClear(info);     // okay, assumes that
    ...                                      // sendClear will be 
    }                                        // inherited
    ...
};

请记住:

  • 可在derived class template内通过this->指涉base class template内的成员名称,或由一个明白写出的base class资格修饰符完成。

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

请记住:

  • template生成多个class和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生依赖关系;
  • 因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替换template参数;
  • 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的实例化类型共享代码。

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

如果class内声明了一个泛化的copy构造函数,但是如果没有声明non-template的copy构造函数,编译器还是会自动生成一个。

请记住:

  • 请使用成员函数模板生成“可接受所有兼容类型”的函数;
  • 如果你声明member template用于泛化copy构造或泛化assignmeng操作,你还是需要生命正常的copy构造函数和copy assignment操作符。

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

template<typename T>
class Rational {
public:
Rational(const T& numerator = 0,
const T& denominator = 1);  
const T numerator() const;

const T denominator() const;

...

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

Rational<int> oneHalf(1, 2);   // this example is from Item 24,
                               // except Rational is now a template
Rational<int> result = oneHalf * 2;  // error! won’t compile

template实参推导过程中从不将隐式类型转换为函数纳入考虑,上面的编译错误就是因为在template实参推导时不会将2隐式转换为Rational。

解决办法可以将template class内的friend声明式指涉某个特定函数,class template并不依赖template实参推导,同时还要将其定义实现在class体内,也就是inline。

template<typename T> class Rational;
template<typename T>

const Rational<T> doMultiply( const Rational<T>& lhs,
                                const Rational<T>& rhs);// template
template<typename T>
class Rational {
    public:
    ...
    friend
    const Rational<T> operator*(const Rational<T>& lhs,
    const Rational<T>& rhs) // Have friend
    { return doMultiply(lhs, rhs); }

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

请记住:

  • 当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。

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

如何设计并实现一个traits class:

  • 确认若干你希望将来可取得的类型相关信息。例如对于迭代器而言,我们希望将来可取得其分类;
  • 为该信息选择一个名称(例如iterator_category);
  • 提供一个template和一组特化版本(例如iterator_traits),内含你希望支持的类型信息。

如何使用一个traits class:

  • 建立一组重载函数(身份像劳工)或函数模板,彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相应;
  • 建立一个控制函数或函数模板,它调用上述那些劳工函数并传递traits所提供的信息。

请记住:

  • Traits class是的“类型相关信息”在编译期可用。它们以template和“template特化”完成实现;
  • 整合重载技术后,traits class有可能在编译期对类型执行if…else测试。

条款48:认识template元编程

请记住:

  • template metaprogramming(模板元编程)可将工作由运行期移到编译期,因而得以实现早期错误侦测和更高的执行效率;
  • TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

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

当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个指定的错误处理函数,一个所谓的new-handler。可以调用set_new_handler指定这个处理函数,其声明如下

namespace std {
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

set_new_handler函数指定一个新的处理函数,返回的是原来的处理函数。当operator new无法满足内存申请时,它会不断调用new-handler函数,知道找到足够内存。因此设计一个良好的new-handler函数必须做以下事情:

  • 让更多内存可被使用;
  • 安装另一个new-handler函数;
  • 卸除new-handler;
  • 抛出bad_alloc异常;
  • 不返回。

如何实现一个class专属的new-handlers,只需令每一个class提供自己的set_new_handler和operator new即可。

class Widget
{
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    private:
    static std::new_handler currentHandler;
};

std::new_handler Widget::currentHandler = 0;

std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

class NewHandlerHolder
{
public:
    explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}  // new-handler
    ~NewHandlerHolder()   // release it
    {
        std::set_new_handler(handler);
    }
private:
    std::new_handler handler;  // remember it
    NewHandlerHolder(const NewHandlerHolder&); // prevent copying
    NewHandlerHolder& operator=(const NewHandlerHolder&);
};

void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);// allocate memory or throw
}


void outOfMem()
{
    std::cerr << "Unable to satisfy request for memory\n";
    std::abort();
}

int main()
{

    Widget::set_new_handler(outOfMem);   // set outOfMem as Widget¡¯s
                                        // new-handling function
    Widget *pw1 = new Widget;// if memory allocation
                            // fails, call outOfMem
    std::string *ps = new std::string;   // if memory allocation fails,
                                        // call the global new-handling
                                       // function (if there is one)
    Widget::set_new_handler(0);  // set the Widget-specific
                                 // new-handling function to
                                // nothing (i.e., null)
    Widget *pw2 = new Widget;   // if mem. alloc. fails, throw an
                                // exception immediately. (There is
                                // no new- handling function for
                                // class Widget.)
}

实现这一方案的代码对每个class并没有差异,因此可以利用base class和template来使每个derived class将获得实体互异的class data复件。

template<typename T>
class NewHandlerSupport {
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
private:
    static std::new_handler currentHandler;
};
template<typename T>
std::new_handler
NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
{
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
}
// this initializes each currentHandler to null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

class Widget: public NewHandlerSupport<Widget> {
...             // as before, but without declarations for
};              // set_new_handler or operator new

请记住:

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用;
  • Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。

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

何时可在“全局性的”或“class 专属的”基础上合理替换缺省的new和delete。

  • 为了检测运用错误
  • 为了收集动态分配内存之使用统计信息;
  • 为了增加分配和归还的速度
  • 为了降低缺省内存管理器带来的空间额外开销
  • 为了弥补缺省分配器中的非最佳对齐
  • 为了将相关对象成族集中
  • 为了获得非传统的行为

请记住:

  • 有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。

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

请记住:

  • operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 bytes申请。class专属版本则还应该处理“比正确大小更大的申请”。
  • operator delete应该在收到null指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”。

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

请记住:

  • 当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐藏而时断时续的内存泄漏;
  • 当你声明placement new 和 placement delete,请确定不要无意识的遮掩了它们的正常版本。

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

请记住:

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高警告级别下争取“无任何警告”的荣誉;
  • 不要过度依赖编译器报警的能力,因为不同的编译器对待事情的态度并不相同,一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失。

条款54:让自己熟悉包括TR1在内的标准程序库

条款55:让自己熟悉Boost

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值