您的位置:首页 > 编程语言 > C语言/C++

Effective C++ 读书笔记之implemenations(2)

2008-12-10 18:57 357 查看
Item 28:Avoid returning "handles" to object internals(避免返回内部对象的句柄)

例如,有一个矩形类rectangle,需要表示它的四个顶点,可以定义以下类:

class point
{
public:
point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};

class RectData
{
point ulhc; //upper left-hand corner
point lrhc; //lower right-hand corner
};

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

有时候客户需要取得矩形的顶点,因此可以定义以下成员函数:
class Rectangle
{
...
private:
std::tr1::shared_ptr<RectData> pData;

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

上述代码可以编译,但是存在错误:自相矛盾。定义为常量函数的目的是为了不让用户修改,但上述函数返回的是指向私有数据的句柄。因此即可以修改,又访问了私有的数据。例如:
point coord1(0,0);
point coord2(100,100);
const Rectangle rec(coord1, coord2); //rec

rec.upperLeft().setX(50); //not we expected.

上面讲的是引用(reference),对指针(pointer)以及迭代器(Vector)也是一样的,它们都是句柄(handles)。

禁止修改的办法是定义函数的返回类型为const,即:
class Rectangle
{
...
private:
std::tr1::shared_ptr<RectData> pData;

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

尽管如此,上面的函数仍然返回指向内部对象的句柄,因此仍然会存在问题。特别的,可能会指向空的句柄。
例如:某个函数返回GUI对象的外框(bounding box):
class GUIObject {...};

const Rectangle boundingBox(const GUIObject& obj);

现在用户使用该函数:
GUIObject* pgo;
...
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft()); //取得一个指针指向外框左上点

但是一旦pgo对象被销毁,则指向左上点的句柄不复存在,也即pUpperLeft成为空的指针(dangle handles空悬的指针)。这也就是为什么说返回一个指向内部对象的句柄总是危险的。但也并不是说绝对不能使用这种方式,有时候必须这样做。比如Operator []允许你返回strings或者vector的单个元素,那么这些数据会随着容器的销毁而被销毁。

Item 29:Strive for exception-safe code(为异常安全努力是值得的)

考虑一个实现GUI菜单的类,该类可以修改菜单的背景图片,另外该类被设计用于一个多线程的环境中,因此有一个互斥体mutex用于并发控制:

class PrettyMenu
{
public:
...
void changeBackground(std::istream& imgSrc); //change background image
...
private:
Mutex mutex; //mutex for this object
Image* bgImage; //Current background image
int imageChanges; //# of times image has been changed.
};

下面是changeBackground的实现:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex); //acquire mutex
delete bgImage; //get rid of old background
++imageChanges; //update image change count
bgImage = new Image(imgSrc); //install new background
unlock(&mutex); //release mutex
}

从exception-safe的角度,上面的代码不好。

exception-safe通常要满足两个要求:
不泄露资源。上面的代码中,如果new Image(imgSrc)失败了而产生异常,那么远unlock将永远不会执行,mutex永远不会被release;
不要让数据结构corrupted. 如果new Image(imgSrc)失败了而throw exception,那么bgImage将指向一个deleted object. 此外,imageChanges已经增加了。

对于第一个要求,比较好满足。在Item 13中讲到了利用资源管理的对象,Item 14中又介绍了Lock类来确保mutex可以被及时释放。
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex); //see Item 14

delete bgImage; //get rid of old background
++imageChanges; //update image change count
bgImage = new Image(imgSrc); //install new background
}
象Lock这样的资源管理类的最大一个好处就是使得函数代码短了,比如上面不再需要unlock了。
通常来说,短代码就是较好的代码,因为短不容易出错。

现在回来第二个问题,数据的完整性。首先,给出数据完整性的三种不同保证:
函数提供基本的保证(basic guarantee): 如果产生异常,程序的一切保证都在有效状态。但是有效状态有时是不可预测的,比如上面异常发生时,PrettyMenu可能继续显示原来的背景图片,也可能显示缺省的背景图片,用户对此是无法作出预测的。
函数提供很强的保证(Strong Guarantee):如果产生异常,程序的状态是不变的。换句话说,函数的操作是”原子“操作,要么全部成功,要么完全不成功。显然,这种方式容易控制,因此程序只有两种可选的状态;
函数绝不产生异常(nothrow guarantee):换句话说,函数总是能够完成它们原先承诺的功能。注意,也许我们会把程序抛出空的异常(empty exception)当成无异常(nothrow exception)。这是不对的,例如 int doSomthing() throw();这并不代表doSomthing不产生异常,而是说如果

异常安全代码必须抛出以上三种保证的一种。理想情况是最后一种,但是很难实现。因此通常会选择前两种。
对于上面的PrettyMenu类来说,要提供Strong Guarantee并不是太难,只需要将PrettyMenu's bgImage数据成员从built-in Image Pointer变为smart resource-managing pointers. 其次,将imageChanges的顺序改变一下。
class PrettyMenu
{

private:
...
std::tr1::shared_ptr <Image> bgImage; //Current background image
...
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex); //see Item 14
bgImage.reset(new Image(imgSrc)); //Replace bgImage's internal pointer with smart one.

++imageChanges; //update image change count
}

但是上面的实现还是有一个问题,即参数imageSrc。如果Image的构造器抛出一个异常,例如可能是读输入流的标记移除了,因此此时并不能提供强保证,只能提供基本保证。

对于这一点,一个通常的作法叫做"copy and swap",其思想是将打算修改的对象作一个附本,在副本上再做修改,一旦抛出异常,则原对象没有发生改变。如果修改成功,则再将副本和原对象对调即可。这种方法叫"pimpl idiom”,在Item 31 中有描述。

Struct PMImpl
{
std::tr1::shared_ptr<Image> bgImage;
int imageChanges;
};

class PrettyMenu
{

private:
Mutex mutex;
std::tr1::shared_ptr <PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Using std::swap; //See Item 25
Lock m1(&mutex); //see Item 14

std::tr1::shared_ptr <PMImpl> pNew(new PMImpl(*pImpl)); //copy obj. data
pNew->bgImage.reset(new Image(imgSrc)); //modify copy
++pNew->imageChanges;
swap(pImpl, pNew); //swap the new data into place
}

"copy and swap" 是对对象状态作出"全有或全无"改变的一个很好的办法,但是它没有办法提供强保证的异常安全,原因在于内嵌函数的“连带影响(Side effect)”。例如:下面函数包含两个子函数.
void SomeFunc()
{
... //对local状态作备份
f1();
f2();
... //将修改后的状态转换回来
};

很显然, 如果f1,f2的异常安全性比”强烈保证“低,则SomeFunc也难以实现”强烈保证“。此外,实现强烈保证另一个问题在于效率,copy-and swap的关键在于副本及交换,因此需要时间和空间上的消耗。

当”强烈保证“不切实际时,就必须提供基本保证。对许多函数来说,提供异常安全性之基本保证”是合理的,也是必须的。另外,如果系统中有一个函数不具备异常安全性,则整个系统也不具备安全性。在实际实现中,首先要调用“对象管理资源类”以阻止资源泄漏,然后挑选一个“异常安全性保证"来实施在你写的函数上。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: