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 catch
s 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.