C++的异常处理
📌noexcept 说明符可以用于指定某个函数不抛出异常(替代 throw() )
noexcept关键字只会在编译期间影响优化方法,不会对运行期间造成任何影响
设计意图
C++11 为了替代 throw() 而提出的一个新的关键字,在 C++ 中使用函数异常声明列表来查看函数可能抛出的异常,预先知道函数不会抛出异常有助于简化调用该函数的代码,而且编译器确认函数不会抛出异常,它就能执行某些特殊的优化操作。
如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。
**C++中的异常处理是在运行时而不是编译时检测的。为了实现运行时检测,编译器创建额外的代码,然而这会妨碍程序优化。
如何使用
noexcept
规范是函数类型的一部分,因此在函数声明和定义中都需要指定。如果你在函数声明中指定了noexcept
,那么在函数定义中也必须指定noexcept
,反之亦然。如果函数声明和定义中的noexcept
规范不一致,那么编译器将会报错。
这与inline
关键字有所不同。inline
关键字只需要在函数定义中指定,而不需要在函数声明中指定。如果你在函数声明中指定了inline
,那么在函数定义中也可以指定inline
,但这并不是必需的。
以下是一个例子:
// 声明 void foo() noexcept; // 定义 void foo() noexcept { // ... }
在这个例子中,函数foo
在声明和定义中都指定了noexcept
。如果你去掉定义中的noexcept
,那么编译器将会报错。
两种异常抛出方式
在实践中,一般两种异常抛出方式是常用的:
- 一个操作或者函数可能会抛出一个异常;
- 一个操作或者函数不可能抛出任何异常。
后面这一种方式中在以往的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); }
什么时候该使用noexcept?
使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。
然而,并不是加上noexcept就能提高效率,步子迈大了也容易扯着蛋。
以下情形鼓励使用noexcept:
- 移动构造函数(move constructor)
- 移动分配函数(move assignment)
- 析构函数(destructor)
因此出于安全考虑,C++11 标准中类的析构函数默认为 noexcept(true)。
但是,如果程序员显式地为析构函数指定了 noexcept(false) 或者类的基类或成员有 noexcept(false) 的析构函数,析构函数就不会再保持默认值。 - 叶子函数(Leaf Function)
叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。
最后强调一句,在不是以上情况或者没把握的情况下,不要轻易使用noexception。
下面代码可以检测编译器是否给析构函数加上关键字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!"); }
📌正确使用 noexcept 的注意事项
尽管 noexcept 提供了一种显著提高 C++ 程序性能的方式,但我们需要明白,不是所有的函数都适合声明为 noexcept。下面是一些在使用 noexcept 时应考虑的注意事项:
- 不要假设 noexcept 函数不会失败。即使函数声明为 noexcept,也可能因为其他原因(如内存分配失败)导致函数失败。此时,如果你的代码没有正确处理这种情况,程序可能会在运行时出现问题。
- 谨慎使用 noexcept。在没有充分理由的情况下,不要轻易将函数声明为 noexcept。如果一个函数可能抛出任何类型的异常,那么它不应该被声明为 noexcept。
- 理解 noexcept 的传播规则。在 C++ 中,函数的 noexcept 属性可能会根据其参数和返回类型的 noexcept 属性变化。例如,如果一个函数的返回类型是通过移动构造函数创建的,那么该函数的 noexcept 属性将与移动构造函数的 noexcept 属性相同。
- 在可能的情况下,优先考虑 noexcept。特别是在设计类时,如果你的成员函数(特别是移动构造函数和移动赋值运算符)能够保证不抛出异常,那么将它们声明为 noexcept 可以提高代码的性能和可读性。
下面是一个示例,说明如何根据条件决定函数是否应声明为 noexcept:
template <class T, class U> void foo(T& t, U& u) noexcept(noexcept(t.swap(u))) { t.swap(u); }
在上述代码中,我们声明 foo
函数为 noexcept,但这取决于 T::swap(U&)
的 noexcept 属性。这样做的好处是,如果 T::swap(U&)
是 noexcept 的,那么 foo
函数也会是 noexcept 的。反之,如果 T::swap(U&)
可能抛出异常,那么 foo
函数也可以抛出异常。
使用 noexcept 时要保持谨慎和明智,理解其意图和后果。这将帮助你编写出更安全、更高效的代码。
📌深入理解 noexcept 及其性能优化
让我们深入研究一下 noexcept
在程序性能优化中的作用。首先,我们需要明确 noexcept
对性能的影响并非直接的,而是通过允许编译器做出某些优化来实现的。
一般来说,编译器在处理可能抛出异常的函数时需要考虑的情况更多,因此需要生成更复杂的代码,尤其是在涉及栈展开(stack unwinding)的情况下。当一个函数标记为 noexcept
时,编译器可以确保这个函数不会抛出异常,从而在生成代码时可以忽略处理异常的部分,产生更简洁、更高效的代码。
最直接的影响是编译器可能不需要生成处理异常的代码,也不需要在函数调用之后检查是否有异常被抛出。这可以减少生成的代码的大小,也可能使代码运行得更快。然而,这种优化通常是微不足道的,因为大多数函数调用的成本都远大于检查异常的成本。
另一个可能的优化是,如果编译器知道一个函数不会抛出异常,它可能更愿意将这个函数内联。这是因为异常处理代码通常不能被内联,所以如果一个函数可能抛出异常,编译器可能会选择不将其内联。然而,这种优化也是依赖于具体的编译器和优化级别的。
此外,noexcept
还可以影响 C++ 对象的移动语义。特别是在容器重排序或调整大小等操作时,如果一个对象的移动构造函数和移动赋值运算符被标记为 noexcept
,那么 C++ 运行时环境可以安全地移动这些对象,而不是进行更复杂、更耗费时间的拷贝操作。
让我们看一下一个简单的例子,说明 noexcept 如何提升性能:
void process_elements(std::vector<MyType>& elements) noexcept { for(auto& elem : elements) { // Some complex processing on elem... } // Rearrange elements for next processing phase. std::sort(elements.begin(), elements.end()); }
在上面的例子中,如果 MyType
的移动构造函数和移动赋值运算符都是 noexcept
,std::sort
可以用更有效的方法来移动元素,从而提升整体性能。
因此,noexcept
不仅表示函数的异常安全性,还可以对函数的性能产生重要影响。
重要提醒: 虽然 noexcept
可以提高性能,但我们不应滥用它。只有当你确定一个函数不会抛出异常时,才应将其声明为 noexcept
。记住,标记为 noexcept
的函数如果抛出了异常,程序将会直接调用 std::terminate()
终止运行,这通常是我们想要避免的。
结语
在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。
这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。
我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。