【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() 终止运行,这通常是我们想要避免的。

结语

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

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

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

目录
相关文章
|
27天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
45 6
|
1月前
|
C++
C++ 多线程之线程管理函数
这篇文章介绍了C++中多线程编程的几个关键函数,包括获取线程ID的`get_id()`,延时函数`sleep_for()`,线程让步函数`yield()`,以及阻塞线程直到指定时间的`sleep_until()`。
23 0
C++ 多线程之线程管理函数
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
154 1
|
1月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
30 1
|
1月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
41 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
6天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
29 4
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
25 4
|
30天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4