C++11 noexcept 关键字用法学习

本文介绍了C++中的异常规范和noexcept关键字。异常规范在C++98中用于指定函数可能抛出的异常类型,但在C++11中被废弃,转而采用noexcept来声明函数是否可能抛出异常。noexcept(true)表示函数不会抛出异常,noexcept(false)或不带noexcept则表示可能抛出异常。不正确的noexcept声明可能导致std::terminate调用,因此使用时需谨慎。文章还讨论了noexcept对编译器优化的影响以及在构造函数、析构函数等特殊情况下的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近学习和写了一个 mint 的板子 ,其中用到了 noexcept 关键字,对这个关键字不太熟悉,便学习一下刘毅学长的文章。

C++98 中的异常规范(Exception Specification)

傲芙小说网 https://www.3973.info

throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范,有些教程也称为异常指示符或异常列表。请看下面的例子:

double func1 (char param) throw(int);

函数 func1 只能抛出 int 类型的异常。如果抛出其他类型的异常,try 将无法捕获,并直接调用 std::unexpected。

如果函数会抛出多种类型的异常,那么可以用逗号隔开,

double func2 (char param) throw(int, char, exception);

如果函数不会抛出任何异常,那么只需写一个空括号即可,

double func3 (char param) throw();

同样的,如果函数 func3 还是抛出异常了,try 也会检测不到,并且也会直接调用 std::unexpected。

虚函数中的异常规范

C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。请看下面的例子:

class Base {
  public:
    virtual int fun1(int) throw();
    virtual int fun2(int) throw(int);
    virtual string fun3() throw(int, string);
};

class Derived: public Base {
  public:
    int fun1(int) throw(int);    //错!异常规范不如 throw() 严格
    int fun2(int) throw(int);    //对!有相同的异常规范
    string fun3() throw(string); //对!异常规范比 throw(int, string) 更严格
}

异常规范与函数定义和函数声明

C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。请看下面的几组函数:

// 错!定义中有异常规范,声明中没有
void func1();
void func1() throw(int) { }

// 错!定义和声明中的异常规范不一致
void func2() throw(int);
void func2() throw(int, bool) { }

// 对!定义和声明中的异常规范严格一致
void func3() throw(float, char *);
void func3() throw(float, char *) { }

异常规范在 C++11 中被摒弃

异常规范的初衷是好的,它希望让程序员看到函数的定义或声明后,立马就知道该函数会抛出什么类型的异常,这样程序员就可以使用 try-catch 来捕获了。如果没有异常规范,程序员必须阅读函数源码才能知道函数会抛出什么异常。

不过这有时候也不容易做到。例如,func_outer() 函数可能不会引发异常,但它调用了另外一个函数 func_inner(),这个函数可能会引发异常。再如,编写的一个函数调用了老式的一个库函数,此时不会引发异常,但是老式库更新以后这个函数却引发了异常。

其实,不仅仅如此,

  1. 异常规范的检查是在运行期而不是编译期,因此程序员不能保证所有异常都得到了 catch 处理。

  2. 由于第一点的存在,编译器需要生成额外的代码,在一定程度上妨碍了优化。

  3. 模板函数中无法使用。比如下面的代码,

    template<class T>
    void func(T k) {
        T x(k);
        x.do_something();
    }
    

    赋值函数、拷贝构造函数和 do_something() 都有可能抛出异常,这取决于类型 T 的实现,所以无法给函数 func 指定异常类型。

  4. 实际使用中,我们只需要两种异常说明:抛异常和不抛异常,也就是 throw(...) 和 throw()。

所以 C++11 摒弃了 throw 异常规范,而引入了新的异常说明符 noexcept。

C++11 noexcept

noexcept 紧跟在函数的参数列表后面,它只用来表明两种状态:"不抛异常" 和 "抛异常"。

void func_not_throw() noexcept; // 保证不抛出异常
void func_not_throw() noexcept(true); // 和上式一个意思

void func_throw() noexcept(false); // 可能会抛出异常
void func_throw(); // 和上式一个意思,若不显示说明,默认是会抛出异常(除了析构函数,详见下面)

对于一个函数而言,

  1. noexcept 说明符要么出现在该函数的所有声明语句和定义语句,要么一次也不出现。
  2. 函数指针及该指针所指的函数必须具有一致的异常说明。
  3. 在 typedef 或类型别名中则不能出现 noexcept。
  4. 在成员函数中,noexcept 说明符需要跟在 const 及引用限定符之后,而在 final、override 或虚函数的 =0 之前。
  5. 如果一个虚函数承诺了它不会抛出异常,则后续派生的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则派生类的虚函数既可以抛出异常,也可以不允许抛出异常。

