11

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).

Fureeish
  • 12,533
  • 4
  • 32
  • 62
  • Closely related question: https://stackoverflow.com/questions/27921250/constructor-as-a-function-try-block-exception-aborts-program – 1201ProgramAlarm Jun 16 '21 at 22:33

2 Answers2

19

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.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • Huh, so this means that there **is** a difference between `int main() { try { ... } catch(...) { } }` and `int main() try { ... } catch(...) { }`? The latter cannot exit gracefully, if an exception is thrown, can it? It will be rethrown, uncaught and `std::terminate` will be called. Is that correct? – Fureeish Jun 16 '21 at 22:38
  • 1
    @Fureeish No. For a non-constructor/destructor _functiion-try-block_ you flow off the end of the function like with a `return;`. For `main`, this will return a 0, for void functions it will be OK, but with other functions that return a value it will be undefined behavior. – 1201ProgramAlarm Jun 16 '21 at 22:41
  • @Fureeish - There is nothing stopping the handler from exiting gracefully - the clause described only says what happens if control reaches the end of the handler. Practically though, there's not much to be gained by using such an approach over simply catching and handling all exceptions within the body of `main()`. – Peter Jun 17 '21 at 06:48
11

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.