С++: специализация класса - допустимое преобразование для соответствующего компилятора?

Надеюсь, это не слишком специализированный вопрос для StackOverflow: если он есть и может быть перенесен в другое место, дайте мне знать...

Много лет назад я написал дипломную работу, предлагающую различные методы девиртуализации для С++ и родственных языков, обычно основанные на идее прекомпилированной специализации путей кода (несколько как шаблоны), но с проверками выбора правильных специализаций выбираются во время выполнения в случаях, когда они не могут быть выбраны во время компиляции (в качестве шаблонов должны быть).

Основная (основная) идея состоит в следующем: предположим, что у вас есть класс C, как показано ниже:

class C : public SomeInterface
{
public:
    C(Foo * f) : _f(f) { }

    virtual void quack()
    {
        _f->bark();
    }

    virtual void moo()
    {
        quack(); // a virtual call on this because quack() might be overloaded
    }

    // lots more virtual functions that call virtual functions on *_f or this

private:
    Foo * const _f; // technically doesn't have to be const explicitly
                    // as long as it can be proven not be modified
};

И вы знали, что существуют конкретные подклассы Foo, такие как FooA, FooB и т.д., с известными полными типами (без обязательного наличия исчерпывающего списка), тогда вы можете прекомпилировать специализированные версии C для некоторые выбранные подклассы Foo, например, (обратите внимание, что конструктор здесь не включен, специально, поскольку он не будет вызываться):

class C_FooA final : public SomeInterface
{
public:
    virtual void quack() final
    {
        _f->FooA::bark(); // non-polymorphic, statically bound
    }

    virtual void moo() final
    {
        C_FooA::quack(); // also static, because C_FooA is final
        // _f->FooA::bark(); // or you could even do this instead
    }

    // more virtual functions all specialized for FooA (*_f) and C_FooA (this)

private:
    FooA * const _f;
};

И заменим конструктор C на что-то вроде следующего:

C::C(Foo * f) : _f(f)
{
    if(f->vptr == vtable_of_FooA) // obviously not Standard C++
        this->vptr = vtable_of_C_FooA; 
    else if(f->vptr == vtable_of_FooB)
        this->vptr = vtable_of_C_FooB;
    // otherwise leave vptr unchanged for all other values of f->vptr
}

Таким образом, динамический тип создаваемого объекта изменяется в основном на основе динамического типа аргументов его конструктору. (Обратите внимание: вы не можете сделать это с помощью шаблонов, потому что вы можете создавать только C<Foo>, если вы знаете тип f во время компиляции). С этого момента любой вызов FooA::bark() через C::quack() включает только один виртуальный вызов: либо вызов C::quack() статически привязан к неспециализированной версии, которая динамически вызывает FooA::bark(), либо вызов C::quack() динамически перенаправляется на C_FooA::quack(), который статически вызывает FooA::bark(). Кроме того, динамическая отправка может быть полностью устранена в некоторых случаях, если анализатор потока имеет достаточную информацию, чтобы сделать статический вызов C_FooA::quack(), что может быть очень полезно в замкнутом цикле, если оно позволяет встраивать. (Хотя технически в этот момент вы, вероятно, будете в порядке даже без этой оптимизации...)

(Обратите внимание, что это преобразование безопасно, хотя и менее полезно, даже если _f является неконстантным и защищенным вместо private и C наследуется от другой единицы перевода... единица перевода, создающая vtable для унаследованный класс вообще ничего не знает о специализациях, а конструктор унаследованного класса просто установит this->vptr в свою собственную таблицу vtable, которая не будет ссылаться на какие-либо специализированные функции, потому что она ничего о них не знает.)

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

В любом случае не зависит от того, действительно ли такая оптимизация будет иметь достаточное количество ошибок для реализации, а также стоит накладные расходы в конечном исполняемом файле, мой вопрос:, есть ли что-нибудь в стандартном С++, которое помешало бы компилятору выполнить такое преобразование?

Я чувствую, что нет, поскольку стандарт вообще не указывает, как виртуальная диспетчеризация выполняется или как представлены функции-указатели-члены. Я почти уверен, что механизм RTTI не предотвращает C и C_FooA от маскарадинга как одного и того же типа для всех целей, даже если они имеют разные виртуальные таблицы. Единственное, что я мог подумать об этом, возможно, имеет значение, - это небольшое чтение ODR, но, вероятно, нет.

Я что-то пропускаю? Если запретить проблемы с ABI/связыванием, возможны ли такие преобразования без нарушения совместимых программ на С++? (Более того, если да, можно ли это сделать в настоящее время с ABI Itanium и/или MSVC? Я уверен, что ответ есть и да, но, надеюсь, кто-то может подтвердить.)

ИЗМЕНИТЬ: Кто-нибудь знает, реализуется ли что-либо подобное в любом компиляторе /JIT для С++, Java или С#? (См. Обсуждение и связанный чат в комментариях ниже...) Я знаю, что JITs делают спекулятивную статическую привязку/вложение виртуальных объектов непосредственно на сайтах вызовов, но я не знаю, делают ли они что-нибудь подобное (с совершенно новыми vtables генерируется и выбирается на основе проверки одного типа, выполненной в конструкторе, а не на каждом сайте вызова).

Ответ 1

Есть ли что-нибудь в стандартном С++, которое помешало бы компилятору выполнить такое преобразование?

Нет, если вы уверены, что наблюдаемое поведение не изменилось - это правило "как есть", которое является стандартным разделом 1.9.

Но это может привести к тому, что ваше преобразование будет довольно сложным: 12.7/4:

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

Итак, если деструктор Foo::~Foo() происходит прямо или косвенно вызывает C::quack() объекта c, где c._f указывает на уничтожаемый объект, вам нужно вызвать Foo::bark(), даже если _f был FooA, когда вы построили объект c.

Ответ 2

В первом чтении это похоже на С++-сфокусированное изменение полиморфного встроенного кэширования. Я думаю, что V8 и Oracle JVM используют его, и я знаю, что .NET делает.

Чтобы ответить на ваш первоначальный вопрос: я не думаю, что в стандарте есть что-то, что запрещает подобные реализации. С++ очень серьезно относится к правилу "как есть"; до тех пор, пока вы добросовестно выполняете правильную семантику, вы можете сделать реализацию любым безумным способом, который вам нравится. Виртуальные вызовы С++ не очень сложны, поэтому я сомневаюсь, что вы тоже пройдете по любым случаям с краем (в отличие от того, если, скажем, вы пытались сделать что-то умное со статическим привязкой).