需要注意的是,编译器不会检查带有 noexcept 说明符的函数是否有 throw

void func_not_throw() noexcept {
    throw 1; // 编译通过,不会报错(可能会有警告)
}

这会发生什么呢?程序会直接调用 std::terminate,并且不会栈展开(Stack Unwinding)(也可能会调用或部分调用,取决于编译器的实现)。另外,即使你有使用 try-catch,也无法捕获这个异常。

#include <iostream>
using namespace std;

void func_not_throw() noexcept {
    throw 1;
}

int main() {
    try {
        func_not_throw(); // 直接 terminate,不会被 catch
    } catch (int) {
        cout << "catch int" << endl;
    }
    return 0;
}

所以程序员在 noexcept 的使用上要格外小心!

noexcept 除了可以用作说明符(Specifier),也可以用作运算符(Operator)。noexcept 运算符是一个一元运算符,它的返回值是一个 bool 类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。例如,

void f() noexcept {
}

void g() noexcept(noexcept(f)) { // g() 是否是 noexcept 取决于 f()
    f();
}

其中 noexcept(f) 返回 true,则上式就相当于 void g() noexcept(true)

析构函数默认都是 noexcept 的。C++ 11 标准规定,类的析构函数都是 noexcept 的,除非显示指定为 noexcept(false)

class A {
  public:
    A() {}
    ~A() {} // 默认不抛出异常
};

class B {
  public:
    B() {}
    ~B() noexcept(false) {} // 可能会抛出异常
};

在为某个异常进行栈展开的时候,会依次调用当前作用域下每个局部对象的析构函数,如果这个时候析构函数又抛出自己的未经处理的另一个异常,将会导致 std::terminate。所以析构函数应该从不抛出异常。

显示指定异常说明符的益处

  1. 语义

    从语义上,noexcept 对于程序员之间的交流是有利的,就像 const 限定符一样。

  2. 显示指定 noexcept 的函数,编译器会进行优化

    因为在调用 noexcept 函数时不需要记录 exception handler,所以编译器可以生成更高效的二进制码(编译器是否优化不一定,但理论上 noexcept 给了编译器更多优化的机会)。另外编译器在编译一个 noexcept(false) 的函数时可能会生成很多冗余的代码,这些代码虽然只在出错的时候执行,但还是会对 Instruction Cache 造成影响,进而影响程序整体的性能。

  3. 容器操作针对 std::move 的优化

    举个例子,一个 std::vector<T>,若要进行 reserve 操作,一个可能的情况是,需要重新分配内存,并把之前原有的数据拷贝(copy)过去,但如果 T 的移动构造函数是 noexcept 的,则可以移动(move)过去,大大地提高了效率。

    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    class A {
      public:
        A(int value) {
        }
    
        A(const A &other) {
            std::cout << "copy constructor
    ";
        }
    
        A(A &&other) noexcept {
            std::cout << "move constructor
    ";
        }
    };
    
    int main() {
        std::vector<A> a;
        a.emplace_back(1);
        a.emplace_back(2);
    
        return 0;
    }
    

    上述代码可能输出:

    move constructor
    

    但如果把移动构造函数的 noexcept 说明符去掉,则会输出:

    copy constructor
    

    你可能会问,为什么在移动构造函数是 noexcept 时才能使用?这是因为它执行的是 Strong Exception Guarantee,发生异常时需要还原,也就是说,你调用它之前是什么样,抛出异常后,你就得恢复成啥样。但对于移动构造函数发生异常,是很难恢复回去的,如果在恢复移动(move)的时候发生异常了呢?但复制构造函数就不同了,它发生异常直接调用它的析构函数就行了。

使用建议

我们所编写的函数默认都不使用,只有遇到以下的情况你再思考是否需要使用,

  1. 析构函数

    这不用多说,必须也应该为 noexcept。

  2. 构造函数(普通、复制、移动),赋值运算符重载函数

    尽量让上面的函数都是 noexcept,这可能会给你的代码带来一定的运行期执行效率。

  3. 还有那些你可以 100% 保证不会 throw 的函数

    比如像是 int,pointer 这类的 getter,setter 都可以用 noexcept。因为不可能出错。但请一定要注意,不能保证的地方请不要用,否则会害人害己!切记!如果你还是不知道该在哪里用,可以看下准标准库 Boost 的源码,全局搜索 BOOST_NOEXCEPT,你就大概明白了。

