13

Consider this class hierarchy:

struct Animal { virtual ~Animal(); };
struct Cat : virtual Animal {};
struct Dog final : virtual Animal {};

My understanding is that putting final on class Dog ensures that nobody can ever create a class inheriting from Dog, which, corollary, means that nobody can ever create a class that IS-A Dog and IS-A Cat simultaneously.

Consider these two dynamic_casts:

Dog *to_final(Cat *c) {
    return dynamic_cast<Dog*>(c);
}

Cat *from_final(Dog *d) {
    return dynamic_cast<Cat*>(d);
}

GCC, ICC, and MSVC ignore the final qualifier and generate a call to __dynamic_cast; this is unfortunate but not surprising.

What surprised me is that Clang and Zapcc both generate optimal code for from_final ("always return nullptr"), but generate a call to __dynamic_cast for to_final.

Is this really a missed optimization opportunity (in a compiler where obviously somebody put some effort into respecting the final qualifier in casts), or is the optimization impossible in this case for some subtle reason that I'm still not seeing?

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
  • 4
    My guess is that this kind of situation doesn't arise often enough for most compilers to worry too much about it. Optimizations are implemented in response to a common real-world need for efficiency. – cdhowie Jun 12 '17 at 00:11
  • @cdhowie: You may be right; but what gives me pause is that somebody clearly *did* go to the trouble of writing a Clang optimization in the `from_final` case. The `to_final` case is symmetrical (especially in terms of codegen, where it's pulling the typeinfo for both types), yet that unknown somebody did *not* add the symmetrical optimization. "Conspicuously half-implemented optimization" appears weirder to my brain than "no optimization at all" (cf. GCC, ICC, MSVC). – Quuxplusone Jun 12 '17 at 01:10

1 Answers1

4

Ok, I dug through Clang's source code to find this method:

/// isAlwaysNull - Return whether the result of the dynamic_cast is proven
/// to always be null. For example:
///
/// struct A { };
/// struct B final : A { };
/// struct C { };
///
/// C *f(B* b) { return dynamic_cast<C*>(b); }
bool CXXDynamicCastExpr::isAlwaysNull() const
{
  QualType SrcType = getSubExpr()->getType();
  QualType DestType = getType();

  if (const PointerType *SrcPTy = SrcType->getAs<PointerType>()) {
    SrcType = SrcPTy->getPointeeType();
    DestType = DestType->castAs<PointerType>()->getPointeeType();
  }

  if (DestType->isVoidType()) // always allow cast to void*
    return false;

  const CXXRecordDecl *SrcRD = 
    cast<CXXRecordDecl>(SrcType->castAs<RecordType>()->getDecl());

  //********************************************************************
  if (!SrcRD->hasAttr<FinalAttr>()) // here we check for Final Attribute
    return false; // returns false for Cat
  //********************************************************************

  const CXXRecordDecl *DestRD = 
    cast<CXXRecordDecl>(DestType->castAs<RecordType>()->getDecl());

  return !DestRD->isDerivedFrom(SrcRD); // search ancestor types
}

I'm getting a little tired from parsing code, but it doesn't seem to me that your from_final is not simply always null because of the Final Attribute, but in addition because that Cat is not in the Dog derived record chain. Granted, if it didn't have the final attribute, then it would have exited early (as it does when Cat is a Src), but it wouldn't have necessarily been always null.

I'm guessing there are a couple reasons for this. The first is that dynamic_cast casts both up and down the record chain. When the Source Record has the Final Attribute, you can simply check the chain if Dest is an ancestor (because there can be no derived classes from Source).

But what if the class is not final? I suspect there might be more to it. Maybe multiple inheritance makes searching up casts more difficult than down casts? Without stopping the code in a debugger, all I can do is speculate.

This I do know: isAlwaysNull is an early exiting function. It's a reasonable assertion that it's trying to prove that the result is always null. You can't prove a negative (as they say), but you can disprove a positive.


Perhaps you can check the Git history for the file and email the person who wrote that function. (or at least added the final check).

James Poag
  • 2,320
  • 1
  • 13
  • 20
  • 1
    According to dynamic_cast docs, it can cast up, down and sideways. I suspect that `Final` prevents not only future casts, but sideways as well. Again, I think that searching inheritance records might be prohibitively expensive (at least for such trivial check). – James Poag Jun 12 '17 at 02:48
  • Nice digging! "The first is that dynamic_cast casts both up and down the record chain. When the Source Record has the Final Attribute, you can simply check the chain if Dest is an ancestor" — But notice that if *Dest* has the Final attribute, then Dest cannot be an ancestor of *anything*; there's no need to check any chains in that case. – Quuxplusone Jun 12 '17 at 18:40
  • I was a little tired last night, and i kept rewriting that line. In any event, yeah, you're right. I couldn't help but think the only reason that check isn't made is 1) search time or 2) omission (as you pointed out). Cheers! – James Poag Jun 12 '17 at 20:46