Search code examples
c++exceptioninitializationlanguage-lawyer

Catching exceptions by `const` value in C++. Compilers diverge


In the following program, struct A has both copy-constructor A(const A&) and a constructor from lvalue-reference A(A&). Then an object of A is thrown and then caught as const A:

#include <iostream>

struct A {
    A() {}
    A(A&) { std::cout << "A(A&) "; }
    A(const A&) { std::cout << "A(const A&) "; }
};

int main() {
    try {
        throw A{};
    }
    catch ( const A ) {
    }
}

All compilers accept the program.

As far as I understand exception objects are never cv-qualified, and handler variables are initialized from an lvalue that refers to them. So one could expect that A(A&) constructor be preferred in catch. And indeed Clang does so.

But GCC prefers copy constructor printing A(const A&). Demo: https://gcc.godbolt.org/z/1an5M7rWh

Even more weird thing happens in Visual Studio 2019 16.11.7, which prints nothing during program execution.

Which compiler is right here?


Solution

  • In summary, clang and MSVC are both correct. GCC calls the wrong constructor.

    There are two separate objects

    To understand the required behavior, we must understand that in the following code, there are two objects:

    int main() {
        try {
            throw A{};
        }
        catch ( const A ) {
        }
    }
    

    Firstly, [except.throw] p3 states

    Throwing an exception initializes a temporary object, called the exception object. [...]

    Secondly [except.handle] p14.2 explains,

    The variable declared by the exception-declaration, of type cv T or cv T&, is initialized from the exception object, of type E, as follows:

    • [...]
    • otherwise, the variable is copy-initialized from an lvalue of type E designating the exception object.

    GCC calls the wrong constructor

    What happens is similar to:

    A temporary = throw A{};
    const A a = temporary;
    

    The fact that the variable in the handler is const doesn't affect the cv-qualifications of the temporary object because they are two separate objects. The temporary object is not const, so A(A&) is a better match during initialization. GCC is wrong.

    MSVC is allowed to perform copy elision

    Furthermore, it might possible that copy elision is performed. The standard even has an example of that in [except.throw] p7:

    int main() {
      try {
        throw C();      // calls std​::​terminate if construction of the handler's
                        // exception-declaration object is not elided
      } catch(C) { }
    }
    

    [class.copy.elision] p1.4 confirms that it's allowed, even if cv-qualifications don't match:

    [...] copy elision, is permitted in the following circumstances ([...]):

    • [...]
    • when the exception-declaration of an exception handler declares an object of the same type (except for cv-qualification) as the exception object, the copy operation can be omitted by [...]

    A and const A are not the same type, but they only differ in cv-qualification, so copy elision is allowed.