Надеемся, что это не слишком специализированный вопрос для StackOverflow: если это так и может быть перенесено в другое место, дайте мне знать …
Много месяцев назад я написал диссертацию для студентов, предлагающую различные методы девиртуализации для C ++ и родственных языков, обычно основанные на идее предварительно скомпилированной специализации путей кода (чем-то вроде шаблонов), но с проверками для выбора правильных специализаций, которые выбираются во время выполнения в тех случаях, когда они не могут быть выбраны во время компиляции (как должны быть шаблоны).
(Очень) основная идея — что-то вроде следующего … предположим, у вас есть класс 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
неконстантен и защищен вместо частного и C
наследуется от другого модуля перевода … модуль перевода, создающий виртуальную таблицу для унаследованного класса, вообще ничего не будет знать о специализациях, а конструктор унаследованного класса просто установит this->vptr
к своей собственной vtable, которая не будет ссылаться на какие-либо специализированные функции, потому что не будет ничего о них знать.)
Это может показаться большим усилием для устранения одного уровня косвенности, но дело в том, что вы можете сделать это на любом произвольном уровне вложенности (любая глубина виртуальных вызовов, следующих этому шаблону, может быть уменьшена до одного), основываясь только на локальной информации внутри модуль перевода, и делать это таким образом, чтобы он был устойчивым, даже если новые типы определены в других модулях перевода, о которых вы не знаете … вы просто можете добавить много раздувания кода, которого не было бы в противном случае, если бы вы сделал это наивно.
Тем не мение, Независимо от того, будет ли этот вид оптимизации иметь достаточную выгоду, стоит усилий по реализации, а также затрат на пространство в конечном исполняемом файле., мой вопрос, есть ли что-нибудь в стандарте C ++, что помешает компилятору выполнить такое преобразование?
Мне кажется, что нет, так как стандарт вообще не указывает, как осуществляется виртуальная диспетчеризация или как представлены указатели на функции-члены. Я уверен, что нет ничего о механизме RTTI, препятствующем C
а также C_FooA
от маскировки под один и тот же тип для всех целей, даже если у них разные виртуальные таблицы. Единственное, о чем я мог подумать, это может иметь значение, это некоторое чтение ODR, но, вероятно, нет.
Я что-то пропускаю? Если бы не было проблем с ABI / связыванием, были ли бы возможны такие преобразования без нарушения соответствующих программ на C ++? (Более того, если да, может ли это быть сделано в настоящее время с помощью ABI для Itanium и / или MSVC? Я вполне уверен, что ответ тоже есть, но, надеюсь, кто-то может подтвердить.)
РЕДАКТИРОВАТЬКто-нибудь знает, реализовано ли что-нибудь подобное в каком-либо основном компиляторе / JIT для C ++, Java или C #? (См. Обсуждение и связанный чат в комментариях ниже …) Я знаю, что JIT делают спекулятивное статическое связывание / встраивание виртуалов непосредственно на сайтах вызовов, но я не знаю, делают ли они что-то подобное (с совершенно новыми виртуальными таблицами). генерируется и выбирается на основе одной проверки типа, выполненной в конструкторе, а не на каждом сайте вызова).
Есть ли в стандарте C ++ что-нибудь, что помешало бы компилятору выполнить такое преобразование?
Нет, если вы уверены, что наблюдаемое поведение не изменилось — это «как если бы правило», которое является стандартным разделом 1.9.
Но это может затруднить доказательство правильности вашего преобразования: 12.7 / 4:
Когда виртуальная функция вызывается прямо или косвенно из конструктора (включая мем-инициализатор или же скобки или равно-инициализатор для нестатического члена данных) или из деструктора, а объект, к которому применяется вызов, является объектом, находящимся в процессе конструирования или уничтожения, вызываемая функция определена в собственном классе конструктора или деструктора или в одной из его баз, но не функция, переопределяющая его в классе, производном от собственного класса конструктора или деструктора, или не переопределяющая его в одном из других базовых классов наиболее производного объекта.
Так что если деструктор Foo::~Foo()
происходит прямо или косвенно C::quack()
на объекте c
, где c._f
указывает на уничтожаемый объект, нужно позвонить Foo::bark()
, даже если _f
был FooA
когда вы построили объект c
,
При первом чтении это звучит как с ++ — сфокусированный вариант полиморфное встроенное кэширование. Я думаю, что V8 и Oracle JVM оба используют его, и я знаю, что .NET делает.
Чтобы ответить на ваш первоначальный вопрос: я не думаю, что в стандарте есть что-то, что запрещает подобные реализации. C ++ относится к правилу «как есть» довольно серьезно; до тех пор, пока вы добросовестно реализуете правильную семантику, вы можете реализовать ее любым сумасшедшим способом. Виртуальные вызовы c ++ не очень сложны, поэтому я сомневаюсь, что вы бы тоже ни разу там не подошли (в отличие, скажем, если вы пытались сделать что-то умное с статический связывание).