c++exceptionlanguage-lawyerc++20object-slicing

Where from the standard do I read that exceptions of derived class held by base class reference are sliced when caught?


Consider this code:

#include <iostream>
#include <vector>
#include <exception>

void foo(std::exception const& e1) {
    try {
        std::cout << e1.what() << std::endl;
        throw e1;
    } catch (std::exception const& e2) {
        std::cout << e2.what() << std::endl;
    }
}

int main() {
    foo(std::runtime_error{"this is not std::exception"});
}

It will print the following

this is not std::exception
std::exception

where the second line, if I understand correctly, is not mandated by the standard.

Anyway, the code from which I distilled the snippet above is how I discovered that, despite in throw e1; e1 is an object of derived class (even though held via a base class reference), it gets sliced before reaching the handler.

What part of the standard tells me this is the expected behavior?

I think this sentence is probably relevant:

The exception object is copy-initialized ([dcl.init.general]) from the (possibly converted) operand.

Does is copy-initialized mean that the "entity" bound to e in the catch line is initialized like this

auto entityToWhich_e2_isBound = e1;

instead of like this?

auto& entityToWhich_e2_isBound = e1;

If I follow the link to [dcl.init.general], I find 14 where I read that

The initialization that occurs in […] throwing an exception ([except.throw]), handling an exception ([except.handle]) […] is called copy-initialization.

But I don't really understand how that tells me about the slicing.

In my example, e1 has a static type (std::exception) and a dynamic type (std::runtime_error), right? So I don't know exactly how to read §14.4 as regards the types E and T mentioned there.


A side, non- quesion is: why is the behavrior as it is? Is it a design choice? Or a design necessity?


Solution

  • First, note that there is an object controlled by the implementation and not explicit in the source:

    [except.throw/3]

    Throwing an exception initializes an object with dynamic storage duration, called the exception object. If the type of the exception object would be an incomplete type ([basic.types.general]), an abstract class type ([class.abstract]), or a pointer to an incomplete type other than cv void ([basic.compound]) the program is ill-formed.

    This exception object will be initialized by the throw expression throw e1 and it is the object to which the handler will bind. For this reason, you should not expect three copies of the same address in your example here. The first two are addresses for the object passed to throw, and the third is an address for the exception object. And these are going to be different because these are different objects.

    What is the type of the exception object?

    [expr.throw/2]

    A throw-expression with an operand throws an exception ([except.throw]). The array-to-pointer ([conv.array]) and function-to-pointer ([conv.func]) standard conversions are performed on the operand. The type of the exception object is determined by removing any top-level cv-qualifiers from the type of the (possibly converted) operand. The exception object is copy-initialized ([dcl.init.general]) from the (possibly converted) operand.

    The type of the expression e1 (the operand of the throw expression) is const std::exception. The rules are here, but expressions never have reference type. This is what you would get from:

    std::remove_reference_t<decltype((e1))>
    

    You have to add parentheses around a name to get the type of the expression, and decltype adds a reference depending on the value category, which you must remove if you only want the type of the expression.

    So... the operand has type const std::exception. We check array-to-pointer and function-to-pointer conversions, and neither apply. So the type of the exception object is determined by removing any top-level cv-qualifiers. Thus, the type of the exception object is std::exception.

    The object of type std::exception is copy-initialized from the operand. Therefore, it is as if we had written:

    std::exception exception_object = e1;
    

    That we are copy-initializing is not the relevant part of the paragraph. The type of the object we are initializing is the relevant part.

    the second line, if I understand correctly, is not mandated by the standard

    The string returned by std::exception::what() is implementation-defined.

    In my example, e1 has a static type (std::exception) and a dynamic type (std::runtime_error), right? So I don't know exactly how to read §14.4 as regards the types E and T mentioned there.

    E is the type of the exception object which has static and dynamic type std::exception.

    A side, non-language-lawyer quesion is: why is the behavrior as it is? Is it a design choice? Or a design necessity?

    As I noted in a comment, the reason this is natural is that there needs to be an object "on the side", not on the stack, because we are unwinding. The way in C++ we create a new object from an existing one is to copy.