五、实现--条款28-30

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

通常我们说的内部成分就是它的成员变量或者非public的成员函数。

一、为何要做此避免

书上的例子很好的解释了为什么不返回对象内部成分。
首先有一个坐标类,

class Point
{
public:
    Point(int x, int y);
    void SetX(int x);
    void SetY(int y);
private:
    int x;
    int y;
};

然后有一个矩形数据类:包含矩形的左上角和右下角坐标。

struct RectData
{
    Point upLeft;
    Point lowRight;
};

最后看我们的矩形类:

class Rectangle
{
public:
    Point &upperLeft() const
    {
        return pData->upLeft;
    }
    Point &lowerRight() const
    {
        return pData->lowRight;
    }
private:
    shared_ptr<RectData> pData;
};

当我们执行以下代码时:

Point p1(0,0);
Point p2(100,100);
const Rectangle rec(p1,p2);
// 修改左上角坐标
rec.upperLeft.SetX(20);

经过这样的代码后,虽然我们使用const修饰了rec变量,但是我们还是可以修改rec内部的数据!!

这就和bitwise带来的额外效果一样,如果我们返回了一个引用,在函数中我们并没有改变任何数据,但是实际上我们通过其返回的引用改变了成员变量!这让我们的const功亏一篑。

解决方法

只需要在Rectangle中的函数添加一个const即可:

const Point &upperLeft() const;
const Point &lowerRight() const;

这样返回的也是一个常量引用,不能修改一个常量了。

导致dangling handles

即这种handles所指东西将不复存在。最常见的就是一个函数返回值:

假设有一个GUI对象外框类:

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

考虑以下代码调用会发生什么:

GUIObject *pgo;
const Point *pUpLeft = &(boundingBox(pgo).upperLeft);

事实上这一句非常危险! boundingBox函数返回一个Rectangle的临时对象,当语句结束后,这个临时对象就会被析构,不复存在。而pUpLeft则指向一个已经被销毁的对象。所以会变成空悬、虚吊(dangling)的情况!

特别注意:无论何时,返回一个临时对象不要写成一句复杂的语句,分成几句去写,我们如果用变量来接受这个临时对象,就不会产生一些语句结束后这个临时对象马上就被销毁,从而造成虚吊的情况。

作者总结:

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

条款29:为“异常安全”而努力是值得的

一、特别注意new抛出异常的处理情况

本条款一开始所用的例子其实之前也有讲过几次,比如在处理自我赋值的时候就讲过。本质是一样的。在此条款中只是又举了一个例子:切换背景图像的例子。

class PrettyMenu
{
public:
    void changeBackGround(std::istream &imgSrc);  // g改变背景图像
    ...
private:
    Mutex mutex;                // 互斥器
    int imageChanges;           // 背景图像改变的次数
    shared_ptr<Image> bgImage;  // 当前的背景图像
};
void PrettyMenu::changeBackGround(std::istream *imgSrc)
{
    Lock m1(&mutex);
    bgImage.reset(new Image(imgSrc));
    imageChanges++;
}

为了防止new抛出异常后导致锁一直被持有,或者是次数已经增加了但是后面执行却由于new抛出的异常而失败了,
我们还是利用条款14中的一个资源管理类Lock。这样做的好处:

  • 我们不用写析构函数,在计数器为0的时候会自动调用删除器。
  • 倘若new失败了,出了这个作用域m1也会被析构,也就是调用删除器,就不会导致锁一直被持有。
  • imageChanges放到最后才自增的原因是只有改变背景图片成功之后我们才去增加次数。

二、异常安全函数所提供的三个保证

(1) 基本承诺。 如果异常被抛出,程序的任何事物仍然保持在有效的状态下,没有任何对象或者数据结构因此被破坏。比如上述的增加的次数不能没成功就加1,又或者锁被一直持有。

(2) 强烈保证。 如果异常被抛出,程序状态不改变。如果函数成功,就是完全成功,如果失败,那就是完全恢复到调用之前的状态。不能够 部分成功。

(3) 不抛掷保证。 承诺绝不抛出异常。作用于内置类型的身上的所有操作都有nothrow保证。一般有nothrow的方法可以这么写:

int doSomething() throw();

这只是表明doSomething抛出异常的话会造成致命错误!并不是说这个函数决定不会抛出异常!

