从C++11开始,我们能看到很多代码当中都有关键字noexcept。比如下面就是std::initializer_list的默认构造函数,其中使用了noexcept。
constexpr initializer_list() noexcept
: _M_array(0), _M_len(0) { }
该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。
如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。
C++中的异常处理是在运行时而不是编译时检测的。为了实现运行时检测,编译器创建额外的代码,然而这会妨碍程序优化。
在实践中,一般两种异常抛出方式是常用的:
一个操作或者函数可能会抛出一个异常;
一个操作或者函数不可能抛出任何异常。
后面这一种方式中在以往的C++版本中常用throw()表示,在C++ 11中已经被noexcept代替。
void swap(Type& x, Type& y) throw() //C++11之前
{
x.swap(y);
}
void swap(Type& x, Type& y) noexcept //C++11
{
x.swap(y);
}
在第2节中单独使用noexcept,表示其所限定的swap函数绝对不发生异常。然而,使用方式可以更加灵活,表明在一定条件下不发生异常。
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y))) //C++11
{
x.swap(y);
}
它表示,如果操作x.swap(y)不发生异常,那么函数swap(Type& x, Type& y)一定不发生异常。
一个更好的示例是std::pair中的移动分配函数(move assignment),它表明,如果类型T1和T2的移动分配(move assign)过程中不发生异常,那么该移动构造函数就不会发生异常。
pair& operator=(pair&& __p)
noexcept(__and_,
is_nothrow_move_assignable<_T2>>::value)
{
first = std::forward(__p.first);
second = std::forward(__p.second);
return *this;
}
使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上noexcept就能提高效率,步子迈大了也容易扯着蛋。
以下情形鼓励使用noexcept:
移动构造函数(move constructor)
移动分配函数(move assignment)
析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。下面代码可以检测编译器是否给析构函数加上关键字noexcept。
struct X
{
~X() { };
};
int main()
{
X x;
// This will not fire even in GCC 4.7.2 if the destructor is
// explicitly marked as noexcept(true)
static_assert(noexcept(x.~X()), "Ouch!");
}
叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。
最后强调一句,在不是以上情况或者没把握的情况下,不要轻易使用noexception。
在c++ 17异常处理规范成为函数类型的一部分。也就是说,下面两个函数现在有两种不同的类型:
void f1();
void f2() noexcept; // different type
在c++ 17之前,这两个函数都具有相同的类型。
因此,编译器现在将检测如果你使用一个函数抛出异常,而一个函数不抛出任何异常的情况:
void (*fp)() noexcept; // pointer to function that doesn’t throw
fp = f2; // OK
fp = f1; // ERROR since C++17
当然,在允许抛出函数的地方使用不抛出的函数仍然是有效的:
void (*fp2)(); // pointer to function that might throw
fp2 = f2; // OK
fp2 = f1; // OK
因此,这个新特性不会破坏那些还没有使用noexcept函数指针的程序,但是现在可以确保您不再违反函数指针中的noexcept规范。
但是,不允许重载具有不同异常规范的同一签名的函数名(因为只允许重载具有不同返回类型的函数):
void f3();
void f3() noexcept; // ERROR
注意,所有其他规则都不受影响。例如,仍然不允许忽略基类的noexcept规范:
class Base {
public:
virtual void foo() noexcept;
...
};
class Derived : public Base {
public:
void foo() override; // ERROR: does not override
...
};
在这里,派生类中的成员函数foo()具有不同的类型,因此它不会覆盖基类的foo()。这段代码仍然不能编译。即使没有覆盖说明符,这段代码也无法编译,因为我们仍然不能使用更松散的抛出规范进行重载。
当使用条件noexcept规范时,函数的类型取决于条件是真还是假:
void f1();
void f2() noexcept;
void f3() noexcept(sizeof(int)<4); // same type as either f1() or f2()
void f4() noexcept(sizeof(int)>=4); // different type than f3()
这里,f3()的类型取决于编译代码时条件的类型:
如果sizeof(int)等于4(或大于4),则函数签名为
void f3() noexcept(false);//与f1()类型相同
如果sizeof(int)小于4,则函数签名为
void f3() noexcept(true); // same type as f2()
因为f4()的异常条件使用了f3()的相反表达式,所以f4()总是有不同的类型(即,它保证在f3()不抛出时抛出,反之亦然。
以前的throw规范仍然可以使用,但自从c++ 17以来就被弃用了:
void f5() throw(); // same as void f5() noexcept but deprecated
不再支持动态抛出规范(自c++ 11以来一直不支持):
void f6() throw(std::bad_alloc); // ERROR: invalid since C++17
对于泛型库,只使用类型的except声明部分可能会产生一些后果。
例如,下面的程序在c++ 14之前是有效的,但是不再用c++ 17编译:
#include
template<typename T>
void call(T op1, T op2)
{
op1();
op2();
}
void f1()
{
std::cout << "f1()\n";
}
void f2() noexcept
{
std::cout << "f2()\n";
}
int main()
{
call(f1, f2); // ERROR since C++17
}
问题是,由于c++ 17 f1()和f2()具有不同的类型,所以编译器在实例化函数模板调用()时不再为这两种类型找到一个公共类型T。
在c++ 17你必须使用两种不同的类型:
template<typename T1, typename T2>
void call(T1 op1, T2 op2)
{
op1();
op2();
}
如果希望或必须重载所有可能的函数类型,那么现在必须加倍重载。
例如,这适用于标准类型trait std::is_function<>的定义。定义了主模板,因此通常类型T没有函数:
// primary template (in general type T is no function):
template<typename T> struct is_function : std::false_type { };
模板派生自std::false_type,因此is_function::value对于任何类型的T通常都会产生false。
对于所有属于函数的类型,都存在部分专门化,它们派生自std::true_type,使成员值为true:
// partial specializations for all function types:
template<typename Ret, typename... Params>
struct is_function<Ret (Params...)> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) &> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const &> : std::true_type { };
......
在c++ 17之前,已经有24个局部专门化,因为函数类型可以有const和volatile限定符,也可以有lvalue(&)和rvalue(&&)引用限定符,而且需要重载带有可变参数列表的函数。
在c++ 17中,通过在所有这些局部专门化中添加noexcept限定符,局部专门化的数量增加了一倍,现在我们得到了48个局部专门化:
...
// partial specializations for all function types with noexcept:
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) noexcept> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const noexcept> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) & noexcept> : std::true_type { };
template<typename Ret, typename... Params>
struct is_function<Ret (Params...) const& noexcept> : std::true_type { };
...
没有实现noexcept重载的库可能不再编译代码,这些代码使用它们将函数或函数指针传递到不需要noexcept的地方。