目录
1.概述
2.noexcept作为说明符
3.noexcept作为运算符
4.传统throw与noexcept比较
5.原理剖析
6.总结
1.概述
在C++中,noexcept
是一个关键字,用于指定函数不会抛出异常。如果函数保证不会抛出异常,编译器可以进行更多优化,比如防止异常传播时的栈展开,或者生成更有效的代码。
C++11之前我们可以使用throw抛出异常,但是随着C++11中移动语义的产生,throw不能很好的解决移动语义过程中的异常处理,不同于拷贝,移动的过程如果出错的话不能保证原来的数据是否受到影响,为此C++11引入了noexcept关键字来处理这个问题。
C++11后,逐渐形成“函数要么可能发射异常,要么保证不会发生异常”的共识。并提出了关键字noexcept用于指明函数保证自己不会发生异常。
示例如下:
constexpr int myAdd(int a, int b) noexcept {
return a + b;
}
把noexcept关键字放在函数声明和定义的括号后,表示该函数不会抛出任何异常。若函数抛出异常,则程序会终止。
如果想要允许该程序抛出异常,则可在noexcept后添加false:
constexpr int myAdd(int a, int b) noexcept (false) {
return a + b;
}
此时允许函数抛出异常。
2.noexcept作为说明符
函数使用noexcept声明时,会告诉编译器这个函数不会发生异常,进而让编译器使用效率更高的检测代码,如果真的发生异常时程序会调用terminate函数直接结束程序。
noexcept还可以接收一个bool类型的参数,该参数必须是一个常量表达式用以决定函数是否可以抛出异常,默认是true。如:
void myFunction() noexcept {
// 这个函数不会抛出异常
}
//带参数
template <class T>
void myFunction() noexcept(std::is_class<T>::value){}
noexcept也可以用于重载解析:
void myFunction() {
// 这个函数可能会抛出异常
}
void myFunction() noexcept {
// 这个函数不会抛出异常
}
noexcept既可以表征普通函数不发射异常,也可以用于表征成员函数不发射异常。
//普通函数
int add(int a, int b)noexcept
{
return a+b;
}
//成员函数
class People {
public:
People(std::string name, int age) :m_name{ name }, m_age{ age } {}
~People()noexcept = default;
inline const int GetAge()const noexcept
{
return m_age;
}
inline const std::string GetName()const noexcept
{
return m_name;
}
private:
std::string m_name{ "" };
int m_age{ 0 };
};
3.noexcept作为运算符
作为运算符接收表达式参数返回bool类型,判断表达式是否可以抛出异常。
void no_exception()noexcept
{
throw true;
}
void exception()
{
throw true;
}
void myfunc()
{
int a=10;
int b=10;
exception();
int c= a+b;
}
int test_noexcept_oper() {
std::cout<<std::boolalpha<<noexcept(no_exception())<<"\n"
<<noexcept(exception())<<"\n"
<<noexcept(myfunc())<<"\n";
return 0;
}
移动构造函数和移动赋值运算符通常被设计为不抛出异常,因为它们只是“窃取”资源而不是复制它们。在这些函数上使用noexcept
可以确保它们不会抛出异常,并允许编译器进行额外的优化。
class MyClass {
public:
// 移动构造函数
MyClass(MyClass&& other) noexcept : /* 初始化列表 */ {
// 实现资源的移动
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
// 实现资源的移动
return *this;
}
};
解决移动语义的风险
1)遇到风险直接编译报错
template<class T>
void swap(T& a,T& b)
noexcept(noexcept(T(std::move(a))))&&
noexcept(noexcept(T(std::move(b))))
{
static_assert(noexcept(T(std::move(a)))&&
noexcept(T(std::move(b))));
T tmp(std::move(a));
a=std::move(b);
b=std::move(tmp);
}
2)遇到风险改用拷贝
template<class T>
void swap_impl(T& a,T& b,std::integral_constant<bool,true>)noexcept{
T tmp(std::move(a));
a=std::move(b);
b=std::move(tmp);
}
template<class T>
void swap_impl(T& a,T& b,std::integral_constant<bool,false>){
T tmp(a);
a=b;
b=tmp;
}
template<class T>
void swap(T& a,T& b)
noexcept(noexcept(swap_impl(a,b
std::integral_constant<bool,noexcept(T(std::move(a)))&&
noexcept(a.operator=(std::move(b)))>())))
{
swap_impl(a,b,
std::integral_constant<bool,noexcept(T(std::move(a)))&&
noexcept(a.operator=(std::move(b)))>());
}
4.传统throw与noexcept比较
在C++中,传统的throw
异常机制和noexcept
说明符提供了两种处理运行时错误的方法,它们在用途和目的上有明显的不同。下面是关于这两者之间比较的一些关键点:
传统的throw
异常机制
-
错误处理:
throw
语句用于在代码中抛出一个异常,表示发生了一个运行时错误或异常情况。这允许程序员在代码的多个位置中定义可能的错误情况,并在一个集中的错误处理位置(通常是catch
块)中处理它们。 -
传播性:当异常被抛出时,它会沿着调用栈向上传播,直到找到一个能够处理该异常的
catch
块。如果没有找到合适的catch
块,程序将调用std::terminate()
并终止。 -
灵活性:异常机制允许程序员在多个不同的函数或方法之间传播错误,而不必依赖返回值或错误代码来指示错误。这使得代码更加清晰和易于维护。
-
性能开销:异常处理机制在运行时有一定的性能开销,因为它涉及到堆栈展开、异常表查找和可能的函数调用(对于
catch
块)。因此,在性能敏感的代码区域中频繁使用异常可能会导致性能下降。
noexcept
说明符
-
异常保证:
noexcept
说明符用于指示一个函数是否可能抛出异常。当函数被标记为noexcept
时,它承诺不会抛出任何异常(除非它是由另一个noexcept(false)
的函数调用引起的)。 -
优化机会:当编译器知道一个函数是
noexcept
时,它可以执行一些优化,例如消除额外的异常处理代码,从而提高程序的执行速度。 -
提高异常安全性:如果一个
noexcept
函数确实抛出了异常,程序将调用std::terminate()
并终止。这有助于确保在资源转移或关键操作期间不会因异常而导致资源泄漏或其他未定义行为。 -
设计约束:
noexcept
提供了一种方式来限制函数的异常行为,这在设计高性能或低延迟的系统时尤为重要。通过使用noexcept
,程序员可以确保某些关键操作不会因异常而中断。
比较
- 用途:
throw
用于在代码中抛出异常以指示错误情况,而noexcept
用于指示函数是否可能抛出异常。 - 性能:
throw
/catch
机制在运行时有一定的性能开销,而noexcept
则可以提高性能,因为它允许编译器进行额外的优化。 - 错误处理:
throw
/catch
提供了一种灵活的错误处理机制,允许程序员在多个不同的函数或方法之间传播错误。而noexcept
则更多地关注于函数的异常行为约束和性能优化。 - 设计考虑:在设计高性能或低延迟的系统时,
noexcept
可能是一个重要的考虑因素,因为它可以确保关键操作不会因异常而中断。而在设计更一般性的库或应用程序时,throw
/catch
可能更为适用。
5.原理剖析
noexcept保证函数不会发射异常,那么noexcept是如何保证的呢?为了分析这个问题,不妨让noexcept函数抛出异常,同时让普通函数抛出异常作为对照组,对比分析两个函数的行为。验证代码及行为如下:
//当noexcept函数触发异常时,会直接在函数内抛出异常的位置中断,异常未扩散。
//已在 xxx.exe 中执行断点指令(__debugbreak()语句或类似调用)。
void no_exception()noexcept
{
throw true;
}
//当常规函数触发异常时会提示异常;
//0x00007FFA2D8F543C 处(位于 xxxx.exe 中)有未经处理的异常:
// Microsoft C++ 异常: bool,位于内存位置 0x0000005B28B3F444 处。
void exception()
{
throw true;
}
由如上行为可知,noexcept函数在触发异常时直接中断,异常自然无法向外发射(传递)。
正是由于其不向外发射异常特性,为编译器提供了更大的舞台。
-
更大的优化空间:因为noexcept标注的函数,其异常不会向外传递,自然也就不存在开解调用栈(开解调用栈是指在异常处理、函数返回或程序终止过程中,系统自动执行的调用栈回溯和资源清理行为),也就给编译器更大的优化空间。
-
提升性能:vector的push_back函数在扩容时,如果移动构造函数是noexcept形式时(is_nothrow_move_constructible_v)将使用移动来转移原有数据,而非之前的拷贝完成再删除的方式。使用“能移动则移动,必须拷贝再拷贝”的策略来提升性能。
注意事项
-
只有在时间维度上恒为不发射异常的函数才可标注为noexcept,否则不要做出该函数noexcept的假设。
-
如果函数标注为noexcept,则该函数调用的所有函数应也是noexcept,否则不要做出该函数noexcept的假设。尽管noexcept调用非noexcept函数会通过编译但不推荐这样做。
-
不要为了使函数满足noexcept而修改函数,大可不必。
-
释放内存的函数和析构函数默认为noexcept
6.总结
综上所述,C++11通过引入noexcept
关键字、智能指针、移动语义等特性,以及增强STL和其他库,显著提高了C++的异常安全性。这些特性使程序员能够更好地管理资源、避免资源泄漏和未定义行为,并在异常发时代码的健壮性和可靠性。
noexcept