异常声明
相比于断言适用于排除逻辑上不可能存在的状态,异常通常是用于逻辑上可能发生的错误。
异常声明
Item 1:当函数不可能抛出异常或不能接受抛出异常时,使用noexcept
理由
如果不打算抛出异常的话,程序就会认为无法处理这种错误,并且应当尽早终止,如此可以有效地阻止异常的传播与扩散。
示例
//不可能抛出异常时
double compute(double d) noexcept
{
return log(sqrt(d <= 0 ? 1 : d));
}
这里,已知compute不会抛出异常,因为它仅由不会抛出异常的操作所组成。 通过将compute声明为noexcept,让编译器和读者获得信息,使其更容易理解和操作compute。
注解
许多标准库函数都是noexcept的,这包括所有从C标准库中“继承”来的标准库函数,C++标准库隐含地为这些函数标上了noexcept。
示例
//不能接受抛出异常时
vector<double> munge(const vector<double>& v) noexcept
{
vector<double> v2(v.size());
// ... 做一些事 ...
}
这里的noexcept表明不希望或无法处理vector对象抛出异常的情形。认为内存耗尽是一种严重的设计错误(类比于硬件故障),因此希望当其发生时让程序崩溃。
注解
在大多数程序中,大多数函数都会抛出异常(比如说,
它们可能使用new
,调用会抛出异常的函数,或者通过抛出异常
来报告失败的库函数),因此请勿随意到处散布noexcept
而不考虑清楚异常是否可以被处理。
Item 2:C++11中析构函数和内存释放函数都被默认隐式地具备noexcept性质
理由
析构函数(无论是用户自定义的,还是编译器自动生成的)不能失败,若析构函数试图抛出异常来退出,这是一种设计错误,程序最好终止执行。
示例
struct A
{
~A() {throw 1;} //隐式地具备noexcept性质
}
struct B
{
~B() noexcept(false) {throw 2;} //noexcept(false),显式地声明为可以抛出异常
}
struct C
{
B b; //成员有noexcept(false)的析构函数,同样可以抛出异常
}
注解
如果程序员显示地为析构函数指定了noexcept,或者类的基类或成员有noexcept(false)的析构函数,析构函数就不会再保持默认值(比如这里的B和C)。上例中的类A析构函数被默认为noexcept(true),可以阻止异常的扩散,而B和C均可以抛出异常。
理由
delete函数常被析构函数所调用,C++11默认将delete函数设置成noexcept,从而提高应用程序的安全性。
示例
void operator delete (void *) noexcept;
void operator delete (void *) noexcept;
Item 3:对于constexpr
函数,移动操作,swap
函数,应该加上noexcept
理由
constexpr
函数在运行时执行时可能抛出异常,因此可能需要对其中的一些使用noexcept
。
示例
//constexpr函数
constexpr int fac(int n) noexcept
{
return (n>1) ? n*fac(n-1) : 1;
}
理由
能够抛出异常的移动操作将违反大多数人的合理假设。不会抛出异常的移动操作可以更高效地被标准库和语言设施所利用。
示例
//移动操作
//正例
template<typename T>
class Vector
{
// ...
Vector(Vector&& a) noexcept :elem{ a.elem }, sz{ a.sz }
{
a.sz = 0;
a.elem = nullptr;
}
Vector& operator=(Vector&& a) noexcept
{
elem = a.elem;
sz = a.sz;
a.sz = 0;
a.elem = nullptr;
}
// ...
public:
T * elem;
int sz;
};
这些操作不会抛出异常。
//反例
template<typename T>
class Vector2
{
// ...
Vector2(Vector2&& a)
{
*this = a; // 直接利用复制操作
}
Vector2& operator=(Vector2&& a)
{
*this = a; // 直接利用复制操作
}
// ...
public:
T * elem;
int sz;
};
Vector2 不仅低效,而且由于向量的复制需要分配内存而使其可能抛出异常。
理由
swap广泛地以假定永不失败的方式被使用,而且如果存在可能失败的 swap函数的话,程序也很难编写为可以正确工作。如果元素类型的 swap会失败的话,标准库的容器和算法也无法正确工作。
示例
//swap函数
class Foo
{
// ...
public:
void swap(Foo& rhs) noexcept
{
m1.swap(rhs.m1);
std::swap(m2, rhs.m2);
}
private:
Bar m1;
int m2;
};
为调用者方便起见,可以在类型所在的相同命名空间中提供一个非成员的 swap 函数。
void swap(Foo& a, Foo& b)
{
a.swap(b);
}