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.