Почему компиляторы С++ не оптимизируют этот dynamic_cast из последнего класса?

Рассмотрим эту иерархию классов:

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

Я понимаю, что размещение final на class Dog гарантирует, что никто не сможет создать класс, наследующий от Dog, что, в свою очередь, означает, что никто не может создать класс, который IS-A Dog и IS -A Cat.

Рассмотрим эти два dynamic_cast s:

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

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

GCC, ICC и MSVC игнорируют квалификатор final и генерируют вызов __dynamic_cast; это несчастливо, но не удивительно.

Что меня удивило, так это то, что Clang и Zapcc генерируют оптимальный код для from_final ( "always return nullptr" ), но генерируют вызов до __dynamic_cast для to_final.

Является ли это действительно упущенной возможностью оптимизации (в компиляторе, где, очевидно, кто-то приложил некоторые усилия для соблюдения квалификатора final в приведениях) или оптимизация невозможна в этом случае по какой-то тонкой причине, что я все еще не вижу

Ответ 1

Хорошо, я вырыл через Clang исходный код, чтобы найти этот метод:

/// 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
}

Я немного устаю от кода синтаксического анализа, но мне не кажется, что ваш from_final не всегда всегда является нулевым из-за конечного атрибута, но, кроме того, поскольку <<22 > не находится в Dog производная цепочка записей. Если бы у него не было атрибута final, он бы вышел раньше (как это делает, когда Cat является Src), но он не обязательно всегда был бы нулевым.

Я предполагаю, что для этого есть несколько причин. Первый заключается в том, что dynamic_cast использует как вверх, так и вниз цепочку записей. Когда исходная запись имеет конечный атрибут, вы можете просто проверить цепочку, если Dest является предком (потому что из исходного кода не могут быть производные классы).

Но что, если класс не является окончательным? Я подозреваю, что может быть и больше. Может быть, многократное наследование делает поиск бросков сложнее, чем прикладывание? Не останавливая код в отладчике, я могу только предположить.

Этого я знаю: isAlwaysNull - это ранняя функция выхода. Это разумное утверждение, что оно пытается доказать, что результат всегда равен нулю. Вы не можете доказать отрицательный (как говорится), но вы можете опровергнуть позитив.


Возможно, вы можете проверить историю Git для файла и отправить письмо человеку, который написал эту функцию. (или, по крайней мере, добавил проверку final).