2

Is this valid as an exception handler, where T is some class with a non-const member function func?

In other words: is the catch guaranteed to bind directly to the (modifiable) exception object, or is there latitude for the compiler to do some trickery when you catch by const reference?

catch(const T &t)
{
    const_cast<T &>(t).func();
}
M.M
  • 138,810
  • 21
  • 208
  • 365
  • 1
    Whyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy?? – Lightness Races in Orbit Jul 20 '15 at 21:45
  • because Matt is apparently a language lawyer – dufresnb Jul 20 '15 at 21:46
  • @dufresnb: That is absolutely no reason to write code like this. – Lightness Races in Orbit Jul 20 '15 at 21:48
  • @LightnessRacesinOrbit so I can respond definitively to someone else who wrote this code – M.M Jul 20 '15 at 21:49
  • @MattMcNabb: Haha okay then. Still. The answer is "you're safe but stupid" so just answer with confidence and let that be that. – Lightness Races in Orbit Jul 20 '15 at 21:52
  • Why do you have a `const` after the `catch` to begin with? – fredoverflow Jul 20 '15 at 21:59
  • @fredoverflow some people recommend to catch exceptions by const reference (although I'm yet to see a good explanation for this) – M.M Jul 20 '15 at 22:09
  • @MattMcNabb: Not when you want to mutate them, no. – Lightness Races in Orbit Jul 20 '15 at 22:09
  • @LightnessRacesinOrbit: In the case in question, the `func()` is not being used to mutate the exception, only to read some data from it, but `func()` itself is not declared as `const`. – Remy Lebeau Jul 20 '15 at 23:46
  • @RemyLebeau: That design flaw notwithstanding, from an outside perspective I _must_ treat it as though it's a mutation. That's literally what lack of `const` means on a member function. If it doesn't actually modify any data then that's a design flaw but I can't and won't reason about that externally. :) – Lightness Races in Orbit Jul 20 '15 at 23:54
  • @LightnessRacesinOrbit: In this case, the object type being thrown is coming from a library that uses very little `const`-correctness in C++, and in this case class member functions are never declared as `const`. – Remy Lebeau Jul 21 '15 at 00:03
  • @RemyLebeau: (a) Yeah, that's all well and good, I'm just saying that does not come into the picture when discussing general guidelines for exception handling. It's an exception [sic] to the rule, caused by poor design. (b) How do you know?! – Lightness Races in Orbit Jul 21 '15 at 10:57
  • @LightnessRacesinOrbit: (b) because I know the situation MattMcNabb is asking about, it is based on comments we exchanged in [another discussion](http://stackoverflow.com/questions/31524633/). This question is keeping the details vague to focus on the core C++ issue without getting into vendor specifics. – Remy Lebeau Jul 21 '15 at 17:35
  • @RemyLebeau: (b) Okay – Lightness Races in Orbit Jul 22 '15 at 09:02

4 Answers4

6

(I'm using C++11; I need to update ;))

15.1/3 (emphasis mine):

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

This means that the exception object is never "born const" and as such should not trigger undefined behavior to modify.

Billy ONeal
  • 104,103
  • 58
  • 317
  • 552
6

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();
}
Barry
  • 286,269
  • 29
  • 621
  • 977
3

I see no evidence of any "trickery" in the standard. Can't prove a negative, but I believe you're "safe". The constness appears to be equivalent in form to this:

T obj;
const T& t = obj;
const_cast<T &>(t).func();

That is, the constness first comes into being on the reference that exists within the catch block and that's that.

But this all really begs the question: if you can't be sure by looking at it, why do it at all?

Just catch a T&, surely.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
0

As it says here:

If the parameter of the catch-clause is a reference type, any changes made to it are reflected in the exception object, and can be observed by another handler if the exception is rethrown with throw;

So this suggests that it's safe to modify the exception object, and the changes will be observable.

However, one should consider just catching by an ordinary reference. Or, make the function const if it can work only on members that are marked as mutable.

Any of this doesn't sound readable and can make co-developers hostile.

BartoszKP
  • 34,786
  • 15
  • 102
  • 130