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

1

13

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?

Fluorescence answered 23/2, 2022 at 12:29 Comment(4)
To allow for polymorphism you should be catching exceptions by reference and therefore avoid the copying anywayCampbell
For the VS2019 case, see cppreference: The copy/move (since C++11) may be subject to copy elisionSneakbox
@AdrianMole That is for the construction of the exception object, not the parameter in the handler.Deryl
Looks like Wikipedia has an example very similar to yours (without const though): en.wikipedia.org/wiki/Copy_elisionDerive
V
2

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.

Veliavelick answered 1/9, 2023 at 7:19 Comment(2)
@HansOlsson you're right. I've misinterpreted the meaning of "except for cv-qualification". I've updated the answer accordingly.Veliavelick
Thanks, GCC bug submitted: gcc.gnu.org/bugzilla/show_bug.cgi?id=104661Fluorescence

© 2022 - 2024 — McMap. All rights reserved.