1

I came across this article: Best practice: throw by value, catch by const reference and got curious about when an exception object is destructed.

Here are my exception structs, just as the second example in the article (with some std::cout's).

struct BASE_EX {
    static int id;
    BASE_EX() { 
        ++id; 
        std::cout << "constructing BASE_EX " << id << std::endl; 
    }
    virtual std::string const what() const { return "BASE_EX " + std::to_string(id); }
    ~BASE_EX() { std::cout << "destructing BASE_EX " << id << std::endl; }
};

struct DERIVED_EX : BASE_EX {
    static int derived_id;
    DERIVED_EX() {
        ++derived_id; 
        std::cout << "constructing DERIVED_EX " << derived_id << std::endl; 
    }
    std::string const what() const { return "DERIVED_EX " + std::to_string(derived_id); }
    ~DERIVED_EX() { std::cout << "destructing DERIVED_EX " << derived_id << std::endl; }
};

int BASE_EX::id = 0;
int DERIVED_EX::derived_id = 0;

When running this main function, catching by const&:

int main() {
    try {
        try {
            throw DERIVED_EX();
        } catch(BASE_EX const& ex) {
            std::cout << "First catch block: " << ex.what() << std::endl;
            throw ex;
        }
    } catch(BASE_EX const& ex) {
        std::cout << "Second catch block: " << ex.what() << std::endl;
    }
}

I got

constructing BASE_EX 1
constructing DERIVED_EX 1
First catch block: DERIVED_EX 1
destructing DERIVED_EX 1
destructing BASE_EX 1
Second catch block: BASE_EX 1
destructing BASE_EX 1

Question 1: If the BASE_EX is destructed before the second catch, how is it caught?

Question 2: Why is there one more destructing than constructing?

Question 3: When I changed both catches to catch by value instead of by const&, why does the output become

constructing BASE_EX 1
constructing DERIVED_EX 1
First catch block: BASE_EX 1
destructing BASE_EX 1
destructing DERIVED_EX 1
destructing BASE_EX 1
Second catch block: BASE_EX 1
destructing BASE_EX 1
destructing BASE_EX 1

Any recommended reading on how cpp try-catch works under the hood would be great. Thanks.

Facemask
  • 13
  • 3
  • 1
    The exception is notionally destructed at the end of each block it passes through, and a copy is passed on. However, the compiler may (and, in practice, typically do, in interests of efficiency) elide some - but, importantly, not necessarily all - of the copies. If the exception is eventually caught, it ceases to exist at the end of the relevant catch handler. If the exception is not caught, then the program ends by calling `std:abort()` and the destructor is not necessarily called. If you track calls of copy (or move?) constructors, you may get a better idea of what is happening. – Peter Feb 04 '21 at 04:01
  • *"If the [object] is destructed before [X], how is it [still around]?"* -- this form of question often comes up when people try to track construction and destruction but neglect the [Rule of Three](https://stackoverflow.com/questions/4172722/what-is-the-rule-of-three). Let's see... yep, that would be the case here. – JaMiT Feb 04 '21 at 04:42
  • Does this answer your question? [Scope of Exception object when caught(using catch) by reference](https://stackoverflow.com/questions/33188891/scope-of-exception-object-when-caughtusing-catch-by-reference) – JaMiT Feb 04 '21 at 04:52
  • Thank you @Peter and JaMiT. I think the two links JaMiT shared together answer my question. – Facemask Feb 05 '21 at 01:53

2 Answers2

1

If you're going to mark the constructors of an object, mark all of them ;)

struct BASE_EX {
    static int count;
    int id;
    BASE_EX() : id(count++) { 
        std::cout << "constructing BASE_EX " << id << std::endl; // usually std::endl is unnecessary (it's just "\n" followed by std::flush), but since we're playing with crashes it's probably a good idea
    }
    BASE_EX(BASE_EX const &other) : id(count++) {
        std::cout << "copying BASE_EX " << other.id << " as BASE_EX " << id << std::endl;
    }
    // implicit move constructor not declared
    virtual std::string what() const { return "BASE_EX " + std::to_string(id); } // marking by-value return as const does absolutely nothing
    ~BASE_EX() { std::cout << "destructing BASE_EX " << id << std::endl; } // reminder that base class destructors should generally be virtual; not required in this case
};
int BASE_EX::count = 0;

struct DERIVED_EX : BASE_EX {
    static int count;
    int id;
    DERIVED_EX() : BASE_EX(), id(count++) {
        std::cout << "constructing DERIVED_EX " << id << std::endl; 
    }
    DERIVED_EX(DERIVED_EX const &other) : BASE_EX(other), id(count++) {
        std::cout << "copying DERIVED_EX " << other.id << " as DERIVED_EX " << id << std::endl;
    }
    // implicit move constructor not declared
    std::string what() const override { return "DERIVED_EX " + std::to_string(id); }
    ~DERIVED_EX() { std::cout << "destructing DERIVED_EX " << id << std::endl; }
};
int DERIVED_EX::count = 0;

You get

constructing BASE_EX 0
constructing DERIVED_EX 0
First catch block: DERIVED_EX 0
copying BASE_EX 0 as BASE_EX 1
destructing DERIVED_EX 0
destructing BASE_EX 0
Second catch block: BASE_EX 1
destructing BASE_EX 1

The first throw sets the exception object to be DERIVED_EX 0. The inner catch gets a reference to the BASE_EX 0 base class subobject of that exception object. Since what is virtual, calling it causes the DERIVED_EX to report its type. However, when you throw ex again, ex only has static type BASE_EX, so the new exception object is chosen to be a BASE_EX, and it is created by copying only the BASE_EX part of the first exception object. That first exception object is destroyed as we exit the first catch, and the outer catch receives the new BASE_EX object. Since it really is a BASE_EX, not a DERIVED_EX, calling what reflects that. If you make both catchs by-value, you get

constructing BASE_EX 0
constructing DERIVED_EX 0
copying BASE_EX 0 as BASE_EX 1
First catch block: BASE_EX 1
copying BASE_EX 1 as BASE_EX 2
destructing BASE_EX 1
destructing DERIVED_EX 0
destructing BASE_EX 0
copying BASE_EX 2 as BASE_EX 3
Second catch block: BASE_EX 3
destructing BASE_EX 3
destructing BASE_EX 2

When you catch by-value, the exception object is copied to initialize the catch parameter. During the execution of such a catch block, there are two objects representing the exception: the actual exception object, which has no name, and the copy of it made for the catch block, which may be named. The first copy is the copy of the first exception object to the first catch's parameter. The second copy is the copy of that parameter as the second exception object. The third is the copy of that exception object into the second catch's parameter. The DERIVED_EX part of the exception has been sliced off by the time we enter the first catch. The catch parameters are destroyed upon the end of each catch, by the usual rules of scoping. The exception objects are destroyed whenever the corresponding catch block exits.

You avoid the copying issues and the slicing issues by not taking exceptions by value and not using throw <catch-parameter> to rethrow exceptions.

int main() {
    try {
        try {
            throw DERIVED_EX();
        } catch(BASE_EX const &ex) {
            std::cout << "First catch block: " << ex.what() << std::endl;
            throw;
        }
    } catch(BASE_EX const &ex) {
        std::cout << "Second catch block: " << ex.what() << std::endl;
    }
}

gives

constructing BASE_EX 0
constructing DERIVED_EX 0
First catch block: DERIVED_EX 0
Second catch block: DERIVED_EX 0
destructing DERIVED_EX 0
destructing BASE_EX 0

The exception object is not destroyed at the end of the first catch because it exits with throw, which signals for the same exception object to be used to match more catch clauses. It's not copied into a new exception object and destroyed like throw ex would call for.

Please see cppreference for a detailed description of the rules.

HTNW
  • 27,182
  • 1
  • 32
  • 60
0

The best way to understand when they get destroyed is to pretend that the catch clase is a function:

catch(BASE_EX const& ex) {
    std::cout << "Second catch block: " << ex.what() << std::endl;
}

Make a temporary adjustment, and pretend that this is a function, and ex is simply a parameter to this function:

void exception_handler(BASE_EX const& ex) {
    std::cout << "Second catch block: " << ex.what() << std::endl;
}

The exception object is going to get destroyed whenever this pseudo-function parameter would get destroyed, if the exception handler is entered as if it was an ordinary function call, here.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148