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 ...
随机推荐
- 2018-8-17-C#-从零开始写-SharpDx-应用-控制台创建-Sharpdx-窗口
title author date CreateTime categories C# 从零开始写 SharpDx 应用 控制台创建 Sharpdx 窗口 lindexi 2018-8-17 9:3:3 ...
- vue如何发请求
1.vue 支持开发者引入 jquery 使用 $.ajax() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1.首先,在 package.json 中添加 j ...
- Java集合(七)--基于jdk1.8的HashMap源码
HashMap在开发中经常用,面试源码方面也会经常问到,在之前也多次了解过源码,今天算是复习一下,顺便好好总结一下,包括在后面有 相关面试题.本文不会对红黑树代码由太多深入研究,特别是删除方面太复杂, ...
- 【noip】跟着洛谷刷noip题2
noip好难呀. 上一个感觉有点长了,重开一个. 36.Vigenère 密码 粘个Openjudge上的代码 #include<cstdio> #include<iostream& ...
- php传入参数
项目中时常会使用php编写脚本,进行一些统计与批量更新的需求需要进行操作其中使用的方式主要是getopt函数,写法如下 <?php $opt = getopt('m:n:'); $value_m ...
- 【JZOJ5363】【NOIP2017提高A组模拟9.14】生命之树 Trie+启发式合并
题面 45 在比赛中,我只想到了45分的暴力. 对于一个树中点对,相当于在他们的LCA及其祖先加上这个点对的贡献. 那么这个可以用dfs序+树状数组来维护. 100 想法 我想到了可能要用trie树来 ...
- tesseract3.0.2font_id >= 0 && font_id < font_id_map_.SparseSize():Error:Assert failed:in file ..\..\classify\trainingsampleset.cpp, line 622
https://stackoverflow.com/questions/14025965/mftraining-gives-warning-no-protos-configs-for-f-in-cre ...
- 玩转Spring Boot 自定义配置、导入XML配置与外部化配置
玩转Spring Boot 自定义配置.导入XML配置与外部化配置 在这里我会全面介绍在Spring Boot里面如何自定义配置,更改Spring Boot默认的配置,以及介绍各配置的优先 ...
- Python网络爬虫与信息提取[request库的应用](单元一)
---恢复内容开始--- 注:学习中国大学mooc 嵩天课程 的学习笔记 request的七个主要方法 request.request() 构造一个请求用以支撑其他基本方法 request.get(u ...
- Leetcode73. Set Matrix Zeroes矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0.请使用原地算法. 示例 1: 输入: [ [1,1,1], [1,0,1], [1,1,1] ] 输 ...