Effective C++ 第五章——实现
条款26——尽可能延后变量定义式的出现时间
只要你定义了一个变量而其类型带有一个构造函数和析构函数时,那么当程序的控制流(control flow)到达这个变量定义时,你变得承受构造成本;当这边变量离开作用域时,就得承受析构成本;即使这个变量最终没有被使用,也需要耗费这些成本。
例如:
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变量没有使用,但是却要承担构造与析构的成本,我们应当尽量延后变量定义式出现的时间,直到非得使用该变量的前一刻为止,甚至尝试延后这份定义直到能够给他初值实参为止。
例如:
void encrypt(std::string& s);//在其中适当的地点对s加密
std::string encryptPassword(const std::string& password)
{
using namespace std;
if(password.length() < MinimumPasswordLength)
throw logic_error("Password is too short");
...
string encrypted(password);
encrypt(encrypted);
return encrypted;
}
对于循环情况,怎么办?——即变量在循环内使用,把变量定义在循环外并在每次循环时进行赋值好,还是定义在循环内然后初始化好?
//方法A:定义在循环外
Widget w;//一个构造函数 + 一个析构函数 + n个赋值操作
for(int i=0;i<n;++i)
{
w = 取决于i的某个值;
...
}
//方法B:定义在循环内(优先)
for(int i=0;i<n;++i)//n个构造函数 + n个析构函数
{
Widget w(取决于i的某个值);
...
}
若class中赋值操作成本低于构造和析构成本,A好,特别是当n很大的时候;否则B更好。但是A造成w的作用于比B更大,所有除非你知道(1)若class中赋值操作成本低于构造和析构成本;(2)处理的代码中效率高度敏感,否则你应该优先用B。
请记住:
- 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
条款27——尽量少做转型操作
C风格的转型操作:
(T) expression
——将expression转型为TT(expression)
——将expression转型为T
上述两种C风格的转型没有本质区别,只是表达方式不同。
C++风格的转型操作:
const_cast<T>(expression);
dynamic_cast<T>(expression);
reinterpret_cast<T>(expression);
static_cast<T>(expression);
- const_cast:将对象的常量性转出,唯一有此能力的转型操作符;
- dynamic_cast:执行“安全向下转型”,决定对象是否归属继承体系中的某个类型,唯一一个可能耗费大量运行成本的转型动作;
- reinterpret_cast:执行低级转型;
- static_cast:强迫隐式转型,non-const转为const,int转double,pointer-to-base转换为point-to-derived.
始终理智的使用新式的转型操作。
很多情况下,要求derived class内的virtual 函数代码的第一个动作就先调用调用base class中对于的函数代码。
class Window{ //base class
public:
virtual void onResize(){...} //base class 的 onResize 函数的实现代码
...
};
class SpecialWindow:public Window {//derived class
public:
virtual void onResize(){ //derived class 的 onResize 函数的实现代码
static_cast<Window>(*this).onResize(); //将*this转型为Window,然后调用其onResize函数
//这是不可行的!!!!!!!!
...
};
将*this转型后,他调用的不是当前对象上的函数,而是稍早转型动作所建立的一个“*this对象的base class成分”的暂时副本上的onResize函数。上述代码并非在当前对象身上调用Window::onResize之后又在该对象身上执行 SpecialWindow的专属操作,而是在“当前对象之base class成分”的副本上调用Window::onResize之后又在当前对象身上执行 SpecialWindow的专属操作。对于base class成分,改动的是副本,当前对象没有改动,因此是不对的。
正确的写法是:
class SpecialWindow:public Window {//derived class
public:
virtual void onResize(){
Window::onResize();//调用的Window::onResize()作用于*this身上。
...
};
dynamic_cast的许多实现版本执行速度相当的慢,应该避免使用。绝对需要避免的是“连串的dynamic_cast”。
请记住:
- 如果可以,尽量避免转型,特别是在注重效率的代码中,避免dynamic_cast。如果有个设计需要转型操作,试着发展无需转型的替代设计。
- 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要转型放进他们自己的代码。
- 宁可使用C+±风格的转型也不要使用旧式转型。前者很容易识别而且也比较易于分辨是何种转型操作。
条款28——避免返回handles指向对象内部成分
handles(号码牌,用来取得某个对象):References、指针、迭代器都是handles,返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。
//定义一个矩形class,矩形class包括一个指向矩形的指针,矩形定义为一个结构体,
//其含有一个矩形的左上角点和右下角点
class Point{
public:
Point(int x,int y);
...
void setX(int newval);
void setY(int newval);
}
struct RectData{
Point ulhc;//左上角点
Point lrhc;//右上角点
}
class Rectangle{
public:
Point& upperLeft() const { return pData->ulhc; }//不能保证const的特性
Point& lowerRight() const { return pData->ulhc; }
...
private:
std::tr1::shared_ptr<RectData> pData;
};
这里的upperLeft()和lowerRight()函数返回了指向内部私有数据,调用者可以通过reference改变内部数据。
可以得到以下启示:1.成员变量的封装性最多只等于“返回其reference”的函数的访问级别;2.如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那个数据。
不被公开使用的成员函数也不应该被返回其handles;
上述问题可以修改为如下代码:
class Rectangle{
public:
const Point& upperLeft() const { return pData->ulhc; }//不能保证const的特性
const Point& lowerRight() const { return pData->ulhc; }
...
private:
std::tr1::shared_ptr<RectData> pData;
};
dangling handles(悬空的号码牌)问题:handles指向的对象不复存在,这种情况最常见的发生在函数返回值。
请记住:
- 避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可以增加封装性,帮助const成员函数的行为更像个const,并将发生“虚吊号码牌”的可能性降到最低。
条款29——为“异常安全”而努力是值得的
异常安全的两个条件:
- 不泄露任何资源:可以通过资源管理类实现;
- 不允许数据损坏:
异常安全函数(Exception-safe function)提供一下是三个保证:
- 基本承诺:如果异常抛出,程序内的任何事物仍然保持在有效的状态下,没有任何对象或数据结构会因此而破坏,所有对象都处于一种内部前后一致的状态。
- 强烈保证:如果异常抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功,就是完全成功;如果函数失败,程序会回复到“函数调用之前”的状态。可以采用copy-and-swap实现(将你打算修改的对象复制一份复本,然后在副本上进行一系列必要的修改,若修改没有任何异常抛出,则进行置换swap,否则有异常抛出则原对象保持不变),但是此种方法会降低效率。
- 不抛掷(throw)保证:承诺不抛出异常,总能完成原先承诺的功能。
异常安全代码(Exception-safe code)必须提供上述三种保证之一。
如果没有办法提供强烈保证的话,起码提供基本保障。一个软件系统要不具备异常安全性,要不就不具备,没有部分具备这一说法。如果一份代码中有一个函数不具备异常安全性,那这份代码就不具备异常安全性。
请记住:
- 异常安全函数(Exception-safe functions)即使发生异常也不会泄露资源或者允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型。
- “强烈保证”往往可以用copy-and-swap实现,但强烈保证并非对所有函数都可以实现或者具备实现的意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中最弱者。
条款30——透彻了解inlining的里里外外(内联函数)
inline函数:编译器对此类函数的调用是直接以原代码进行替换的,这回增大代码的体积,但是会免除函数调用的成本。过多的使用inline函数会导致代码膨胀,导致额外的换也行为,降低告诉缓存装置的击中率。当inline函数本体很小时,可能会提高效率。
inline函数可以隐式声明也可以显示声明,隐式声明是将函数定义在class内。显示声明的话是加上关键字inline。
inline函数一般被定义在头文件中。因为inlining在编译器执行。
//隐式声明
class Person{
public:
int age() const { return theAge; }
...
private:
int theAge;
};
//显示声明
template<typename T>
inline const T& std::max(const T& a,const T& b)
{ return a<b ? b:a; }
virtual函数是不可能为inline函数的,因为virtual需要在运行期间才知道调用哪个函数。而inline意味着在编译期将调用动作替换为调用函数的本体。如果编译器不知道调用那个函数是没有办法inlining的。
一个表面上看似inline的函数是否inline取决于编译器,如果编译器无法inline化,则会给你一个警告。
编译器不对通过函数指针进行的调用实施inlining。
inline void f() {}
void (* pf)() = f;
...
f();//调用被inline,正常调用
pf();//调用或许不被inline,通过函数指针调用
编程者常常可能在定义构造函数和析构函数将其隐式声明为inline的,这时的构造函数和析构函数看起来是空的,什么代码也没有,但其实编译器会为你加入很多代码。是否将class的构造函数和析构函数inline话要仔细思考决定。
inline函数无法岁程序库的升级而升级。f是一个inline函数,一旦程序设计者改变inline函数f,则用到f函数的所有客户端都要重新编译;如果f是non-inline函数并被修改,只需要重新动态链接即可。
大部分调试器对inline函数束手无策,大多数调试器只是在调试过程中禁止inline。
请记住:
- 将大多数inlining限制在小型、被频繁使用的函数上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
- 不要因为function template 出现在头文件中就将其声明为inline。
条款31——将文件间的编译依存关系降至最低
#include <string>
#include "date.h"
#include "address.h"
class Person{
public:
...
private:
std::string theName;//实现细目
Date theBirthDay;//实现细目
Address theAddress;//实现细目
};
Person的定义与其含入的文件形成了一种编译依存关系(compilation dependency)。如果这些头文件中任何一个改变或这些头文件所依赖的任何其他头文件改变,那么每一个含入Person class的文件就得重新编译,任何使用了Person class的文件也要重新编译。形成一种连串编译依存关系(cascading compilation dependencies)。
为了使编译依存性最小化,提出了两种方法,将接口与实现分离
- Handle classes
使用一种pimpl idiom的思想(pointer to implementation)。定义两个class,一个指提供接口,一个负责实现接口。这种分离的关键是以“声明的依存性”替换“定义的依存性”。
#include <string>//标准程序库组件不该被前置声明
#include <memory>//为了将tr1::shared_ptr含入
class PersonImpl;//
class Date;//
class Address;
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::tr1::shared_ptr<PersonImpl> pImpl;
};
设计策略:
- 如果使用object reference 或 object pointers可以完成任务,就不要使用object;
- 如果能够,尽量以class声明式替换class定义式,将#include完成的替换为声明式;
- 为声明式和定义式提供不同的头文件,程序库客户总是以#include一个声明文件而非前置声明若干函数。
为了将handle class做点事情(将实现部分的功能能够被调用),将他们的所有函数转交给相应的实现类。,并由后者完成实际工作。
#include "Person"
#include "PersonImpl"
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();
}
- Interface class
提供一种特殊的抽象基类Interface class,它通常不含有成员变量,也没有构造函数,只有一virtual析构函数以及一组pure virtual函数,用来叙述整个接口。加上一个特殊的函数,此函数扮演真正的将被具现化derived class 的构造函数角色,该函数返回例如:
class Person {
public:
virtual ~Person();
virtual std::string name() const =0;
virtual std::string birthDate() const =0;
virtual std::string address() const =0;
static std::tr1::shared_ptr<Person> create(const std::string& name,
const Date& birthday, const Address& addr);
};
请记住:
- 支持“编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handles class和Interface class。
- 程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及template都使用。