C++11 noexcept specifier和noexcept operator:从入门到精通

举报
码事漫谈 发表于 2025/06/17 22:38:05 2025/06/17
【摘要】 引言 异常处理回顾 noexcept说明符 基本概念 示例代码 注意事项 noexcept运算符 基本概念 示例代码 应用场景 noexcept的优势 性能优化 更好的错误处理 更安全的代码 使用场景 移动构造函数和移动赋值运算符 析构函数 不会抛出异常的函数 注意事项 总结 引言在C++编程中,异常处理是一个关键的主题。C++11引入了noexcept关键字,它既是说明符,也是运算符,为...

引言

在C++编程中,异常处理是一个关键的主题。C++11引入了noexcept关键字,它既是说明符,也是运算符,为异常处理带来了新的特性和优化。本文将详细介绍noexcept specifiernoexcept operator,帮助你从入门到精通。

异常处理回顾

在深入了解noexcept之前,我们先回顾一下C++中的异常处理机制。在C++中,通常使用trycatchthrow关键字来实现异常处理。例如:

#include <iostream>
#include <stdexcept>

void riskyFunction() {
    throw std::runtime_error("Something went wrong");
}

int main() {
    try {
        riskyFunction();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

然而,在C++11之前,C++提供了throw规范用于声明一个函数可能抛出的异常类型,例如:

void func() throw(int, double); // 表示func可能抛出int或double类型的异常

但这种规范存在一些问题,如运行时检查带来的性能开销、限制性不足等。因此,C++11引入了更高效的noexcept关键字来替代throw规范。

noexcept说明符

基本概念

noexcept说明符用于指定函数是否抛出异常。其语法格式如下:

noexcept  // noexcept等价于noexcept(true)
noexcept(expression) // expression可转换为bool的常量表达式,expression为true表示函数不会抛出异常
throw() // C++11 deprecated,C++20 removed,throw()等价于noexcept(true)

noexcept(false)表示允许抛出异常;noexcept(true) 表示不允许抛出异常,noexceptnoexcept(true)等价;标记了noexcept(true)noexcept的函数如果抛出异常了,那么std::terminate()将会调用并结束进程。

示例代码

#include <iostream>

// 不允许抛出异常
void safeFunction() noexcept {
    std::cout << "This function is noexcept" << std::endl;
}

// 允许抛出异常
void riskyFunction() noexcept(false) {
    throw std::runtime_error("Exception in riskyFunction");
}

int main() {
    try {
        safeFunction();
        riskyFunction();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

注意事项

  • C++11规定,满足某些条件的函数允许抛出异常,如使用throw声明异常(除throw()外)、noexcept(expression)expression求值为false的函数、函数声明中没有noexcept说明符的函数。
  • 同时,满足某些条件的函数不允许抛出异常,如析构函数(在特定情况下可能转为允许抛出异常)、隐式声明(或=default)的默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符、operator deleteoperator delete[]
  • noexcept(expression) 中的expression必须在编译期间可求值,编译器最终会将expression 的结果转换为truefalse再传递给noexcept()
  • C++17之前,noexcept说明符是函数签名的一部分,C++17之后不是。

noexcept运算符

基本概念

noexcept运算符用于在编译时检查,如果表达式不会抛出任何异常则返回true,否则返回false。其语法格式为:

noexcept(expression)

示例代码

#include <iostream>

void mayThrow() {
    throw std::runtime_error("error");
}

void noThrow() noexcept {
    // 不抛出异常
}

int main() {
    std::cout << std::boolalpha;
    std::cout << "mayThrow() noexcept: " << noexcept(mayThrow()) << std::endl; // false
    std::cout << "noThrow() noexcept: " << noexcept(noThrow()) << std::endl;   // true
    return 0;
}

应用场景

noexcept运算符常用于模板函数中,根据表达式是否会抛出异常来决定模板函数是否为noexcept。例如:

#include <iostream>

template <class T>
void fun() noexcept(noexcept(T())) {
    // 函数体
}

在这个例子中,fun函数是否是noexcept的,将由T()表达式是否会抛出异常所决定。

noexcept的优势

性能优化

声明为noexcept的函数可以帮助编译器进行更好的优化。编译器在生成代码时,可以省略某些与异常处理相关的检查和代码路径,从而提高程序运行效率。例如,对于标准库中的某些容器(如std::vector),当其内部使用的元素类型的移动构造函数或移动赋值运算符被声明为noexcept时,可以避免不必要的内存分配和拷贝操作,从而提高性能。

#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass() = default;
    MyClass(MyClass&&) noexcept {
        std::cout << "Move constructor called" << std::endl;
    }
    MyClass& operator=(MyClass&&) noexcept {
        std::cout << "Move assignment called" << std::endl;
        return *this;
    }
};

int main() {
    std::vector<MyClass> vec;
    vec.push_back(MyClass());  // 使用移动构造函数
    MyClass obj;
    obj = MyClass();  // 使用移动赋值运算符
    return 0;
}

更好的错误处理

使用noexcept明确声明函数的异常行为,有助于程序员在编写代码时更清楚地了解函数是否会抛出异常,从而可以更好地进行错误处理和资源管理。例如:

#include <iostream>

class MyClass {
public:
    MyClass() noexcept;            // 不抛出异常的构造函数
    MyClass(const MyClass& other); // 可能抛出异常的拷贝构造函数
};

在这个示例中,通过查看函数签名,开发者可以清楚地知道哪些操作是安全的,哪些操作可能会导致异常,从而采取相应的措施。

更安全的代码

使用noexcept声明函数不会抛出异常,可以确保某些关键操作在执行过程中不会因为异常而中断,特别是在析构函数和移动操作中,这一点尤为重要。例如:

#include <iostream>

class Widget {
public:
    ~Widget() noexcept {
        // 析构函数不会抛出异常
    }
};

使用场景

移动构造函数和移动赋值运算符

在标准库中,许多容器(如std::vector)在重新分配内存时会使用移动语义来优化性能。如果你的类的移动构造函数或移动赋值运算符不会抛出异常,那么将其标记为noexcept可以帮助容器类进行更高效的内存管理。例如:

#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
private:
    int* data;
    int size;
};

int main() {
    std::vector<MyClass> vec;
    vec.emplace_back(10);
    std::cout << "Vector capacity before push_back: " << vec.capacity() << std::endl;
    vec.push_back(std::move(vec.front()));
    std::cout << "Vector capacity after push_back: " << vec.capacity() << std::endl;
    return 0;
}

析构函数

析构函数通常不应该抛出异常,因为它们在对象生命周期结束时被调用,如果抛出异常,可能会导致程序崩溃。将析构函数标记为noexcept可以确保它们的安全调用。例如:

#include <iostream>

class MyClass {
public:
    ~MyClass() noexcept {
        // 清理资源,不抛出异常
    }
};

不会抛出异常的函数

任何不会抛出异常的函数都应该声明为noexcept,以便编译器进行优化和提高代码可读性。例如:

#include <iostream>

void doSomething() noexcept {
    // 函数内部保证不会抛出异常
    std::cout << "Doing something..." << std::endl;
}

注意事项

  • 如果一个函数被标记为noexcept但实际上抛出了异常,那么程序将调用std::terminate并立即终止。因此,在标记函数为noexcept之前,必须确保函数确实不会抛出异常。
  • 对于可能抛出异常的函数,不要随意将其标记为noexcept,这会导致未定义行为。
  • noexcept也可以用于函数模板和函数重载,以及lambda表达式和函数对象。在使用时,需要注意其在不同场景下的语法和规则。

总结

noexcept关键字在C++中扮演着重要角色,通过正确使用noexcept,可以提升程序的性能、增强代码的可读性和安全性,并且有助于编译器进行优化。在编写C++代码时,应仔细考虑每个函数是否应该声明为noexcept,以充分利用这一特性带来的优势。希望本文能帮助你更好地理解和应用noexcept关键字,编写出更加高效、健壮的C++程序。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。