From [except.throw]:
Evaluating a throw-expression with an operand throws an exception (15.1); the type of the exception object
is determined by removing any top-level cv-qualifiers from the static type of the operand and adjusting the
type from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T”,
respectively.
and, emphasis mine:
Throwing an exception copy-initializes (8.5, 12.8) a temporary object, called the exception object. The
temporary is an lvalue and is used to initialize the variable declared in the matching handler (15.3).
So if we throw an operand of type cv T, we're copy-initializing a temporary object of type T.
Then according to [except.handle], a handler for const T& (for non-pointer-type T) is matched for an exception object of type E if:
- [...] E and T are the same type (ignoring the top-level cv-qualifiers),
- [...] T is an unambiguous public base class of E
This handler is initialized by:
The variable declared by the exception-declaration, of type cv T or cv T&, is initialized from the exception
object, of type E, as follows:
— if T is a base class of E, the variable is copy-initialized (8.5) from the corresponding base class subobject
of the exception object;
— otherwise, the variable is copy-initialized (8.5) from the exception object.
So if we catch by const T&, we're copy-initializing the reference from the exception object - which we know from the previous section will be either of type T or is derived publicly from T. From [dcl.init.ref]:
A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:
— If the reference is an lvalue reference and the initializer expression
— is an lvalue (but is not a bit-field), and “cv1 T1” is reference-compatible with “cv2 T2”, or [...]
then the reference is bound to the initializer expression lvalue in the first case
The key is that the temporary exception object is still an lvalue. Thus, if our handler was matched for const T&, we know that the reference is bound directly to an object of type T or D (where D derives from T) - either way, it's a type that is reference-compatible with const T
. As such, there is no undefined behavior. If the temporary object were an rvalue or the handler could match a wider range of types, then a temporary would be created of type const T
- and your const_cast
would definitely be undefined behavior.
While your code exhibits no undefined behavior on a conforming compiler, there's really no reason not to just do:
catch(T &t)
{
t.func();
}