13

In More Effective C++, Scott Meyers says

C++ specifies that an object thrown as an exception is copied.

I suppose then, that if the copy constructor throws an exception in turn, std::terminate is called, so this is a good reason for declaring all my exceptions' copy constructors noexcept (and also, I guess, to not throw objects which allocate memory from the heap, like std::string).

Yet I was surprised to see that the standard library implementation shipped with GCC 4.7.1 doesn’t define those copy constructor for std::bad_alloc and std::exception. Shouldn’t they define them noexcept?

4

4 回答 4

5

第 18.8.1 节 [exception]/p1 指定:

namespace std {
    class exception {
    public:
      exception() noexcept;
      exception(const exception&) noexcept;
      exception& operator=(const exception&) noexcept;
      virtual ~exception();
      virtual const char* what() const noexcept;
  };
}

即 std::exception 的复制构造函数和复制赋值应该是noexcept,这可以通过以下方式进行测试:

static_assert(std::is_nothrow_copy_constructible<std::exception>::value, "");
static_assert(std::is_nothrow_copy_assignable<std::exception>::value, "");

即如果一个实现没有使这些成员成为noexcept,那么它在这方面是不符合的。

同样,18.6.2.1 [bad.alloc]/p1 也指定了 noexcept 副本:

namespace std {
       class bad_alloc : public exception {
       public:
         bad_alloc() noexcept;
         bad_alloc(const bad_alloc&) noexcept;
         bad_alloc& operator=(const bad_alloc&) noexcept;
         virtual const char* what() const noexcept;
  };
}

此外,所有标准定义的异常类型都具有显式或隐式的 noexcept 复制成员。对于在<stdexcept>此定义的类型,通常使用what()字符串的引用计数缓冲区来实现。这在 [exception]/p2 中有明确说明:

从类异常派生的每个标准库类T都应具有可公开访问的复制构造函数和可公开访问的复制赋值运算符,它们不会因异常而退出。...

也就是说,在一个高质量的实现中(并且在这方面创建一个高质量的实现并不需要英雄主义),异常类型的副本成员不仅不会抛出异常(自然是因为它们被标记了noexcept),它们也不会调用terminate().

复制标准定义的异常类型没有失败模式。要么没有要复制的数据,要么数据是引用计数且不可变的。

于 2014-02-08T18:08:51.803 回答
2

异常的内存分配在常规通道之外完成:

15.1 抛出异常[except.throw]

3抛出异常复制初始化(8.5,12.8)一个临时对象,称为异常对象。[...]

4异常对象的内存以未指定的方式分配,除非在 3.7.4.1 中注明。[...]

3.7.4.1 分配函数[basic.stc.dynamic.allocation]

4 全局分配函数仅作为 new 表达式的结果调用 (5.3.4),或使用函数调用语法 (5.2.2) 直接调用,或通过调用 C++ 标准库中的函数间接调用。[注意:特别是,不会调用全局分配函数来为异常对象(15.1)分配[...]的存储空间。——尾注]

大多数实现都有一个单独的内存区域,从中分配异常对象,因此即使您重新抛出std::bad_alloc异常对象,也不会要求耗尽的空闲存储本身分配复制的异常对象。所以不应该有复制本身产生另一个异常的理由。

于 2014-02-08T12:10:17.307 回答
0

好吧,声明它很好noexcept,但它要求你可以保证它不会抛出异常(对于可移植代码,在所有它的实现中!)。我希望这就是标准没有以这种方式声明的原因。

声明复制构造函数显然没有什么害处noexcept,但尝试实现这一点可能会受到很大限制。

于 2014-02-08T10:23:10.603 回答
0

它们必须是noexcept,因为 C+11 和throw()在 C++11 之前(https://en.cppreference.com/w/cpp/error/exception/exceptionhttps://en.cppreference.com/w/ cpp/memory/new/bad_alloc),以及其他标准库异常。如果它们不是except,而是throw(),这只是意味着编译器版本在该部分不支持 C++11,但根据该语言的早期版本支持。如果它们既不是noexcept也不是throw(),这意味着编译器的版本既不能正确实现 C++11 也不能正确实现该语言的早期版本。

至于

C++ 指定复制作为异常抛出的对象。

, 由于 C++11(在 C++11 之前,标准没有规范这个问题)在某些情况下允许编译器(尽管没有义务)在抛出和捕获异常时省略复制,并且在某些情况下编译器(因为 C+ +11)在抛出而不是复制时必须移动到异常对象。因此,尽管存在编译器在抛出时必须复制对象的情况,但在其他情况下,可能会发生抛出时没有复制的情况,以及尽管存在编译器必须在捕获时复制异常对象的情况,但也可能发生这种情况没有复制然后捕捉。此处详述:https ://en.cppreference.com/w/cpp/language/copy_elision 。

此外,您应该避免从构造函数中抛出异常,并移动用于异常的类型(类)的构造函数,因为它们可以在抛出异常时被调用(这里有很多东西是编译器选择的——见相同链接https://en.cppreference.com/w/cpp/language/copy_elision)并抛出额外的异常将阻止抛出最初的异常,这将丢失。抛出时复制构造函数的异常也会导致相同的结果。

至于你是否应该声明你的复制构造函数noexcept,

是的,您应该将其声明为“noexcept”——这是性能问题。但这里的主要问题是你必须保证它实际上不会抛出异常。

于 2021-08-24T17:13:00.033 回答