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