条款二十八:避免返回handlers指向对象内部成分
class Student
{
private:
int ID;
string name;
public:
string& GetName()
{
return name;
}
};
这是一个学生的类,类里面有两个成员变量,一个是学生ID,用整数表示,另一个是姓名,用string表示。有一个公有的方法GetName(),获得学生的名字,根据条款20所说的,使用引用可以防止资源不必要地拷贝,那么在返回值这边就用string&。但现在问题来了,这个函数只是想返回学生的姓名,并不想用户对之进行修改,但返回引用却提供了这样的一个接口,如:
int main()
{
Student s;
s.GetName() = "Jerry";
cout << s.GetName() << endl;
}
就可以把名字进行修改。
你也许想到了,如果在前面加上const,像这样:
const string& GetName()
{
return name;
}
就可以阻止s.GetName() = “Jerry”这样的代码了。
但这样写还是存在问题,就是如果返回的引用生命周期比对象本身要长时,引用就会悬空,它会指向一个不存在的string。下面看一下“返回的引用生命周期比对象本身要长”的情况,这种情况还是很容易举出例子的,比如:
const string& fun()
{
return Student().GetName();
}
int main()
{
string name = fun(); //name指向一个不存的对象的成员变量
}
这时候即使name读取不报错,也是一个巨大的隐患,因为它已经是虚吊(dangling)的了。
这就是为什么函数如果“返回一个handle代表对象内部成分”总是危险的原因,不在于返回值是不是const,而是在于如果handle(指针或引用)传出去了,就会暴露在“handle比其所指对象更长寿”的风险下。
但有些情况还是需要返回handle的,比如string或者vector里面的operator[],就是返回的引用,因为需要对这里面的元素进行操作。
好了,总结一下:
避免返回handles(包括reference、指针、迭代器)指向对象内部,遵守这个条款可增加封装性,并将发生dangling handles的可能性降至最低。如果有必要必须要返回handles,在编写代码时就一定要注意对象和传出handle的生命周期。
条款二十九:为“异常安全”而努力是值得的
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++ imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}
这段代码大致的意思就是改变背景图片,删掉旧的背景图片,记录修改次数,然后创建新的背景图片。考虑到多线程操作,所以这里用了lock和unlock。
但这里会出现问题,因为并不是每次new都会成功的,有可能抛出异常,一旦抛出异常,unlock就没有执行了,这样资源就会一直处于lock状态,而无法继续操作了。另一方面,虽然本次改变背景的操作的失败了,但imageChanges仍然自增了一次,这就不符合程序员设计的初衷了。
有读者就会想,那还不简单,加上try…catch块就行了,像这样:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
try
{
lock(&mutex);
delete bgImage;
bgImage = new Image(imgSrc);
++ imageChanges;
unlock(&mutex);
}
catch (Exception* e)
{
unlock(&mutex);
}
}
在catch里面写上unlock函数,另外,调换imageChanges的位置,在new之后再对齐自增。这样做固然可以,但回想一下,我们在条款十三和条款十四做了资源管理类,让类的析构函数自动替我们完成这个操作,不是会更好吗?像这样:
class PrettyMenu
{
…
shared_ptr<Image> bgImage;
…
}
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc));
++ imageChanges;
}
这样,即使抛出了异常,锁资源还有imageChanges都保证是异常发生之前的状态。
现在上升到理论的高度,异常安全性要做到:
- 不泄漏任何资源
- 不允许数据败坏
带异常安全性的函数会提供三个保证之一:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或者数据结构会因此被破坏。比如上例中本次更换背景图失败,不会导致相关的数据发生破坏。
- 强烈保证:在基本承诺的基础上,保证成功就是完全成功,失败也能回到之前的状态,不存在介于成功或失败之间的状态。
- 不抛出异常:承诺这个代码在任何情况下都不会抛出异常,但这只适用于简单的语句。
强烈保证有一种实现方法,那就是copy and swap。原则就是:在修改这个对象之前,先创建它的一个副本,然后对这个副本进行操作,如果操作发生异常,那么异常只发生在这个副本之上,并不会影响对象本身,如果操作没有发生异常,再在最后进行一次swap。
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
弄一个临时的tempBgImage
对tempBgImage进行操作
swap(tempBgImage, bgImage);
++ imageChanges;
}
copy-and-swap策略关键在于“修改对象数据的副本,然后在一个不抛异常的函数中将修改后的数据和原件置换”。它确实提供了强异常安全保障,但代价是时间和空间,因为必须为每一个即将被改动的对象造出副本。另外,这种强异常安全保障,也会在下面的情况下遇到麻烦:
void someFunc()
{
f1();
f2();
}
f1()和f2()都是强异常安全的,但万一f1()没有抛异常,但f2()抛了异常呢?是的,数据会回到f2()执行之前的状态,但程序员可能想要的是数据回复到f1()执行之前。要解决这个问题就需要将f1与f2内容进行融合,确定都没有问题了,才进行一次大的swap,这样的代价都是需要改变函数的结构,破坏了函数的模块性。如果不想这么做,只能放弃这个copy-and-swap方法,将强异常安全保障回退成基本保障。
类似于木桶效应,代码是强异常安全的,还是基本异常安全的,还是没有异常安全,取决于最低层次的那个模块。换言之,哪怕只有一个地方没有考虑到异常安全,整个代码都不是异常安全的。
最后总结一下:
- 异常安全函数是指即使发生异常也不会泄漏资源或者允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
- 强烈保证往往可以通过copy-and-swap实现出来,但“强烈保证”并非对所有函数都可以实现或具备现实意义。
- 异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者。
条款三十:了解inline的里里外外
学过基本程序课的同学都知道,inline是内联的关键字,它可以建议编译器将函数的每一个调用都用函数本体替换。这是一种以空间换时间的做法。把每一次调用都用本体替换,无疑会使代码膨胀,但可以节省函数调用的成本,因为函数调用需要将之前的参数以堆栈的形式保存起来,调用结束后又要从堆栈中恢复那些参数。
但注意inline只是对编译器的一个建议,编译器并不表示一定会采纳,比如当一个函数内部包含对自身的递归调用时,inline就会被编译器所忽略。对于虚函数的inline,编译器也会将之忽略掉,因为内联(代码展开)发生在编译期,而虚函数的行为是在运行期决定的,所以编译器忽略掉对虚函数的inline。对于函数指针,当一个函数指针指向一个inline函数的时候,通过函数指针的调用也有可能不会被编译器处理成内联。
另一方面,即使有些函数没有inline关键字,编译器也会将之内联,用本体替换调用代码,比如直接写在class内部的成员函数,如下:
class Person
{
private:
int age;
public:
int getAge() const
{
return age;
}
void setAge(const int o_age);
};
void Person::setAge(const int o_age)
{
age = o_age;
}
这里getAge()尽管没有inline关键字,但因为是直接写在class里面的,所以编译器将之处理成内联的;setAge()是在类内声明、类外定义的,编译器就不会将之处理成内联的了。
构造函数和析构函数虽然“看”似简单,但编译器会在背后做很多事情,比如一个空的构造函数里面会由编译器写上对所有成员函数的初始化,如果将之inline,将会导致大批量的代码复制,所以不对构造函数和析构函数inline为好。
要慎用inline,是因为一旦编译器真的将之inline了,那么这个inline函数一旦被修改,整个程序都需要重新编译,而如果这个函数不是inline的,那么只要重新连接就好。另外,一些调试器对inline函数的支持也是有限的。
作者认为,“一开始先不要将任何函数声明为inline”,经测试,确实发现某个函数的inline要比不对之inline的性能提升很多,才对之inline。在大多数情况下,inline并不是程序的瓶颈,真正的精力应该放在改善一些算法的修缮,以及反复调用的代码研究上,它们往往才是耗时的瓶颈所在。
最后总结一下:
- 将大多数inlining限制在小型、被频繁调用的函数身上
- 不要只因为function templates出现在头文件,就将它们声明为inline(这个内容知道就行了,我在读书笔记的正文中没有说)