noexcept
是c++11新引入的关键字,取代c++98的throw()
的异常规格,虽然c++的异常规格被大家嫌弃并弃用,但将一定不会发出异常的函数申明成noexcept
还是能一定程度上带来代码效率的提升。
noexcept能怎样带来效率的提升?
- 如果一个函数可能抛出异常,即没有申明
noexcept
的情况下,在运行期会开解调用栈,而如果是加了noexcept
的函数,则是可能开解调用栈,这一点点的区别会帮助编译器生成效率更高的代码。 - 有些特殊情景声明
noexcept
会带来额外的增益。考虑std::vector
,该容器在元素超过容量值时,会开辟一个新的更大空间,把原来的元素复制到新空间后,再往里填新元素。那么如果把这个复制变成移动的话是不是就可以大幅提高效率。这个移动的前提是保证移动操作不会产生异常,因为若复制操作有异常,那至少原数据还在,不会被破坏,但如果移动有异常,那就无法恢复数据了,这是编译器不愿意做的。那如何让编译器知道移动操作不会抛出异常呢?答案就是检查元素的移动构造函数有没有申明noexcept
。因此,将移动操作声明成noexcept
往往有额外正收益。
使用noexcept需要注意什么?
- 将函数申明成
noexcept
后,因为涉及一些兼容性问题,编译器不会阻止你调用可能抛出异常的函数,连警告都不会。 - 将函数申明成
noexcept
后,若果抛出了异常,那么程序就会被直接终止,调用try catch
都没用。 - 需保证此函数永远不会抛出异常,这是对客户端的一个长期承诺,因为
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
重要点总结:
- 默认生成的或使用
= default
显示声明的析构函数都自带noexcept
属性,除非它的基类或成员变量的析构函数不带noexcept
。 - 默认生成的或使用
= 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
,就会调用HasA
的noexcept(false)
的异常规格的移动构造函数,但此接口被删除了,所以会直接编译报错,此现象再次说明了noexcept
是接口规格的一项,一旦敲定了就不要随意更改。