c++ 11 noexcept

1 关键字noexcept

从C++11开始,我们能看到很多代码当中都有关键字noexcept。比如下面就是std::initializer_list的默认构造函数,其中使用了noexcept。

  constexpr initializer_list() noexcept
  : _M_array(0), _M_len(0) { }

该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。
如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。

2 C++的异常处理

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);
}

3 有条件的noexcecpt

在第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<_T1>,
                is_nothrow_move_assignable<_T2>>::value)
{
    first = std::forward<first_type>(__p.first);
    second = std::forward<second_type>(__p.second);
    return *this;
}

4 什么时候该使用noexcept?

使用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。

使用noexcept注意事项

在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()。这段代码仍然不能编译。即使没有覆盖说明符,这段代码也无法编译,因为我们仍然不能使用更松散的抛出规范进行重载。

1. 使用条件异常规范

当使用条件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

2. 对通用库的影响

对于泛型库,只使用类型的except声明部分可能会产生一些后果。
例如,下面的程序在c++ 14之前是有效的,但是不再用c++ 17编译:

#include <iostream>
 
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的地方。

  • 5
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值