Effective C++读书笔记(17)

来源:互联网 发布:网络机顶盒软件排行榜 编辑:程序博客网 时间:2024/06/16 01:11

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

Minimize casting

不要认为强制转换除了告诉编译器将一种类型看作另一种之外什么都没做,任何种类的类型转换(无论是通过强制转换的显式还是编译器添加的隐式)都会导致运行时的可执行代码。例如:

    intx, y;
double d = static_cast<double>(x)/y; // x除以y,使用浮点数除法

int到double的强制转换理所当然要生成代码,因为在大多数系统架构中int的底层表示与 double不同。再看下面这个例子:

    classBase { ... };
class Derived: public Base { ... };
Derived d;
Base *pb = &d; // 隐式转换,Derived* → Base*

这里我们只是创建了一个指向派生类对象的基类指针,但有时候这两个指针的值并不相同。这种情况下,会在运行时的Derived*指针上应用一个偏移量以得到正确的Base*指针值。这表明单一的对象(如一个类型为Derived的对象)可能会有不止一个地址(被Base*指针指向的地址和被Derived*指针指向的地址)。这在C、Java、C# 中都不会发生,仅在C++中发生。实际上,如果使用了多继承,这事一定会发生,甚至在单继承下也会发生。

但请注意偏移量是“有时”被需要。对象的布局方式和他们的地址计算方法在不同的编译器之间有所变化。这就意味着由“知道对象如何布局而设计的转换“,在某一平台行得通,并不意味着它们也能在其它平台工作。例如,将一个对象的地址强制转换为 char* 指针,然后对其使用指针运算,这几乎总是会导致未定义行为。

 

关于强制转换,我们要避免写出某些似是而非的代码。例如,许多应用框架(application framework)要求在派生类中实现virtual函数时要首先调用它们基类的对应函数。假设我们有Window基类和SpecialWindow派生类,它们都定义了虚函数onResize,SpecialWindow的onResize被期望首先调用Window的onResize。下面是实现方式之一,它看起来对,但实际上错:

class Window { // base class
public:
virtual void onResize() { ... } // base onResize 实现代码
...
};

class SpecialWindow: public Window { //derived class
public:
virtual void onResize() { // derived onResize 实现代码
   static_cast<Window>(*this).onResize();
   // 将*this转型为Window,然后调用其onResize,这不可行!
   ... // 这里进行SpecialWindow专属行为
} ...};

正像你所期望的,代码将*this强制转换为一个Window,调用onResize的结果就是调用 Window::onResize。但成员函数都有个隐藏的this指针,会因此影响成员函数操作的数据。上面的代码没有在当前对象调用Window::onResize后执行SpecialWindow专属动作。它是在当前对象的基类部分的副本上调用Window::onResize,然后在对当前对象上执行 SpecialWindow专属动作。如果Window::onResize 改变了当前对象(onResize是non-const成员函数),当前对象并不会改变,改变的是副本。如果 SpecialWindow::onResize改变了当前对象,当前对象真的会被改动,导致没有做基类的变更,却做了派生类的变更。

解决方法就是消除强制转换,用你真正想表达的来代替它。你不应该让编译器将*this当作一个基类对象来处理,你只想调用当前对象的onResize基类版本:

class SpecialWindow: public Window {
public:
virtual void onResize() {
    Window::onResize(); // 调用Window::onResize作用于*this身上
    ...
}
...
};

这个例子也表明如果你发现要做强制转换,这可能是你做错了某事的一个信号。

 

很多dynamic_cast的实现都相当慢。例如有一种很普遍的实现版本,基于对类名称的字符串比较。在这个实现下,如果一个四层深的单继承体系对象上执行dynamic_cast,每一个dynamic_cast都要付出相当于四次调用strcmp来比较类名字的成本。这种实现是有原因的,这样做可以支持动态链接。在性能敏感的代码中,应该特别警惕dynamic_casts。

对dynamic_cast的需要通常发生在这种情况下:你要在派生类对象上执行派生类操作函数,但只能通过一个基类指针或引用来操控。

以下给出两种方法可以避免这一问题:

1.     使用容器并在其中存储直接指向派生类对象的指针(通常为智能指针),从而消除通过基类接口操控这个对象的需要。例如,在Window/SpecialWindow继承体系中,只有 SpecialWindows支持blink效果,试着不要这样做:

class Window { ... };

class SpecialWindow: public Window {
public:
void blink();
...
};
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;
...

for (VPW::iterator iter = winPtrs.begin();iter!= winPtrs.end();++iter) {
if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
    psw->blink();
}  //我们不希望使用dynamic_cast

应该改而这样做:

    typedefstd::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...

for (VPSW::iterator iter =winPtrs.begin();iter != winPtrs.end();++iter)
(*iter)->blink(); //no dynamic_cast

当然,这个方法不允许你在同一个容器中存储所有可能的Window派生类指针。为了与不同的窗口类型一起工作,可能需要多个类型安全容器。

2.     在基类中提供虚函数做你想对各个Windows派生类做的事。例如,尽管只有 SpecialWindows能blink,但可以在基类中声明这个函数,并提供一个什么都不做的缺省实现:

class Window {
public:
    virtual void blink() {}
    ...
};

class SpecialWindow: public Window {
public:
    virtual void blink() { ... }; // 此类中blink做实事
    ...
};

    typedefstd::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs; // 容器,内含指针,指向所有可能的Windows类型
...

for (VPW::iterator iter =winPtrs.begin();iter != winPtrs.end();++iter)
(*iter)->blink(); // no dynamic_cast

