Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Considering this silly looking try-catch chain:

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";
}

its output will surely be (and is) what1, because it will be caught by the closest matching catch. So far so good.

However, when I try to create a constructor for a class that tries to initialise a member via member initialiser list (which will result in an exception being raised) like so:

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"; }
}

The output of the program is now:

caught1

caught2

Why is the exception being rethrown here (I assume that it is, otherwise why would two catches fire?)? Is this mandated by the standard or is it a compiler bug? I am using GCC 10.2.0 (Rev9, Built by MSYS2 project).

like image 719
Fureeish Avatar asked Jun 16 '21 22:06

Fureeish


2 Answers

cppreference has this to say about a function-try-block (which is what we have here):

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.

So there we have it. Your exception is automatically rethrown when the catch on the constructor's member initialization list exits. I guess the logic is that your constructor is deemed to have failed so (after the exception handler in the constructor performs any cleanup, perhaps) the exception is automatically propagated to the caller.

like image 95
Paul Sanders Avatar answered Nov 15 '22 17:11

Paul Sanders


While the other answer gives a great official explanation, there is also a really intuitive way to see why things have to behave this way: Consider the alternative.

I've replaced the int with a string to make the issue obvious, but the same principle applies with arithmetic types as well.

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"; }
}

What would be supposed to happen if the exception did not propagate?

Not only did arg never make it to member, but the string's constructor never got invoked at all. It's not even default constructed. Its internal state is completely undefined. So the program would be simply broken.

It's important that the exception propagates in such a way to avoid messes like this.

To pre-empt the question: Remember that the reason initializer lists are a thing in the first place is so that member variables can be initialized directly without having their default constructor invoked beforehand.

like image 32
Frank Avatar answered Nov 15 '22 18:11

Frank