C++ noexcept关键字详解

noexcept是c++11新引入的关键字,取代c++98的throw()的异常规格,虽然c++的异常规格被大家嫌弃并弃用,但将一定不会发出异常的函数申明成noexcept还是能一定程度上带来代码效率的提升。

noexcept能怎样带来效率的提升?

  1. 如果一个函数可能抛出异常,即没有申明noexcept的情况下,在运行期会开解调用栈,而如果是加了noexcept的函数,则是可能开解调用栈,这一点点的区别会帮助编译器生成效率更高的代码。
  2. 有些特殊情景声明noexcept会带来额外的增益。考虑std::vector,该容器在元素超过容量值时,会开辟一个新的更大空间,把原来的元素复制到新空间后,再往里填新元素。那么如果把这个复制变成移动的话是不是就可以大幅提高效率。这个移动的前提是保证移动操作不会产生异常,因为若复制操作有异常,那至少原数据还在,不会被破坏,但如果移动有异常,那就无法恢复数据了,这是编译器不愿意做的。那如何让编译器知道移动操作不会抛出异常呢?答案就是检查元素的移动构造函数有没有申明noexcept。因此,将移动操作声明成noexcept往往有额外正收益。

使用noexcept需要注意什么?

  1. 将函数申明成noexcept后,因为涉及一些兼容性问题,编译器不会阻止你调用可能抛出异常的函数,连警告都不会。
  2. 将函数申明成noexcept后,若果抛出了异常,那么程序就会被直接终止,调用try catch都没用。
  3. 需保证此函数永远不会抛出异常,这是对客户端的一个长期承诺,因为noexcept关键字涉及接口规格,就如同const一样,如果以后改变的话,就会破坏客户端代码,例如客户端知道此函数会发出异常,因此用了try catch企图处理异常,但之后你改变主意,为此函数加了noexcept,那么客户端在原来try catch的时候,程序就直接崩溃了。抑或时移动构造函数改变此异常规格,那客户端如果依赖了此异常规格,那么代码甚至都不会通过编译,后文详细阐述。

类默认生成的特殊函数的异常规格

我们知道,编译器会为我们默认生成一些特殊函数,参考C++类中默认生成的函数详解,这些隐式生成的函数,或显示声明为= default的函数也会有自带的异常规格,这里的异常规格指的是函数有没有带noexcept。截取cppreference的一段说明

Every function in C++ is either non-throwing or potentially throwing:

  • potentially-throwing functions are:
    • functions declared with noexcept specifier whose expression evaluates to false
    • functions declared without noexcept specifier except for
      • destructors unless the destructor of any potentially-constructed base or member is potentially-throwing
      • default constructors, copy constructors, move constructors that are implicitly-declared or defaulted on their first declaration unless
        • a constructor for a base or member that the implicit definition of the constructor would call is potentially-throwing
        • a subexpression of such an initialization, such as a default argument expression, is potentially-throwing
        • a default member initializer (for default constructor only) is potentially-throwing (see below)
      • copy assignment operators, move assignment operators that are implicitly-declared or defaulted on their first declaration unless the invocation of any assignment operator in the implicit definition is potentially-throwing (see below)
      • deallocation functions

重要点总结:

  1. 默认生成的或使用= default显示声明的析构函数都自带noexcept属性,除非它的基类或成员变量的析构函数不带noexcept
  2. 默认生成的或使用= default显示声明的默认构造函数,拷贝构造函数和移动构造函数都自带noexcept属性,除非它的基类或成员变量在默认构造,移动构造或拷贝构造时调用的构造函数不带noexcept

考虑以下代码

class A {};
class HasA {
 public:
  HasA() = default;
  HasA(const HasA&) = default;
  HasA(HasA&&) = default;

 public:
  std::vector<A> as{100};
  int foo = 0;
};

问题1:HasA的默认构造函数带不带noexcept?
答:不带,可以用noexcept操作符来判断,没错,其实noexcept除了是个关键字以外,还是一个操作符,用来判断一个函数带不带noexcept属性,方法如下

std::cout << "HasA default noexcept? " << noexcept(HasA()) << std::endl;

打印结果:0,为什么不带?因为成员有一个std::vector<A> as{100};,这会调用std::vector的一个带参构造方法。直接贴出std::vector源码

      /**
       *  @brief  Creates a %vector with no elements.
       */
      vector()
#if __cplusplus >= 201103L
      noexcept(is_nothrow_default_constructible<_Alloc>::value)
#endif
      : _Base() { }

      /**
       *  @brief  Creates a %vector with no elements.
       *  @param  __a  An allocator object.
       */
      explicit
      vector(const allocator_type& __a) _GLIBCXX_NOEXCEPT
      : _Base(__a) { }

