Why Catch an Exception as Reference-To-Const

Why catch an exception as reference-to-const?

You need:

  • a reference so you can access the exception polymorphically
  • a const to increase performance, and tell the compiler you're not going to modify the object

The latter is not as much important as the former, but the only real reason to drop const would be to signal that you want to do changes to the exception (usually useful only if you want to rethrow it with added context into a higher level).

Is catching an exception by reference dangerous?

It is indeed safe - and recommended - to catch by const reference.

"e is actually placed on the stack of some_function()"

No it's not... the object actually thrown is created in an unspecified area of memory reserved for use by the exception handling mechanism:

[except.throw] 15.1/4: The memory for the exception object is allocated in an unspecified way, except as noted in 3.7.4.1. The exception
object is destroyed after either the last remaining active handler for the exception exits by any means other than rethrowing, or the last object of type std::exception_ptr (18.8.5) that refers to the exception object is destroyed, whichever is later.

If a local variable is specified to throw, it's copied there-to if necessary (the optimiser may be able to directly create it in this other memory). That's why...

15.1/5 When the thrown object is a class object, the constructor selected for the copy-initialization and the destructor
shall be accessible, even if the copy/move operation is elided (12.8).


If that's not clicked, it might help to imagine implementation vaguely like this:

// implementation support variable...
thread__local alignas(alignof(std::max_align_t))
char __exception_object[EXCEPTION_OBJECT_BUFFER_SIZE];

void some_function() {
// throw std::exception("some error message");

// IMPLEMENTATION PSEUDO-CODE:
auto&& thrown = std::exception("some error message");
// copy-initialise __exception_object...
new (&__exception_object) decltype(thrown){ thrown };
throw __type_of(thrown);
// as stack unwinds, _type_of value in register or another
// thread_local var...
}

int main(int argc, char **argv)
{
try {
some_function();
} // IMPLEMENTATION:
// if thrown __type_of for std::exception or derived...
catch (const std::exception& e) {
// IMPLEMENTATION:
// e references *(std::exception*)(&__exception_object[0]);
...
}
}

C++ catch blocks - catch exception by value or reference?

The standard practice for exceptions in C++ is ...

Throw by value, catch by reference

Catching by value is problematic in the face of inheritance hierarchies. Suppose for your example that there is another type MyException which inherits from CustomException and overrides items like an error code. If a MyException type was thrown your catch block would cause it to be converted to a CustomException instance which would cause the error code to change.

Catch exception by const reference and cast away const

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();
}

About catching exception good practices

You should catch by const lvalue reference (2):

try { throw std::exception{"what"}; }
catch (const std::exception& ex) {}

Rationale:

In C++11 it is possible (via use of shared_future) that two threads could be unwinding the same exception at the same time. This can happen in your code even if you are not aware of shared_future being used, unless you control the entire application.

If two threads are caught unwinding the same exception simultaneously, and one or both of the threads modifies the exception, then you've got a race condition.

So as long as you don't have to modify the exception object in the catch clause, let the compiler enforce that policy for you - catch by const&. If you really do need to modify the exception, then make a copy of it, modify the copy and throw the copy. You can do this by catching by value if you are sure this won't slice your exception object (which is not usually the case if you are catching std::exception).

Meaningless use of pass by reference syntax in catch block- exception?

You are catching the exception object by reference, but not throwing it as such.

Exceptions are always "thrown by value", because they must be allocated in a special region of your process's memory that is immune to the effects of stack unwinding.

[C++14: 15.1/3]: 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). [..]

This is a general rule that is designed to account for the far more common case in which T is actually local to either the try block itself or its encapsulating function. It would be impossible to catch it from calling scopes if it were not copied.

We catch the exception object by reference so that you don't needlessly copy again the already-copied T. It also prevents slicing when your exceptions are in an inheritance heirarchy. Sometimes people use it to mutate the exception object before re-throwing it to calling scopes, though this appears to be a rarity.

Catching it by reference-to-const has the same benefit as catching any other thing by reference-to-const: it ensures that you do not mutate the exception. If you're not rethrowing it then there's no practical benefit here but if, like me, you write const by default as a fail-safe against mistakes, there's no reason not to use it.

Catching an exception type with constructor from not-const reference in C++

This is an MSVC bug: exception objects are never cv-qualified, and handler variables are initialized from an lvalue that refers to them. (The standard doesn’t actually say what the type of that lvalue is, but there’s no reason it should be const-qualified.)



Related Topics



Leave a reply



Submit