Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why don't C++ compilers optimize this dynamic_cast from a final class?

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?

like image 232
Quuxplusone Avatar asked Jun 11 '17 23:06

Quuxplusone


1 Answers

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).

like image 75
James Poag Avatar answered Oct 04 '22 10:10

James Poag