【C/C++ 关键字 函数说明符 】C++ noexcept 关键字(指定某个函数不抛出异常)

简介: 【C/C++ 关键字 函数说明符 】C++ noexcept 关键字(指定某个函数不抛出异常)

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 的移动构造函数和移动赋值运算符都是 noexceptstd::sort 可以用更有效的方法来移动元素,从而提升整体性能。

因此,noexcept 不仅表示函数的异常安全性,还可以对函数的性能产生重要影响。

重要提醒: 虽然 noexcept 可以提高性能,但我们不应滥用它。只有当你确定一个函数不会抛出异常时,才应将其声明为 noexcept。记住,标记为 noexcept 的函数如果抛出了异常,程序将会直接调用 std::terminate() 终止运行,这通常是我们想要避免的。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。

目录
相关文章
|
26天前
|
安全 程序员 编译器
【C++】异常
C++异常处理机制允许在程序运行时出现错误时,通过`try`、`catch`和`throw`关键字将错误信息传递回调用栈,进行异常处理。它支持异常的重新抛出、自定义异常体系以及标准库提供的异常类层次结构,如`std::exception`及其派生类。异常处理提高了代码的健壮性和可维护性,但也带来了性能开销和代码复杂性等问题。合理使用异常机制,可以有效提升程序的稳定性和安全性。
43 3
|
4天前
|
安全 编译器 C++
C++ `noexcept` 关键字的深入解析
`noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
12 0
|
2月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
2月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
79 6
|
2月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
37 0
C++ 多线程之线程管理函数
|
2月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
38 3
|
2月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
346 1
|
2月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
60 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
24天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5