Effective C++: 05实现
26:尽可能延后变量定义式的出现时间
1:只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并未被使用,仍需耗费这些成本,所以你应该尽可能避免这种情形。
2:像下面这个函数:
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short");
}
...
return encrypted;
}
如果有异常抛出,那么就得付出不必要的encrypted的构造成本和析构成本。所以最好延后encrypted的定义,直到确实需要它:
std::string encryptPassword(const std::string& password)
{
using namespace std;
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short");
} string encrypted;
...
return encrypted;
}
实际上,上面这个函数仍然没有达到它本可以达到的那样紧凑。因为encrypted虽获定义却无任何实参作为初值。这意味调用的是其default构造函数,之后肯定还会需要有一个赋值操作:
std::string encrypted; // default-construct encrypted
encrypted = password; // assign to encrypted
因此,更受欢迎的做法是:
std::string encrypted(password); // define and initialize via copy constructor
3:所谓“尽可能延后”的真正意义。你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。这样不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。
27:尽量少做转型动作
1:对于转型,有三种不同的形式。
C风格的转型动作看起来像这样:
(T) expression // cast expression to be of type T
函数风格的转型动作看起来像这样:
T(expression) // cast expression to be of type T
上面两种形式并无差别,纯粹只是小括号的摆放位置不同而己。我称此二种形式为“旧式转型”。
C++提供四种新式转型:
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
const_cast通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C++-style转型操作符;
dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作;
reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int;
static_cast用来强迫隐式转换,例如将non-const对象转为const对象,或将int转为double等等;
旧式转型仍然合法,但新式转型较受欢迎。原因是:第一,它们很容易在代码中被辨识出来(人眼或者类似grep的工具);第二,各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。
2:许多程序员相信,转型其实什么都没做,只是告诉编译器把某种类型视为另一种类型。这是错误的观念。任何一个类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的码。例如:
int x, y;
double d = static_cast<double>(x)/y;
将int 转型为double几乎肯定会产生一些代码,因为在大部分计算器体系结构中,int的底层表述不同于double的底层表述。
再比如:
class Base { ... };
class Derived: public Base { ... }; Derived d;
Base *pb = &d; // implicitly convert Derived* to Base*
这里我们不过是建立一个base class指针指向一个derived class对象,但有时候上述的两个指针值并不相同。这种情况下会有个偏移量在运行期被施行于Derived*指针身上,用以取得正确的Base*指针值。
上个例子表明,单一对象可能拥有一个以上的地址(例如“以Base*指向它”时的地址和“以Derived*指向它”时的地址)。C不可能发生这种事,Java不可能发生这种事,C#也不可能发生这种事。但C++可能!
因此,通常应该避免做出“对象在C++中如何如何布局”的假设。当然更不该以此假设为基础执行任何转型动作。
3:考虑下面的代码:
class Window {
public:
Window(int size): m_size(size) {} virtual void onResize() {
cout << "this is Window onResize\n";
m_size = ;
} int m_size;
void getSize()
{ cout << "m_size is " << m_size << endl; }
}; class SpecialWindow: public Window {
public:
SpecialWindow(int size): Window(size) {} virtual void onResize() {
static_cast<Window>(*this).onResize();
cout << "this is SpecialWindow onResize\n";
}
}; int main()
{
SpecialWindow sw();
sw.onResize();
sw.getSize();
}
上述代码的本意,是想在派生类SpecialWindow的onResize函数中,首先调用基类Window的onResize函数。
但是代码中却使用了转型动作。这段代码将*this转型为Window,对函数onResize的调用也因此调用了Window::onResize。但恐怕你没想到,它调用的并不是当前对象上的函数,而是转型动作所建立的一个“当前对象之base class成分”的副本身上的onResize。
因此,当前对象的m_size没有改动,改动的是副本。所以代码结果如下:
this is Window onResize
this is SpecialWindow onResize
m_size is
正确的做法应该是:
Window::onResize();
4:dynamic_cast的许多实现版本执行速度相当慢。例如至少有一个很普遍的实现版本基于“class名称之字符串比较”,如果你在四层深的单继承体系内的某个对象身上执行dynamic_cast,刚才说的那个实现版本所提供的每一次dynamic_cast可能会耗用多达四次的strcmp调用,用以比较class名称。
因此,除了对一般转型保持机敏与猜疑,更应该在注重效率的代码中dynamic_cast保持机敏与猜疑。
绝对必须避免的一件事是所谓的“连串dynamic_cast",也就是看起来像这样的东西:
if (SpecialWindow1 *psw1 =
dynamic_cast<SpecialWindow1*>(iter->get())) { ... } else if (SpecialWindow2 *psw2 =
dynamic_cast<SpecialWindow2*>(iter->get())) { ... } else if (SpecialWindow3 *psw3 =
dynamic_cast<SpecialWindow3*>(iter->get())) { ... }
...
这样产生出来的代码又大又慢,而且基础不稳,因为每次Window class继承体系一有改变,所有这一类代码都必须再次检阅看看是否需要修改。
28:避免返回handles指向对象内部成分
所谓对象内部成分,就是指对象的成员变量(或成员函数)。所谓的handles,就是指引用、指针或者迭代器。
考虑下面实现的表示矩形的代码,通过矩形的左上角和右下角的点坐标,就可以唯一确定一个矩形,表示矩形的类Rectangle提供了upperLeft和lowerRight函数返回这两个点的坐标:
class Point { // class for representing points
public:
Point(int x, int y); void setX(int newVal);
void setY(int newVal);
...
}; struct RectData { // Point data for a Rectangle
Point ulhc; // ulhc = " upper left-hand corner"
Point lrhc; // lrhc = " lower right-hand corner"
}; class Rectangle {
public:
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; } private:
std::tr1::shared_ptr<RectData> pData;
};
upperLeft和lowerRight函数返回reference。这样的设计可通过编译,但却是错误的。实际上它是自我矛盾的。一方面这俩函数被声明为const成员函数,另一方面两个函数却都返回references指向private内部数据,调用者于是可通过这些references更改内部数据:
Point coord1(, );
Point coord2(, );
const Rectangle rec(coord1, coord2); rec.upperLeft().setX();
尽管rec是个const对象,但是upperLeft的调用者却能够使用被返回的reference来更改成员。
上面的例子说明:第一,成员变量的封装性最多只等于“返回其reference”的函数的访问级别。本例之中虽然ulhc和lrhc都被声明为private,它们实际上却是public,因为public函数upperLeft和lowerRight传出了它们的references;第二,如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。
上面我们所说的每件事情都是由于“成员函数返回references"。如果它们返回的是指针或迭代器,相同的情况还是发生。References、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。同时,它也可能导致“虽然调用const成员函数却造成对象状态被更改”。
通过将upperLeft和lowerRight它们的返回类型加上const,就可以防止客户修改Rectangle内部数据了:
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
尽管如此,upperLeft和lowerRight还是返回了“代表对象内部”的handles,有可能在其他场合带来问题。更明确地说,它可能导致dangling handles(空悬的号码牌):也就是handles所指的东西不复存在的问题。最常见的不复存在的对象问题,来源于函数返回值:
class GUIObject { ... };
const Rectangle boundingBox(const GUIObject& obj); GUIObject *pgo;
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
上面的代码中,对boundingBox的调用获得一个新的、暂时的Rectangle对象。这个对象没有名称,所以我们权且称它为temp。随后upperLeft作用于temp身上,返回一个reference指向temp的一个内部成分,于是pUpperLeft指向那个Point对象。
目前为止一切还好,但故事尚未结束,因为在那个语句结束之后,boundingBox的返回值,也就是我们所说的temp将被销毁,而那间接导致temp内的Points析构。最终导致pUpperLeft指向一个不再存在的对象:也就是说一旦产出pUpperLeft的那个语句结束,pUpperLeft也就变成空悬、虚吊!
这就是为什么函数如果“返回一个handle代表对象内部成分”总是危险的原因。不论这所谓的handle是个指针或迭代器或reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里的唯一关键是,有个handle被传出去了,一旦如此你就是暴露在“handle比其所指对象更长寿”的风险下。
尽管如此,有时候必须返回handle,比如operator[]这种,这样的函数毕竟是例外,不是常态。
29:为“异常安全”而努力是值得的
下面的代码:
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
}
从“异常安全性”的角度来看,这个函数很糟。“异常安全”有两个条件,而该函数没有满足其中任何一个条件:
a:不泄漏任何资源。上述代码没有做到这一点,因为一旦”new Image(imgSrc)”导致异常,就不会调用到”unlock(&mutex)”,于是互斥器就永远被把持住了。
b:不允许数据败坏。如果”new Image(imgSrc)”抛出异常,bgImage 就是指向一个己被删除的对象,imageChanges也己被累加,但其实并没有新的图像被成功安装起来。
解决资源泄漏的问题很容易,因为在之前的条款13讨论过如何以对象管理资源,而条款14也导入了Lock class作为一种“确保互斥器被及时释放”的方法;
解决了资源泄露问题后,现在我们可以专注解决数据的败坏了。此刻我们需要做个抉择,但是在我们能够抉择之前,必须先面对一些术语。
异常安全函数提供以下三个保证之一:
a基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)。比如,在changeBackground函数中,一旦有异常被抛出时,PrettyMenu对象可以继续拥有原背景图像,或是令它拥有某个缺省背景图像,但客户无法预期哪一种情况。如果想知道,他们恐怕必须调用某个成员函数以得知当时的背景图像是什么。
b强烈保证:如果异常被抛出,调用这样的函数,如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。
c不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。
异常安全的代码必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。因此,我们的抉择是,该为我们所写的每一个函数提供哪一种保证?
从异常安全性的观点来看,最强烈保证是最好的。但我们很难在C part of C++领域中完全不调用任何一个可能抛出异常的函数:任何使用动态内存的东西(例如所有STL容器)如果无法找到足够内存以满足需求,通常便会抛出一个bad_alloc异常。因此对大部分函数而言,抉择往往落在基本保证和强烈保证之间。
对changeBackground而言,提供强烈保证几乎不困难。首先改变PrettyMenu的bgImage成员变量的类型,从一个类型为Image*的内置指针改为一个智能指针。这样做是为了防止资源泄漏。然后,重新排列changeBackground内的语句次序,使得在更换图像之后才累加imageChanges:
class PrettyMenu {
...
std::tr1::shared_ptr<Image> bgImage;
...
}; void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex); bgImage.reset(new Image(imgSrc));
++imageChanges;
}
这里不再需要手动delete旧图像,因为这个动作己经由智能指针内部处理掉了。此外,删除动作只发生在新图像被成功创建之后。这两个改变几乎足够让changeBackground提供强烈的异常安全保证。
有个一般化的设计策略很典型地会导致强烈保证,很值得熟悉它。这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。
对PrettyMenu而言,典型写法如下:
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; Lock ml(&mutex); std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); pNew->bgImage.reset(new Image(imgSrc));
++pNew->imageChanges; swap(pImpl, pNew);
}
"copy-and-swap"策略是对对象状态做出“全有或全无”改变的一个很好办法,但一般而言它并不保证整个函数有强烈的异常安全性。为了解原因,让我们考虑一个抽象函数:someFunc,它使用copy-and-swap策略,但函数内还包括对另外两个函数fl和f2的调用:
void someFunc()
{
... // make copy of local state
f1();
f2();
... // swap modified state into place
}
很显然,如果f1或f2的异常安全性比“强烈保证”低,就很难让someFunc成为“强烈异常安全”。举个例子,假设f1只提供基本保证,那么为了让someFunc提供强烈保证,我们必须写出代码获得调用f1之前的整个程序状态、捕捉f1的所有可能异常、异常发生后恢复原状态。
如果f1和f2都是“强烈异常安全”,情况并不就此好转。毕竟如果f1圆满结束,程序状态在任何方面都可能有所改变,因此如果f2随后抛出异常,程序状态和someFunc被调用前并不相同。
如果函数只操作局部性状态,例如someFunc只影响其“调用者对象”的状态,便相对容易地提供强烈保证。但是当函数对非局部性数据有连带影响时,提供强烈保证就困难得多。举个例子,如果调用f1带来的影响是某个数据库被改动了,那就很难让someFunc具备强烈安全性。一般而言在“数据库修改动作”送出之后,没有什么做法可以取消并恢复数据库旧观,因为数据库的其他客户可能已经看到了这一笔新数据。
当“强烈保证”不切实际时,你就必须提供“基本保证”。现实中你或许会发现,你可以为某些函数提供强烈保证,但效率和复杂度带来的成本会使它对许多人而言摇摇欲坠。对许多函数而言,“异常安全性之基本保证”是一个绝对通情达理的选择。
一个软件系统要么就具备异常安全性,要么就全然否定,没有所谓的“局部异常安全系统”。即使系统内只有一个函数不具备异常安全性,整个系统就不具备异常安全性,因为调用那个函数有可能导致资源泄漏或数据结构败坏。
30:透彻了解inlining的里里外外
1:inline函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换之。这样做可能增加你的目标码大小。
2:inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内:
class Person {
public:
...
int age() const { return theAge; }
private:
int theAge;
};
这样的函数通常是成员函数,但friend函数也可被定义于class内,如果是那样,它们也是被隐喻声明为inline。
明确声明inline函数的做法则是在其定义式前加上关键字inline。例如:
template<typename T>
inline const T& std::max(const T& a, const T& b)
{ return a < b ? b : a; }
3:Inline函数通常一定被置于头文件内,因为大多数构建环境是在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。
4:大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining。
所有对virtual函数的调用也都会使inlining落空。因为virtual意味着直到运行期才确定调用哪个函数”,而inline意味“执行前,先将调用动作替换为被调用函数的本体”。
有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体。比如,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。与此并提的是,编译器通常不对“通过函数指针而进行的调用”实施inlining:
inline void f() {...} // assume compilers are willing to inline calls to f void (*pf)() = f; // pf points to f
...
f(); // this call will be inlined, because it's a "normal" call pf(); // this call probably won't be, because it's through a function pointer
即使你从未使用函数指针,“未被成功inlined”的inline函数还是有可能缠住你,因为程序员并非唯一要求函数指针的人。有时候编译器会生成构造函数和析构函数的outline副本,如此一来它们就可以获得指针指向那些函数,在array内部元素的构造和析构过程中使用。
实际上构造函数和析构函数往往是inlining的糟糕候选人。即使是一个空函数体的构造函数或析构函数,编译器也会生成大量代码。当创建一个对象,其每一个base class,以及每一个成员变量都会被自动构造;销毁一个对象,反向程序的析构行为亦会自动发生。如果有个异常在对象构造期间被抛出,该对象已构造好的那一部分会被自动销毁。编译器为了实现这些操作,会在构造函数和析构函数中产生大量代码。
5:程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。换句话说如果f是程序库内的一个inline函数,客户将“f函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。然而如果f是non-inline函数,一旦它有任何修改,客户端只需重新连接就好,远比重新编译的负担少很多。
6:大部分调试器对于inline函数都束手无策。
7:我们在决定哪些函数该被声明为inline而哪些函数不该时,要掌握一个合乎逻辑的策略:一开始先不要将任何函数声明为inline,或至少将inline施行范围局限在那些“一定成为inline 或“十分平淡无奇”的函数身上。
不要忘记80-20经验法则:平均而言一个程序往往将80%的执行时间花费在20%的代码上头。这是一个重要的法则,它提醒你,作为一个软件开发者,你的目标是找出这可以有效增进程序整体效率的20%代码,然后将它inline或竭尽所能地将它瘦身。但除非你选对目标,否则一切都是虚功。
31:将文件间的编译依存关系降至最低
假设某个class的实现文件做了些轻微修改,而且只改private成分。然后重新构建这个程序,有可能会耗费大量时间,因为“整个世界都被重新编译和连接了”!
问题出在C++并没有把“将接口从实现中分离”这事做得很好。Class的定义式不只详细叙述了class接口,还包括十足的实现细目。例如:
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; // 实现细目
Date theBirthDate; // 实现细目
Address theAddress; // 实现细目
};
代码中必须#include必要的头文件才能编译通过,也就是Person类所用到的string, Date, Address类的头文件。
但是,这么一来便是在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency)。如果这些头文件中有任何一个被改变,或这些头文件所倚赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。这样的连串编译依存关系会对许多项目造成难以形容的灾难。
为了避免这种情况,考虑将实现细节从类定义中抽离,也就是像下面这样:
namespace std {
class string;
} class Date; // forward declaration
class Address; // forward declaration 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;
...
};
但是,编译器必须在编译期间知道对象的大小。比如:
int x; // define an int
Person p( params ); // define a Person
当编译器看到x的定义式,它知道必须分配多少内存才够持有一个int。没问题,每个编译器都知道一个int有多大。当编译器看到p的定义式,它也知道必须分配足够空间以放置一个Person,但它如何知道一个Person对象有多大呢?编译器获得这项信息的唯一办法就是询问class定义式。然而如果class定义式不列出实现细节,编译器如何知道该分配多少空间呢?
有两种方法解决这种问题:
1:PIMPL idiom(pointer to implementation)
这就是所谓的“将对象实现细目隐藏于一个指针背后”。
针对Person我们可以这样做:把Person分割为两个classes,一个只提供接口,另一个负责实现该接口。定义Person的代码如下:
//Person.h
#include <string>
#include <memory> class Date;
class Address; class Person {
public:
Person(const std::string& name, const Date& birthday,const Address& addr);
~Person(); std::string name() const;
std::string birthDate() const;
std::string address() const;
... private:
class PersonImpl;
std::unique_ptr<PersonImpl> pImpl;
};
在这里,Person类只内含一个指针成员(这里使用std::unique_ptr),指向其实现类(PersonImpl)。
注意,因为这里使用了std::unique_ptr智能指针,因此必须声明一个析构函数~Person,并在实现文件cpp文件中,定义一个空的~Person函数。这是因为:如果不定义该析构函数的话,编译器会自己定义一个。在编译器定义的析构函数中,会析构各个成员变量,也就是会调用类std::unique_ptr<PersonImpl>的析构函数。而std::unique_ptr是个模板,它的析构模板函数会在实际调用时被实例化,因此,调用~std::unique_ptr<PersonImpl>时就会实例化该模板函数,该函数中调用到了delete,delete中又会调用sizeof,而sizeof的参数PersonImpl此时是个不完全类型,因此会造成编译错误。解决方法就是在Person中要声明其析构函数,在Person的实现源码文件中,定义一个空的析构函数即可。因为此时PersonImpl的定义已经是可见的了。
如果这里用的是shared_ptr,则不存在这个问题,具体原因跟shared_ptr的实现原理有关。但是,针对PIMPL idiom而言,一般是使用unique_ptr,因为多数情况下,具体的实现类不会是共享的。有关PIMPL idiom的问题,参考:
https://herbsutter.com/gotw/_100/
http://www.cppsamples.com/common-tasks/pimpl.html
https://stackoverflow.com/questions/8619708/must-provide-destructor-in-the-pimpl
https://stackoverflow.com/questions/21699201/pimpl-with-smart-ptr-why-constructor-destructor-needed
下面是Person的实现代码:
//Person.cpp
#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();
} ~Person::Person(){}
这样的设计之下,Person的客户就完全与Dates, Addresses以及Persons的实现细节分离了。那些classes的任何实现的修改(也就是Person.cpp中的修改)都不需要Person客户端重新编译,因为客户端只会包含Person.h文件 :
//main.cpp
#include "Person.h" int main()
{
std::string name("Peter");
Date date("1980-01-01");
Address addr("USA");
Person p(name, date, addr);
...
}
像Person这样使用pimpl idiom的classes,往往被称为Handle classes。这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。
2:抽象基类
另一个制作Handle class的办法是,令Person成为一种特殊的抽象基类,称为Intertace class。这种class的目的是详细描述derived classes的接口,它通常不带任何成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口。
一个针对Person而写的Interface class或许看起来像这样:
class Person {
public:
virtual ~Person(); virtual std::string name() const = ;
virtual std::string birthDate() const = ;
virtual std::string address() const = ;
...
};
这个class的客户必须以Person的pointers和references来撰写应用程序,因为它不可能对抽象基类Person classes进行实例化。这种情况下,除非Intertace class的接口被修改否则其客户不需重新编译。
Interface class的客户必须有办法为这种class创建新对象。他们通常调用一个特殊函数,此函数扮演构造具体derived classes的角色。这样的函数通常称为factory(工厂)函数。它们返回指针(或更为可取的智能指针),指向动态分配所得对象,而该对象支持Interface class的接口。该函数往往在Interface class内被声明为static:
class Person {
public:
... static std::shared_ptr<Person>
create(const std::string& name,
const Date& birthday,
const Address& addr);
};
假设Person 有一个派生类:
class RealPerson: public Person {
public:
RealPerson(const std::string& name, const Date& birthday,
const Address& addr)
: theName(name), theBirthDate(birthday), theAddress(addr)
{} virtual ~RealPerson() {} std::string name() const; // implementations of these
std::string birthDate() const; // functions are not shown, but
std::string address() const; // they are easy to imagine private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
而Person::create的实现如下:
std::shared_ptr<Person> Person::create(const std::string& name,
const Date& birthday,
const Address& addr)
{
return std::shared_ptr<Person>(new RealPerson(name, birthday,addr));
}
一个更现实的Person::create实现代码会创建不同类型的derived class对象,取决于诸如额外参数值、读自文件或数据库的数据、环境变量等等。
客户会这样使用它们:
std::string name;
Date dateOfBirth;
Address address;
... std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address)); std::cout << pp->name()
<< " was born on "
<< pp->birthDate()
<< " and now lives at "
<< pp->address();
最后,不论Handle classes或Interface classes,一旦脱离inline函数都无法有太大作为。条款30解释过为什么函数本体为了被inlined必须置于头文件内,但Handle classes和Interface classes正是特别被设计用来隐藏实现细节如函数本体。
Effective C++: 05实现的更多相关文章
- Effective Java 05 Avoid creating unnecessary objects
String s = new String("stringette"); // Don't do this. This will create an object each tim ...
- Effective C++ .05 一些不自动生成copy assigment操作的情况
主要讲了 1. 一般情况下编译器会为类创建默认的构造函数,拷贝构造函数和copy assignment函数 2. 执行默认的拷贝构造/copy assignment函数时,如果成员有自己的拷贝构造/c ...
- More Effective C++: 05技术(30-31)
30:Proxy classes 代理类 在C++中使用变量作为数组大小是违法的,也不允许在堆上分配多维数组: int data[dim1][dim2]; int *data = new int[di ...
- More Effective C++: 05技术(29)
29:引用计数 本章首先实现一个带引用计数String,然后逐步优化,介绍引用计数的常规实现. 实现引用计数的String,首先需要考虑:引用计数在哪存储.这个地方不能在String对象内部,因为需要 ...
- More Effective C++: 05技术(25-28)
25:将constructor 和 non-member functions 虚化 所谓 virtual constructor是某种函数,视其输入可产生不同类型的对象.比如下面的代码: class ...
- Effective Java Index
Hi guys, I am happy to tell you that I am moving to the open source world. And Java is the 1st langu ...
- Effective C++ -----条款05:了解C++默默编写并调用哪些函数
面对“内含reference成员或者含const成员”的class内支持赋值操作,你必须自己定义copy assignment操作符. 如果某个base classes将copy assignment ...
- Effective C++学习笔记 条款05:了解C++默默编写并调用的哪些函数
一.如果用户没有提供构造函数.copy构造函数.copy assignment操作符和析构函数,当且仅当这些函数被需要的时候,编译器才会帮你创建出来.编译器生成的这些函数都是public且inline ...
- effective c++(05)(06)之c++默默编写并调用的函数
1. 当只写一个空类的时候,编译器会为他声明一个copy构造函数,一个copy assignment函数和一个析构函数.如下: 如果写下: class Empty{ }; 编译器就会实现以下代码: c ...
随机推荐
- 安装mysql-workbench
sudo apt-get install mysql-workbench
- js校验文本框只能输入数字(包括小数)
form表单 <form method="POST" action=""> <input type="text" id=& ...
- _mysql_exceptions.IntegrityError: (1062, "Duplicate entry, Python操作MySQL数据库,插入重复数据
[python] view plain copy sql = "INSERT INTO test_c(id,name,sex)values(%s,%s,%s)" param = ...
- java并发系列(四)-----源码角度彻底理解ReentrantLock(重入锁)
1.前言 ReentrantLock可以有公平锁和非公平锁的不同实现,只要在构造它的时候传入不同的布尔值,继续跟进下源码我们就能发现,关键在于实例化内部变量sync的方式不同,如下所示: /** * ...
- Java review-basic2
1.Implement a thread-safe (blocking) queue: Class Producer implements Runable{ Private final Blockin ...
- Ubuntu 16.04 配置 L2tp 客户端
#install lib -dev libsecret--dev libgtk--dev libglib2.-dev xl2tpd strongswan #install network-manage ...
- 全栈数据工程师养成攻略:Python 基本语法
全栈数据工程师养成攻略:Python 基本语法 Python简单易学,但又博大精深.许多人号称精通Python,却不会写Pythonic的代码,对很多常用包的使用也并不熟悉.学海无涯,我们先来了解一些 ...
- 转:CentOS上安装LAMP之第三步:MySQL环境及安装过程报错解决方案(纯净系统环境)
这是AMP运行环境中最后配置的环境: 惯例传送门: 1.编译安装MySQL cd /home/zhangatle/tar tar zxvf mysql-.tar.gz cd mysql- cmake ...
- windows 下 解决 go get 或 dep init 更新不了问题
首先你安装了Shadowsocks 并设置相应的代理,能够访问google等境外网站. 打开dos命令行窗口执行 如下图 这样你就能执行go get 或dep等命令,下载被墙的包了.以上方法为临时方法 ...
- Python学习笔记(二)使用Sublime Text编写简单的Python程序()
一.使用Sublime Text编写Python 1.点击“文件” →”新建文件“ 2.点击”文件“→”保存“,并保存为.py文件 此时已经创建好Python文件了,接下来就可以编写Python程序了 ...