条款29:为“异常安全”而努力是值得的
1、在异常抛出时,带有异常安全性的函数会:
a、不泄露任何资源
b、不允许数据败坏
2、异常安全函数(Exception-safefunctions)提供以下三个保证之一:
a、基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。然而程序的现实状态不可预料。
b、强烈保证:如果异常被抛出,程序状态不改变。
c、不抛掷(nothrow)保证:承诺绝不抛出异常,因为他们总能完成它们原先承诺的功能。
3、copy and swap会导致强烈保证。原则是为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
请记住:
- 异常安全函数(Exception-safefunctions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
- “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻了解inlining的里里外外
1、inline函数只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内:
class Person{ public: ... int age() const { return theAge; } //隐喻申请 ... private: int theAge };
这样的函数通常是成员函数,friend函数也可被定义于class内,如果真是那样,它们也是被隐喻声明为inline。
明确声明inline函数的做法则是在其定义式钱加上关键字inline。例如标准的max template:
template<typename T> inline const T& std::max(const T& a, const T& b){ return a < b ? b : a; }
2、大部分编译器拒绝将过于复杂(例如带有循环或递归)的函数inlining,而所有对虚函数的调用也都会使inlining落空。因为虚函数直到运行期才确定调用哪个函数,而内联函数意味执行前先将调用动作替换为被调用函数的本体。
3、一个表面上看似inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。编译器通常不对“通过函数指针而进行的调用”实施inlining。
4、程序库设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。例如f是程序库内的一个inline函数,客户将“f函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序必须重新编译。但如果f是non-inline函数,客户端只需重新连接即可;如果是动态链接库,升级版函数甚至可以不知不觉地被应用程序吸纳。
5、大部分调试器面对inline函数都束手无策,因为你不能在一个不存在的函数内设立断点。
请记住:
- 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
- 不要只因为function templates出现在头文件,就将它们声明为inline。
class Person{ public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthday() const; std::string address() const; ... private: std::string theName; //实现细节 Date theBirthday; //实现细节 Address theAddress; //实现细节 };
如果没有取得其实现代码所用到的class string,Date和Address的定义式,那么class Person无法通过编译。所以Person定义文件的最上方可能存在:
#include<string> #include"date.h" #include"address.h"
这么一来使得Person定义文件和其含入文件之间形成了一种编译依赖关系。如果这些头文件中有任何一个被改变,或者这些头文件依赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。
2、Handle classes可以解除接口和实现之间的耦合关系,从而降低文件间的编译依存性。
Handle classes例子:
//Address.h #ifndef ADDRESS_H #define ADDRESS_H #include<string> class Address{ public: Address(const std::string& addr) :address(addr){} std::string getAddress() const{ return address; } Address(const Address& addr) :address(addr.address){} private: std::string address; }; #endif //Date.h #ifndef DATE_H #define DATE_H class Date{ public: Date(int d, int m, int y) :day(d), month(m), year(y){} int getDay() const{ return day; } int getMonth() const{ return month; } int getYear() const{ return year; } Date(const Date& date) :day(date.day), month(date.month), year(date.year){} private: int day; int month; int year; }; #endif //PersonImpl.h #ifndef PERSONIMPL_H #define PERSONIMPL_H #include"date.h" #include"Address.h" class PersonImpl{ public: PersonImpl(const std::string& n,const Address& addr,const Date& date) :name(n),address(addr),birthday(date){} std::string getName() const{ return name; } Address getAddress() const{ return address; } Date getBirthday() const{ return birthday; } private: std::string name; Address address; Date birthday; }; #endif //Person.h #ifndef PERSON_H #define PERSON_H #include<string> #include<memory> class Date;//Person接口用到的class的前置声明 class Address;//Person接口用到的class的前置声明 class PersonImpl; //Person实现类的前置声明 class Person{ public: Person(const std::string& name, const Address& addr, const Date& birthdaty); void getName(); void getAddress(); void getBirthday(); private: std::tr1::shared_ptr<PersonImpl> pImpl; }; #endif //Person.cpp #include"Person.h" #include"PersonImpl.h" //必须#include PersonIplm的class的定义式,否则无法调用成员函数 #include<iostream> //注意PersonImpl有着和Person完全相同的成员函数,两者接口完全相同 Person::Person(const std::string& name, const Address& addr, const Date& birthdaty) :pImpl(new PersonImpl(name, addr, birthdaty)){} void Person::getName(){ std::cout << pImpl->getName() << std::endl; } void Person::getAddress(){ std::cout << pImpl->getAddress().getAddress() << std::endl; } void Person::getBirthday(){ std::cout << pImpl->getBirthday().getYear() << "." << pImpl->getBirthday().getMonth() << "." << pImpl->getBirthday().getDay() << std::endl; } //main.cpp #include"Person.h" #include"Address.h" #include"Date.h" using namespace std; int main(){ string name("Tom"); Address addr("Shanghai"); Date date(5, 8, 2015); Person p(name, addr, date); p.getName(); p.getAddress(); p.getBirthday(); system("pause"); return 0; }
Person class只内含一个指针成员,指向其实现类,这种设计被称为pimpl idiom,而Person class被称为Handle classes。这样的设计下,Peason的客户就完全与Dates,Addresses及Persons的实现细目分离了。如果修改了其他几个类的定义,那么Peason并不需要重新编译。这个分离的关键在于以“声明的依存性”替换“定义的依存性”:即让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。由此我们可以得出几个设计策略:
a、如果使用对象的引用或对象指针可以完成任务,那么就不要使用对象。因为对象的引用和指针时只需类型声明式,而定义对象时必须要有类型的定义式。
b、尽量用类声明替换类定义。声明一个函数而它用到某个类时,只需类的声明。
c、为声明式和定义式提供不同的头文件。
3、解除接口和实现之间的耦合关系,从而降低文件间的编译依存性的方法还有Interface classes。
Interface classes例子:
//Address.h #ifndef ADDRESS_H #define ADDRESS_H #include<string> class Address{ public: Address(const std::string& addr) :address(addr){} std::string getAddress() const { return address; } Address(const Address& addr) :address(addr.address){} private: std::string address; }; #endif //Date.h #ifndef DATE_H #define DATE_H class Date{ public: Date(int d, int m, int y) :day(d), month(m), year(y){} int getDay() const{ return day; } int getMonth() const{ return month; } int getYear() const{ return year; } Date(const Date& date) :day(date.day), month(date.month), year(date.year){} private: int day; int month; int year; }; #endif //Person.h #ifndef PERSON_H #define PERSON_H #include<string> #include<memory> class Date;//Person接口用到的class的前置声明 class Address;//Person接口用到的class的前置声明 class Person{ public: virtual ~Person(); virtual void getName() const = 0; virtual void getAddress() const = 0; virtual void getBirthday() const = 0; static std::tr1::shared_ptr<Person> creat(const std::string& n, const Address& addr, const Date& b); }; #endif //RealPerson.h #ifndef REALPERSON_H #define REALPERSON_H #include "date.h" #include "Person.h" #include"Address.h" #include<iostream> class RealPerson :public Person{ public: RealPerson(const std::string& n, const Address& addr, const Date& b) :name(n),address(addr), birthday(b){} virtual ~RealPerson(){} void getName() const { std::cout << name << std::endl; } void getAddress() const { std::cout << address.getAddress() << std::endl; } void getBirthday() const { std::cout << birthday.getYear() << "." << birthday.getMonth() << "." << birthday.getDay() << std::endl; } private: std::string name; Address address; Date birthday; }; #endif //Person.cpp #include"RealPerson.h" Person::~Person(){} std::tr1::shared_ptr<Person> Person::creat(const std::string& n, const Address& addr, const Date& b){ return std::tr1::shared_ptr<Person>(new RealPerson(n, addr, b)); } //main.cpp #include"RealPerson.h" #include<memory> using namespace std; int main(){ string name("Tom"); Address addr("Shanghai"); Date date(5, 8, 2015); shared_ptr<Person> pp(Person::creat(name, addr, date)); pp->getName(); pp->getAddress(); pp->getBirthday(); system("pause"); return 0; }
请记住:
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface classes。
- 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。
版权声明:本文为博主原创文章,未经博主允许不得转载。