首页 > 编程笔记 > C++笔记 阅读:15

C++ noexcept的用法(附带实例)

异常规范(允许你表明函数可抛出的异常类型)是语言特性,可提高性能,但如果使用不当,则会导致程序异常终止。

C++03 的异常规范已被弃用并被 C++11 新 noexcept 规范所替代。此规范只允许你表明函数不会抛出异常,而不是它所抛出的实际异常类型。

C++ noexcept的使用方式

使用以下方式来指定或查询异常规范:
1) 在函数声明中使用 noexcept 来表明函数不会抛出任何异常:
void func_no_throw() noexcept
{
}

2) 在函数声明(如模板元编程)中使用 noexcept(expr) 来表明函数可能会或可能不会基于 bool 条件抛出异常:
template <typename T>
T generic_func_1()
noexcept(std::is_nothrow_constructible_v<T>)
{
    return T{};
}

3) 在编译时,使用 noexcept 操作符检查表达式是否已声明不抛出任何异常:
template <typename T>
T generic_func_2() noexcept(noexcept(T{}))
{
    return T{};
}

template <typename F, typename A>
auto func(F&& f, A&& arg) noexcept
{
    static_assert(!noexcept(f(arg)), "F is throwing!");
    return f(arg);
}
std::cout << noexcept(func_no_throw) << '\n';

深度剖析C++ noexcept

C++17 中,异常规范是函数类型的一部分,但不是函数签名的一部分,它可作为函数说明符的一部分出现。

因为异常规范不是函数签名的一部分,所以两个函数签名不能仅仅因为异常规范而有所不同。在 C++17 之前,异常规范不是函数类型的一部分,只可作为 lambda 说明符或顶层函数说明符的一部分出现,它们甚至不能在 typedef 或类型别名声明中出现。关于异常规范的进一步讨论,可以参见 C++17 标准。

以下几种方式可指明抛出异常过程:
异常规范必须小心使用,因为如果异常(直接抛出或从另一个被调用函数中抛出)在标记为 non-throw 的函数中抛出,则程序会通过调用 std::terminate() 而立即异常终止。

指向不会抛出异常的函数的指针可隐式地转换成指向会抛出异常的函数的指针,反之则不然。另外,如果虚函数有不抛出异常规范,这意味着所有覆盖函数的声明都必须保留此规范,除非覆盖函数被声明为已删除。

在编译期间,使用操作符 noexcept 可检查函数是否声明为不抛出异常。此操作符接收一个表达式,如果表达式声明不抛出异常或布尔值为 false,则此操作符返回 true。表达式被操作符检查时,不会执行。

noexcept 操作符和 noexcept 说明符在模板元编程中特别有用,用于表明对于某些类型,函数是否会抛出异常。它也可以和 static_assert 声明一起使用来检查表达式是否会违反函数不抛出异常的保证。

关于 noexcept 操作符是如何工作的,以下代码展示了更多示例:
int double_it(int const i) noexcept
{
    return i + i;
}

int half_it(int const i)
{
    throw std::runtime_error("not implemented!");
}

struct foo
{
    foo() {}
};

std::cout << std::boolalpha
    << noexcept(func_no_throw()) << '\n'    // true
    << noexcept(generic_func_1<int>()) << '\n'    // true
    << noexcept(generic_func_1<std::string>()) << '\n'// true
    << noexcept(generic_func_2<int>()) << '\n'    // true
    << noexcept(generic_func_2<std::string>()) << '\n'  // true
    << noexcept(generic_func_2<foo>()) << '\n'    // false
    << noexcept(double_it(42)) << '\n'    // true
    << noexcept(half_it(42)) << '\n'    // false
    << noexcept(func(double_it, 42)) << '\n'    // true
    << noexcept(func(half_it, 42)) << '\n';    // true
需要指出的是,noexcept 说明符不提供异常的编译时检查。它只是告知编译器此函数不会抛出异常的一种方式。编译器可用它来做一些优化。例如 std::vector,如果 vector 元素的移动构造函数是 noexcept,则移动元素,否则就复制它们。

总结

就如之前提到的,noexcept 说明符声明的函数会因异常退出,而导致程序异常终止。因此,应当小心使用 noexcept 说明符。noexcept 可优化代码、提升性能,同时提供强异常保证。

很多标准容器提供了一些强异常保证的操作。vector 的 push_back() 方法就是一个例子。此方法可通过 vector 元素的移动构造函数或移动赋值操作符而不是复制构造函数或复制赋值操作符来进行优化。然而,为了保持其强异常保证,只有当移动构造函数或移动赋值操作符不抛出异常时才能这样做。如果其中一个会抛出异常,则必须使用复制构造函数或复制赋值操作符。

如果 std::move_if_noexcept() 工具函数的类型参数的移动构造函数标记为 noexcept,则可以这么做。noexcept 表明移动构造函数或移动赋值操作符不会抛出异常,可能是 noexcept 最重要的使用场景。

针对异常规范,考虑以下规则:
这些规则很重要,因为如前面所述,从 noexcept 函数中抛出异常会调用 std::terminate() 而立即终止程序。

相关文章