60

In one of the projects I'm working on, I'm seeing this code

struct Base {
  virtual ~Base() { }
};

struct ClassX {
  bool isHoldingDerivedObj() const {
    return typeid(1 ? *m_basePtr : *m_basePtr) == typeid(Derived);
  }
  Base *m_basePtr;
};

I have never seen typeid used like that. Why does it do that weird dance with ?:, instead of just doing typeid(*m_basePtr)? Could there be any reason? Base is a polymorphic class (with a virtual destructor).

EDIT: At another place of this code, I'm seeing this and it appears to be equivalently "superfluous"

template<typename T> T &nonnull(T &t) { return t; }

struct ClassY {
  bool isHoldingDerivedObj() const {
    return typeid(nonnull(*m_basePtr)) == typeid(Derived);
  }
  Base *m_basePtr;
};
Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • 6
    Have you tried it without that? – Luke Jul 22 '11 at 20:45
  • 13
    Could it be legacy by chance? (Maybe it wasn't always `1 ? ...`) –  Jul 22 '11 at 20:45
  • 4
    The thing is that the conditional will always evaluate to true, and the two branches yield the exact same value. Can you look at the version control's history (if any) and see if it was something else in the past? – In silico Jul 22 '11 at 20:46
  • 1
    I agree with @pst: Most likely legacy. – NotMe Jul 22 '11 at 20:47
  • 1
    It looks like either a very clever way to defeat some over-zealous compiler optimisation or cargo cult programming. – biziclop Jul 22 '11 at 20:47
  • 1
    @biziclop: Clever isn't the word I'd use. :-P – In silico Jul 22 '11 at 20:48
  • 1
    Is the person who wrote the code fragment in question not around anymore? If they're still contactable, you might want to try going directly to them and asking, if only to see why they did it in the first place. – JAB Jul 22 '11 at 20:50
  • @In silico Remember, clever != wise. :) – biziclop Jul 22 '11 at 20:50
  • 1
    I probably would have written it `typeid(*m_basePtr ? *m_basePtr : *m_basePtr)`. – Nemo Jul 22 '11 at 21:14
  • I don't have an answer for the question, but I suspect that the code doesn't return what the author might have expected if `m_basePtr` points to an object that is derived from `Derived` (unless they really wanted to return `true` only if the object was precisely of type `Derived`). And that's not even considering if `m_basePtr` points to another kind of type that's derived from `Base` but isn't in the `Derived` hierarchy. But I could envision that being intended even it it's probably a problematic design. – Michael Burr Jul 22 '11 at 21:50
  • @Nemo but then `Base` would need to be convertible to `bool`. Not true in the real project's code. – Johannes Schaub - litb Jul 24 '11 at 11:29

4 Answers4

50

I think it is an optimisation! A little known and rarely (you could say "never") used feature of typeid is that a null dereference of the argument of typeid throws an exception instead of the usual UB.

What? Are you serious? Are you drunk?

Indeed. Yes. No.

int *p = 0;
*p; // UB
typeid (*p); // throws

Yes, this is ugly, even by the C++ standard of language ugliness.

OTOH, this does not work anywhere inside the argument of typeid, so adding any clutter will cancel this "feature":

int *p = 0;
typeid(1 ? *p : *p); // UB
typeid(identity(*p)); // UB

For the record: I am not claiming in this message that automatic checking by the compiler that a pointer is not null before doing a dereference is necessarily a crazy thing. I am only saying that doing this check when the dereference is the immediate argument of typeid, and not elsewhere, is totally crazy. (Maybe is was a prank inserted in some draft, and never removed.)

For the record: I am not claiming in the previous "For the record" that it makes sense for the compiler to insert automatic checks that a pointer is not null, and to to throw an exception (as in Java) when a null is dereferenced: in general, throwing an exception on a null dereference is absurd. This is a programming error so an exception will not help. An assertion failure is called for.

WhiZTiM
  • 21,207
  • 4
  • 43
  • 68
curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • 5
    What? Are you serious? Are you drunk? – Mateen Ulhaq Sep 27 '11 at 00:10
  • Nice explanation. I just hope I remember this whenever I come across this 'idiom'; chances are I won't (I rarely see or use `typeid`). – Michael Burr Dec 18 '11 at 20:58
  • 1
    The standard requires an exception to be thrown when the argument to the `typeid` expression is an *lvalue*. The arbitrary use of of the ternary operator in this case does not change that behavior, and a conforming compiler *must* throw. – David Rodríguez - dribeas Aug 14 '12 at 04:22
  • @DavidRodríguez-dribeas Is it a recent change? – curiousguy Aug 14 '12 at 04:39
  • @curiousguy: C++03 5.2.8 [expr.typeid] /3, still present in C++11. – David Rodríguez - dribeas Aug 14 '12 at 04:44
  • [expr.typeid]/2 "If the glvalue expression is obtained by applying the unary * operator to a pointer67 and the pointer is a null pointer value (4.10), the typeid expression throws the std::bad_typeid exception (18.7.3)." The glvalue expression is obtained **by applying the ternary `?:` operator** here. – curiousguy Aug 14 '12 at 04:50
  • The type of `*p` and `(true? *p : *p)` is exactly the same: an *lvalue* in both cases, and the standard mandates that an exception is thrown. You might want to reread your answer, because your comment contradicts it (and agrees with my comment) – David Rodríguez - dribeas Aug 14 '12 at 05:17
  • @DavidRodríguez-dribeas Sorry to be pedantic: "_The type of *p and (true? *p : *p) is exactly the same: an lvalue in both cases_" being an l/rvalue is not part of the type of an expression. I don't think there is a name for (type,lvalueness,bitfieldness) of an expression, which is a shame. "_an lvalue in both cases_" did I wrote the contrary? "_You might want to reread your answer_" No, but **you** might want to reread my previous comment. "_your comment contradicts it (and agrees with my comment)_" Utter nonsense. You seem extremely tired. – curiousguy Aug 14 '12 at 05:24
  • what is "A little known are rarely" meant to say in English? – M.M Jan 23 '15 at 23:42
  • 2
    @curiousguy "value category" is the term for whether an expression is an lvalue etc. The possible categories are { lvalue, xvalue, prvalue }. There are also combinations { glvalue, rvalue }. – M.M Jan 23 '15 at 23:49
5

The only effect I can see is that 1 ? X : X gives you X as an rvalue instead of plain X which would be an lvalue. This can matter to typeid() for things like arrays (decaying to pointers) but I don't think it would matter if Derived is known to be a class. Perhaps it was copied from someplace where the rvalue-ness did matter? That would support the comment about "cargo cult programming"

Regarding the comment below I did a test and sure enough typeid(array) == typeid(1 ? array : array), so in a sense I'm wrong, but my misunderstanding could still match the misunderstanding that lead to the original code!

Ben Jackson
  • 90,079
  • 9
  • 98
  • 150
  • 10
    §5.16/4: "*If the second and third operands are lvalues and have the same type, the result is of that type and is an lvalue.*" – ildjarn Jul 22 '11 at 21:23
  • 2
    I think Visual C++ gets this wrong though (goes to dig through Connect issue reports). Ahh, here's an example of (incorrect) rvalue conversion with the conditional operator: https://connect.microsoft.com/VisualStudio/feedback/details/279444/arrays-decay-into-pointers-in-conditional-operator – Ben Voigt Jul 22 '11 at 22:24
4

This behaviour is covered by [expr.typeid]/2 (N3936):

When typeid is applied to a glvalue expression whose type is a polymorphic class type, the result refers to a std::type_info object representing the type of the most derived object (that is, the dynamic type) to which the glvalue refers. If the glvalue expression is obtained by applying the unary * operator to a pointer and the pointer is a null pointer value, the typeid expression throws an exception of a type that would match a handler of type std::bad_typeid exception.

The expression 1 ? *p : *p is always an lvalue. This is because *p is an lvalue, and [expr.cond]/4 says that if the second and third operand to the ternary operator have the same type and value category, then the result of the operator has that type and value category also.

Therefore, 1 ? *m_basePtr : *m_basePtr is an lvalue with type Base. Since Base has a virtual destructor, it is a polymorphic class type.

Therefore, this code is indeed an example of "When typeid is applied to a glvalue expression whose type is a polymorphic class type" .


Now we can read the rest of the above quote. The glvalue expression was not "obtained by applying the unary * operator to a pointer" - it was obtained via the ternary operator. Therefore the standard does not require that an exception be thrown if m_basePtr is null.

The behaviour in the case that m_basePtr is null would be covered by the more general rules about dereferencing a null pointer (which are a bit murky in C++ actually but for practical purposes we'll assume that it causes undefined behaviour here).


Finally: why would someone write this? I think curiousguy's answer is the most plausible suggestion so far: with this construct, the compiler does not have to insert a null pointer test and code to generate an exception, so it is a micro-optimization.

Presumably the programmer is either happy enough that this will never be called with a null pointer, or happy to rely on a particular implementation's handling of null pointer dereference.

M.M
  • 138,810
  • 21
  • 208
  • 365
0

I suspect some compiler was, for the simple case of

typeid(*m_basePtr)

returning typeid(Base) always, regardless of the runtime type. But turning it to an expression/temporary/rvalue made the compiler give the RTTI.

Question is which compiler, when, etc. I think GCC had problems with typeid early on, but it is a vague memory.

Tony
  • 71
  • 1