三、使用copy-and-swap策略

copy-and-swap策略可以帮我们进行“强烈保证”——要么成功,要么恢复到之前的状态,绝不存在部分成功的状态。

原则

(1) 为你打算修改的对象做出一份副本,在副本上进行一切必要的修改。

(2) 如果修改过程中抛出了异常,原对象还是保持不变,只是副本变了。

(3) 如果所有修改都成功了,就将副本和原对象在一个不会抛出异常的函数中进行交换。

使用copy-and-swap修改上述改变背景图像版本

问题的关键就是有一份副本,我们需要的副本就是实际需要改变的东西:背景颜色和修改次数。所以我们可以写一个struct包含这两个成员。

此外,为了swap的高效,条款25中已经提到,我们如果只是交换指针所指则会更高效。所以我们在原来的PrettyMenu中提供一个智能指针。

如下:

struct PMImpl
{
    shared_ptr<Image> bgImage;
    int imageChanges;
}
class PrettyMenu
{
public:
    void changeBackGround(std::istream &imgSrc);  // 改变背景图像
    ...
private:
    Mutex mutex;                // 互斥器
    shared_ptr<PMImpl> pImpl;  // 当前的背景图像
};
void PrettyMenu::changeBackGround(std::istream *imgSrc)
{
    shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));  //为当前背景图像做一份副本
    pNew->bgImage.reset(new Image(imgSrc)); //修改背景图像
    ++pNew->imageChanges;
    swap(pImpl,pNew);
}

上述代码就实现了copy-and-swap策略。有一个要注意的细节:我们将pNew定义为shared_ptr类型的,这样出了这个作用域,它就自动析构了。

这份代码还有一个可能抛出异常的点在于Image的拷贝构造函数。

而当“强烈保证”不切实际时,就应该提供“基本承诺”。

条款30:透彻了解inlining的里里外外

原理

相信大家都明白编译器是怎么看待inline关键字修饰的成员函数:对此函数的每次调用都会用函数本体来替代。 也就是直接展开函数本体,而不是像普通函数调用。

可被拒绝

而且,inline关键字只是一个对编译器的申请,而不是强制命令。 如果编译器觉得函数太复杂,就会忽略inline关键字。比如函数中带有循环或者递归功能。

是编译期的行为

为了将函数调用替换为展开为调用函数体内的本体,编译器必须知道函数的本体是怎么样的,所以大多数C++的inline都是处于编译期完成的(也有一些是在运行期!)。

接下来我们看看此条款下有对inline函数的建议。

一、inline的缺点

调用上效率的提升不代表别的方面没有损失:

  • 过度使用inline会导致生成的.o文件(目标码)膨胀,导致编译之后的程序体积变大。
  • 即便拥有虚内存,inline造成的代码膨胀仍会导致额外的换页行为,降低指令高速缓存装置的击中率。

二、能否对virtual函数进行inline?

很明显是不能达到目的的。

上面我们已经说明了,编译器为了知道函数本体长什么样子,所以inline在大多数编译器上都是编译期行为。

但是virtual函数是运行期决定调用哪个函数的。所以对一个virtual函数进行inline,编译器肯定会拒绝此inline。

三、不适合使用inline的几种情况

  1. virtual函数,上述已经讲过,不再累述。

  2. 编译器通常不对“通过函数指针而进行的调用”实施inlining。

    inline void f() {...} // 编译器有意愿进行inline
    void (*pf)() = f; // 函数指针
    ...
    f(); // 实施内联
    pf(); // 拒绝内联
    这个地方书上没有细讲,在我看来的原因是 pf只是一个指向一个无参数的函数指针,它可以指向f(),也可以指向其它无参数的函数,编译器不能判断它的函数本体是否适合inlining.

  3. 构造函数和析构函数。 让我们假设一个Drive类,它的构造函数和析构函数都是空的函数体,不含任何代码。看起来是成为inlining的绝佳候选人,但是别忘了考虑继承的情况:倘若Drive类继承自Base类,那么在Drive构造之前就会构造,如果Base类很复杂呢?

作者总结

将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

不要只因为function templates出现在头文件,就将它们声明为Inline.

posted @ 2018-09-21 14:00  _NewMan  阅读(240)  评论(0编辑  收藏  举报