为什么完全包含在 try-catch 中的构造函数中抛出的异常似乎被重新抛出?

Why does an exception thrown in a constructor fully enclosed in try-catch seem to be rethrown?

考虑到这个看起来很傻的 try-catch 链:

try {
    try {
        try {
            try {
                throw "Huh";
            } catch(...) {
                std::cout << "what1\n";
            }
        } catch(...) {
            std::cout << "what2\n";
        }
    } catch(...) {
        std::cout << "what3\n";
    }
} catch(...) {
    std::cout << "what4\n";
}

它的输出肯定是(并且是)what1,因为它会被最接近的匹配 catch 捕获。到目前为止一切顺利。

但是,当我尝试为 class 创建构造函数时 尝试 通过成员初始化列表初始化成员(这将导致引发异常) 像这样:

int might_throw(int arg) {
    if (arg < 0) throw std::logic_error("que");
    return arg;
}

struct foo {
    int member_;

    explicit foo(int arg) try : member_(might_throw(arg)) {

    } catch (const std::exception& ex) { std::cout << "caught1\n"; }
};

int main() {
    try {
        auto f = foo(-5);
    } catch (...) { std::cout << "caught2\n"; }
}

程序的输出现在是:

caught1

caught2

为什么这里会重新抛出异常(我假设是这样,否则为什么会触发两个 catches?)?这是标准强制要求的还是编译器错误?我正在使用 GCC 10.2.0(Rev9,由 MSYS2 项目构建)。

cppreference 有 this 说明函数尝试块(这是我们这里有的):

Every catch-clause in the function-try-block for a constructor must terminate by throwing an exception. If the control reaches the end of such handler, the current exception is automatically rethrown as if by throw.

好了。当构造函数的成员初始化列表中的 catch 退出时,您的异常会自动重新抛出。我猜逻辑是你的构造函数被认为失败了(在构造函数中的异常处理程序执行任何清理之后,也许)异常会自动传播给调用者。

虽然另一个答案给出了很好的官方解释,但也有一种非常直观的方法可以了解为什么事情必须这样:考虑替代方案。

我已将 int 替换为 string 以使问题显而易见,但同样的原则也适用于算术类型。

std::string might_throw(const std::string& arg) {
    if (arg.length() < 10) throw std::logic_error("que");
    return arg;
}

struct foo {
    std::string member_;

    explicit foo(const std::string& arg) try : member_(might_throw(arg)) {

    } catch (const std::exception& ex) { std::cout << "caught1\n"; }
};

int main() {
    try {
        auto f = foo("HI");

        std::cout << f.member_ << "\n"; // <--- HERE

    } catch (...) { std::cout << "caught2\n"; }
}

如果异常没有传播应该发生什么?

不仅 arg 从未达到 member,而且根本没有调用字符串的构造函数。它甚至不是默认构造的。它的内部状态是完全未定义的。所以程序会被简单地破坏。

重要的是异常的传播方式可以避免像这样的混乱。

先发制人:请记住,初始化列表之所以重要,首先是为了让成员变量可以直接初始化,而无需调用其默认构造函数事先.