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

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

18

Решение

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

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

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

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

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

1

Другие решения

При первом чтении это звучит как с ++ — сфокусированный вариант полиморфное встроенное кэширование. Я думаю, что V8 и Oracle JVM оба используют его, и я знаю, что .NET делает.

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

0

По вопросам рекламы [email protected]