#if __cplusplus >= 201103L
      /**
       *  @brief  Creates a %vector with default constructed elements.
       *  @param  __n  The number of elements to initially create.
       *  @param  __a  An allocator.
       *
       *  This constructor fills the %vector with @a __n default
       *  constructed elements.
       */
      explicit
      vector(size_type __n, const allocator_type& __a = allocator_type())
      : _Base(__n, __a)
      { _M_default_initialize(__n); }

      /**
       *  @brief  Creates a %vector with copies of an exemplar element.
       *  @param  __n  The number of elements to initially create.
       *  @param  __value  An element to copy.
       *  @param  __a  An allocator.
       *
       *  This constructor fills the %vector with @a __n copies of @a __value.
       */
      vector(size_type __n, const value_type& __value,
	     const allocator_type& __a = allocator_type())
      : _Base(__n, __a)
      { _M_fill_initialize(__n, __value); }
#else
      /**
       *  @brief  Creates a %vector with copies of an exemplar element.
       *  @param  __n  The number of elements to initially create.
       *  @param  __value  An element to copy.
       *  @param  __a  An allocator.
       *
       *  This constructor fills the %vector with @a __n copies of @a __value.
       */
      explicit
      vector(size_type __n, const value_type& __value = value_type(),
	     const allocator_type& __a = allocator_type())
      : _Base(__n, __a)
      { _M_fill_initialize(__n, __value); }
#endif

      /**
       *  @brief  %Vector copy constructor.
       *  @param  __x  A %vector of identical element and allocator types.
       *
       *  All the elements of @a __x are copied, but any unused capacity in
       *  @a __x  will not be copied
       *  (i.e. capacity() == size() in the new %vector).
       *
       *  The newly-created %vector uses a copy of the allocator object used
       *  by @a __x (unless the allocator traits dictate a different object).
       */
      vector(const vector& __x)
      : _Base(__x.size(),
	_Alloc_traits::_S_select_on_copy(__x._M_get_Tp_allocator()))
      {
	this->_M_impl._M_finish =
	  std::__uninitialized_copy_a(__x.begin(), __x.end(),
				      this->_M_impl._M_start,
				      _M_get_Tp_allocator());
      }

#if __cplusplus >= 201103L
      /**
       *  @brief  %Vector move constructor.
       *  @param  __x  A %vector of identical element and allocator types.
       *
       *  The newly-created %vector contains the exact contents of @a __x.
       *  The contents of @a __x are a valid, but unspecified %vector.
       */
      vector(vector&& __x) noexcept
      : _Base(std::move(__x)) { }

      /// Copy constructor with alternative allocator
      vector(const vector& __x, const allocator_type& __a)
      : _Base(__x.size(), __a)
      {
	this->_M_impl._M_finish =
	  std::__uninitialized_copy_a(__x.begin(), __x.end(),
				      this->_M_impl._M_start,
				      _M_get_Tp_allocator());
      }

可以看出std::vector的带参构造函数是不带noexcept的,因为HasA在默认构造的时候会调用std::vector的这个带参构造,它不是noexcept,所以HasA的默认构造不是noexcept

问题2:HasA的拷贝构造函数带不带noexcept?
答:不带,原因同问题1,因为std::vector的拷贝构造不带noexcept

问题3:HasA的移动构造函数带不带noexcept?
答:带,判断方法如下

std::cout << "HasA move noexcept? " << noexcept(HasA(std::declval<HasA>())) << std::endl;

为什么要用HasA(std::declval<HasA>()),用HasA(Has())不行吗,不行,两个问题
1. A(A())会遭遇恼人的解析语法,会强制让他变成一个函数申明,类似void(fun());。要避免解析语法的话,我们可以知道可以采用{}初始化,因此需先改写成A(A{});
2. 即便改写成A(A{})的话,但编译器还是会将此写法优化成A(),强制改为调用一次默认构造函数。因此要想达到目的,只能用std::declval。元函数std::declval的作用是强制创建一个类的转发引用(万能引用)转发引用可以保留左值或右值引用的属性。std::declval即便没有构造函数,或构造函数不匹配,也可以创建对象。

接着打印结果看,结果是true,为什么是true,因为成员的std::vector的移动构造函数声明了noexcept,见源码。

再考虑以下代码

void Baz() noexcept(noexcept(Bar(std::declval<Bar>()))) {
  //
}

此代码说明noexcept可以串联使用

再考虑以下代码

class Bar : public HasA {
 public:
  Bar() = default;
  Bar(Bar&&) noexcept(false) = default;
};

此代码编译会报错,因为HasA的移动构造函数是noexcept,而Bar的移动构造函数显示声明了= default,就会调用HasAnoexcept(false)的异常规格的移动构造函数,但此接口被删除了,所以会直接编译报错,此现象再次说明了noexcept是接口规格的一项,一旦敲定了就不要随意更改。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值