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

1.实例分析

假设你的程序涉及矩形,每个矩形由其左上角和右下角表示,为了让一个Rectangle对象尽可能小,你可能会决定不把定义矩形的这些点存放在Rectangle对象内,而是放在一个辅助的struct内再让Rectangle去指它:

class Point{//这个class可以用来表述"点"
    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::trl::share_ptr pData;
};

Rectangle的客户必须能够计算Rectangle的范围,所以这个class提供upperLeft函数和lowerRight函数。Point是个用户自定义类型,所以根据条款20给我们的忠告(以by reference方式传递用户自定义类型往往比by value方式传递更高效),这些函数于是返回references,代表底层的Point对象:

class Rectangle{

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

这样的设计可以通过编译,但却是错误的。实际上它是自我矛盾的。一方面upperLeft和lowerRight被声明为const成员函数,因为它们的目的只是为了提供客户一个得知Rectangle相关坐标的方法,而不是让客户修改Rectangle。另一方面两个函数却都返回reference指向private内部数据,调用者于是可以通过references更改内部数据,例如:

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

这里请注意,upperLeft的调用者能够使用被返回的reference(指向rec内部的Point成员变量)来更改成员。但rec其实应该是不可变的(const)。

这给我们带来两个教训。第一是成员变量的封装性最多只等于“返回其reference”的函数访问级别。本例之中虽然ulhc和lrhc都被声明为private,它们实际上却是public,因为public函数upperLeft和lowerRight传出了它们的references;第二,如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据,这正是bitwise constness的一个附带结果。

上面我们所说的每件事情都是由于“成员函数返回reference”。如果它们返回的是指针或者迭代器,相同的情况还是会发生,原因也相同,Reference,指针和迭代器统统都是所谓的handles(号码牌,用来得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。同时,所之前所述,它也可能导致“虽然调用const成员函数却造成对象状态被更改”。

通常,我们认为对象的“内部“就是指它的成员变量,但其实不被公开使用的成员函数(也就是被声明为protected或private者)也是对象"内部"的一部分.因此也应该留心不要返回它们的handles.这意味着你绝对不该令成员函数返回一个指针指向"访问级别较低"的成员函数,如果你这么做,后者的实际访问级别就会提高到如同前者(访问级别较高者),因为客户可以取得一个指针指向那个“访问级别较低”的函数,然后通过那个指针调用它。

然而“返回指针指向某个成员函数”的情况毕竟不多见,所以让我们把注意力收回,专注于Rectangle class和它的upperLeft以及lowerRight成员函数。我们在这些函数身上遭遇的两个问题可以轻松去除,只要对它们的返回类型加上const即可:

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

有了这样的改变,客户可以读取矩形的Points,但不能涂写它们。这意味着当初声明upperLeft和upperRight为const不再是个谎言,因为它们不再允许客户更改对象状态。至于封装问题,我们总是愿意让客户看到Rectangle的外围Points,所以这里是刻意放松封装。更重要的是这是个有限度的放松:这些函数只让渡读取权,涂写全仍然是被禁止的。但即使如此,upperLeft和lowerRight还是返回了“代表对象内部的handles”,有可能在其它场合带来问题,更明确地说,它可能导致dangling handles(空悬的号码牌):这种handles所指向的东西不复存在,这种不复存在的对象最常见的来源就是函数返回值。例如某个函数返回GUI对象的外框,这个外框采用矩形形式:

class GUIObject{...};
boundingBox(const GUIOBject& obj);//以by value方式返回一个矩形

现在,客户有可能这样使用这个函数:

GUIObject* pgo;//让pgo指向某个GUIObject
...
const Point* pUpperLeft=&(Rectangle.upperLeft());

对boundingBox的调用获得一个新的/暂时的Rectangle对象。这个对象没有名称,所以我们权且称它们为temp。随后upperLeft作用于temp身上,返回一个reference指向temp的一个内部成分,更具体地说是指向一个用以标识temp的Points。于是pUpperLeft指向那个Point对象。目前为止一切还好,但故事尚未结束,因为在那个语句结束之后,boundingBox的返回值,也就是我们所说的temp将被销毁,而那间接导致temp内的Points析构。最终导致pUpperLeft指向一个不再存在的对象;也就是说一旦产出pUpperLeft的那个语句结束,pUpperLeft也将变成空悬,虚吊。

这就是为什么函数如果“返回一个handle代表对象内部成分”总是危险的原因。不论这所谓的hangle是个指针或迭代器或reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里唯一的关键是,有个handle被传出去,一旦如此就是暴露在”handle比其所指对象更长寿“的风险下。

但这也并不意味着绝对不可以让成员函数返回handle,有时候必须要这样做,例如operator[]就允许你访问strings和vectors的个别元素,而这些operator[]就是返回references指向“容器内的数据”,那些数据会随着容器被销毁而销毁。尽管如此,这样的函数不是常态。

3.总结

由于内容较多,对本文的内容总结为以下几点:

1.避免返回handles(包括references,指针,迭代器)指向对象内部。遵守这个条款可以增加封装性,帮助const成员函数的行为像个const,并将发生dangling handles的可能性降到最低。

你可能感兴趣的:(开发语言,c++)