Imagine two similar pieces of code:
try { [...] } catch (myErr &err) { err.append("More info added to error..."); throw err; }
and
try { [...] } catch (myErr &err) { err.append("More info added to error..."); throw; }
Are these effectively the same or do they differ in some subtle way? For example, does the first one cause a copy constructor to be run whereas perhaps the second reuses the same object to rethrow it?
Depending on how you have arranged your exception hierarchy, re-throwing an exception by naming the exception variable in the throw statement may slice the original exception object.
A no-argument throw expression will throw the current exception object preserving its dynamic type, whereas a throw expression with an argument will throw a new exception based on the static type of the argument to throw
.
E.g.
int main() { try { try { throw Derived(); } catch (Base& b) { std::cout << "Caught a reference to base\n"; b.print(std::cout); throw b; } } catch (Base& b) { std::cout << "Caught a reference to base\n"; b.print(std::cout); } return 0; }
As written above, the program will output:
Caught a reference to base Derived Caught a reference to base Base
If the throw b
is replace with a throw
, then the outer catch will also catch the originally thrown Derived
exception. This still holds if the inner class catches the Base
exception by value instead of by reference - although naturally this would mean that the original exception object cannot be modified, so any changes to b
would not be reflected in the Derived
exception caught by the outer block.
In the second case according to C++ Standard 15.1/6 copy constructor is not used:
A throw-expression with no operand rethrows the exception being handled. The exception is reactivated with the existing temporary; no new temporary exception object is created. The exception is no longer considered to be caught; therefore, the value of uncaught_exception() will again be true.
In the first case new exception will be thrown according to 15.1/3:
A throw-expression initializes a temporary object, called the exception object, the type of which is determined by removing any top-level cv-qualifiers from the static type of the operand of throw and adjusting the type from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T”, respectively. <...> The temporary is used to initialize the variable named in the matching handler (15.3). The type of the throw-expression shall not be an incomplete type, or a pointer or reference to an incomplete type, other than void*, const void*, volatile void*, or const volatile void*. Except for these restrictions and the restrictions on type matching mentioned in 15.3, the operand of throw is treated exactly as a function argument in a call (5.2.2) or the operand of a return statement.
In both cases copy constructor is required at throw stage (15.1/5):
When the thrown object is a class object, and the copy constructor used to initialize the temporary copy is not accessible, the program is ill-formed (even when the temporary object could otherwise be eliminated). Similarly, if the destructor for that object is not accessible, the program is ill-formed (even when the temporary object could otherwise be eliminated).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With