参考

  • C++ throw 关键字(抛出异常+异常规范)
  • Deprecated throw-list in C++11
  • Exceptions
  • C++11的noexcept标识符与操作符应如何正确使用?
  • When noexcept?
  • Why does reallocating a vector copy instead of moving the elements?
  • When should I really use noexcept?
  • Rvalue References and Exception Safety
<think>嗯,用户想了解noexcept的作用,我需要用中文来解释清楚。首先,我得回想一下C++中的noexcept关键字。这个关键字主要和异常处理相关,对吧?我记得它有两个用途:作为异常说明符和运算符。 作为说明符的时候,noexcept用来声明函数不会抛出异常。那为什么要这么做呢?可能和编译器优化有关。如果编译器知道函数不会抛出异常,可能会生成更高效的代码,减少异常处理的额外开销。另外,标准库里的某些函数,比如vector的移动操作,可能会根据noexcept来优化行为,比如在调整大小时使用移动而非复制,前提是移动构造函数是noexcept的。这样能提升性能。 然后是作为运算符的noexcept,用来检查表达式是否会抛出异常。返回的是bool类型,可以在编译时判断。比如在模板元编程中,可能需要根据某个操作是否noexcept来选择不同的实现策略,这样可以在保证正确性的同时优化代码。 用户可能是个C++开发者,对异常处理机制有一定了解,但想深入理解noexcept的具体应用场景和优势。可能他在写高性能代码,或者遇到需要优化移动操作的情况,比如容器中的元素类型移动时是否标记noexcept会影响容器的行为。需要举例说明,比如移动构造函数加上noexcept后,vector的resize可能更高效。 另外要注意的是,如果函数用noexcept声明但实际上抛出了异常,程序会直接终止,而不是展开栈。这可能带来风险,所以需要强调正确使用的重要性。用户可能需要知道什么时候该用noexcept,比如析构函数、移动操作等通常不应该抛出异常的地方。 总结下来,回答的结构应该是先分点说明noexcept的两个作用,再解释各自的应用场景和好处,最后提醒注意事项。这样用户能清晰地理解其用途和正确使用方法。</think>`noexcept` 是 C++11 引入的关键字,主要用于异常处理相关的优化和约束。它的核心作用分为两方面:**作为异常说明符**和**作为运算符**。 --- ### 1. **作为异常说明符(Exception Specifier)** 当 `noexcept` 修饰函数时,表示该函数**不会抛出异常**。若违反此约定(实际抛出异常),程序会直接调用 `std::terminate()` 终止,而不是展开调用栈。 #### 用途: - **优化性能** 编译器知道函数不会抛出异常后,可以移除异常处理相关的额外开销(如栈展开的逻辑),生成更高效的代码。 - **指导容器操作** 标准库的某些操作(如 `vector` 的扩容)会根据类型的移动构造函数是否标记为 `noexcept` 决定使用“移动”还是“拷贝”。例如: ```cpp class MyClass { public: MyClass(MyClass&& other) noexcept { ... } // 标记为 noexcept }; ``` 若移动构造函数是 `noexcept`,`vector` 扩容时会优先移动元素而非拷贝,提升性能。 - **显式约定接口** 明确告知调用者该函数的安全性,避免异常传播的意外。 --- ### 2. **作为运算符(Operator)** `noexcept(expression)` 是一个编译时运算符,用于判断某个表达式是否可能抛出异常。返回 `bool` 值,常用于模板或条件编译。 #### 用途: - **条件性异常声明** 根据表达式的结果动态决定函数是否 `noexcept`: ```cpp void func() noexcept(noexcept(other_operation())) { // 当 other_operation() 不抛异常时,func() 才是 noexcept } ``` - **模板元编程** 在泛型代码中,根据类型操作是否 `noexcept` 选择不同实现: ```cpp template<typename T> void swap(T& a, T& b) noexcept(noexcept(T(std::move(a))) && noexcept(a.operator=(std::move(b)))) { // 仅在 T 的移动构造和赋值不抛异常时,swap 才是 noexcept } ``` --- ### 注意事项 - **谨慎使用**:若函数标记为 `noexcept` 但实际抛出异常,程序会直接终止,可能引发资源泄漏。 - **默认行为**:移动构造函数/赋值运算符、析构函数默认隐含 `noexcept`,除非显式指定可能抛出异常。 - **替代旧语法**:取代了 C++98 的 `throw()`,但语义更严格(`throw()` 仍有运行时检查成本)。 --- ### 总结 `noexcept` 的主要价值在于: 1. 为编译器提供优化机会。 2. 为标准库容器/算法提供更高效的选择依据。 3. 通过编译时检查增强代码的健壮性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值