无论哪种方法——使用类型安全的容器或在继承体系中上移虚函数——都不是到处适用的,但很多情况下它们提供了dynamic_cast之外另一个可行的候选方案。

你应该绝对避免的是“连串(cascading)dynamic_casts”,看起来类似这样的任何东西:

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类继承体系发生变化,所有这样的代码都要必须被检查,以确认是否需要更新。类似这样的代码应该总是用基于虚函数的调用来替换。

优良的C++代码极少使用强制转换,但完全去除也不实际。例如从int到double的强制转换,就是合理运用,虽然它并不是绝对必要(那些代码可以重写为“声明一个类型为double的变量并用x的值初始化”)。强制转换应该被尽可能地隔离,典型情况是隐藏在函数内部,用函数接口保护调用者远离内部污秽的工作。

·    避免强制转换,特别是在性能敏感的代码中应用dynamic_cast,如果一个设计需要强制转换,设法开发一个没有强制转换的侯选方案。

·    如果必须要强制转换,设法将它隐藏在一个函数中。客户可以用调用那个函数来代替在他们自己的代码中加入强制转换。

·   尽量用新式风格的强制转换替换旧风格。它们更容易被注意到,而且他们做的事情也更加明确。

 

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

Avoid returning “handles” to object internals

假设你程序涉及矩形,每一个矩形都可以用它的左上角和右下角表示出来。为了将一个 Rectangle对象尽可能小,你可能决定那些点的定义不应该包含在Rectangle之中,更合适的做法是放在一个struct内,由Rectangle去指向它:

class Point { // 这个类用来表示点
public:
Point(int x, int y);
void setX(int newVal);
void setY(int newVal);
...
};

struct RectData { // 用点数据表示矩形
Point ulhc; // ulhc = " upper left-hand corner"
Point lrhc; // lrhc = " lower right-hand corner"
};

class Rectangle {
...
private:
    std::tr1::shared_ptr<RectData>pData;
};

由于Rectangle客户必须能够计算Rectangle的范围,因此类提供了upperLeft和 lowerRight函数。可是,Point是一个用户定义类型,所以我们返回指向底层Point对象的引用:

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

这个设计可以编译,但它是错误的,实际上,它是自相矛盾的。一方面,upperLeft和 lowerRight是被声明为const的成员函数,因为它们仅仅给客户提供一个获得Rectangle 点的方法,而不允许客户改变这个 Rectangle;另一方面,两个函数都返回指向私有内部数据的引用,调用者可以利用这些引用修改内部数据:

    Pointcoord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);
// rec是const矩形,从(0, 0)到(100, 100)
rec.upperLeft().setX(50);
// 现在变成从(50, 0)到(100, 100)!

本例中虽然ulhc和lrhc被声明为private,它们实际上是public,因为public函数 upperLeft和lowerRight返回了指向它们的引用。如果一个const成员函数返回一个引用,指向一个与对象自身有关的数据,而它又被存储于对象之外,这个函数的调用者就可以改变那个数据。

不光是成员函数返回的引用,返回指针或者迭代器,因为同样的原因也会存在同样的问题。引用、指针和迭代器都是句柄(handle)(持有其它对象的方法),而返回一个对象内部构件的句柄总是危及对象封装安全,同时还能导致“虽然调用const成员函数却造成对象状态被更改”。

绝不要使成员函数返回一个指向访问级别较低的成员函数的指针。如果你这样做了,后者的实际访问级别就会提高如同前者(访问级别较高者)。

把注意力返回到Rectangle类和它的upperLeft和lowerRight成员函数。我们只需简单地将const用于它们的返回类型就可以解决已有问题:

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

通过这个修改的设计,客户可以读取矩形的Points,但他们不能写它们。

 

虽然如此,upperLeft和lowerRight仍然返回一个对象内部构件的句柄,而这有可能造成其它方面的问题,如空悬句柄(danglinghandles):这种handle所指对象不复存在。这种消失对象的最普通来源就是函数返回值。例如,某个函数返回GUI对象的外框(boundingbox),这个外框采用矩形形式:

    classGUIObject { ... };
const Rectangle boundingBox(const GUIObject& obj);

现在,考虑客户可能会这样使用这个函数:

    GUIObject*pgo; // 让pgo指向某个GUIObject
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
// 取得一个指针指向外框左上点

对boundingBox的调用会返回一个新建的临时Rectangle对象。这个对象没有名字,暂且称它为 temp。于是upperLeft就在temp上被调用,返回一个指向temp的内部构件引用,特别是,它是由Points构成的。随后pUpperLeft指向这个Point对象。到此为止,一切正常,但是我们无法继续了,因为在这个语句的末尾,boundingBox的返回值temp被销毁了,这将间接导致temp的Points的析构。剩下pUpperLeft指向一个已经不再存在的对象;也就是说一旦产出pUpperLeft的那个语句结束,pUpperLeft就变成空悬、虚吊!

这就是为什么函数如果返回一个handle代表对象内部成分都是危险的,它与那个handle是指针,引用,还是迭代器没什么关系;它与是否为cosnt没什么关系;它与那个成员函数返回的句柄本身是否是const没什么关系。

这并不意味着你永远不应该让一个成员函数返回句柄,有时你必须如此。例如,operator[]允许你从string和vector中取出单独的元素,而这些 operator[]就是通过返回指向容器内数据的引用来实现的。当容器本身被销毁,数据也将销毁。尽管如此,这样的函数属于特例,而不是惯例。

·   避免返回handle(引用,指针,或迭代器)指向对象内部。这样会提高封装性,帮助 const成员函数真正产生cosnt效果,并将产生空悬句柄的可能性降到最低。

原创粉丝点击