Указатели функций-членов и наиболее быстрые делегаты C++ - Хитрости Microsoft относительно наименьшего метода для класса

ОГЛАВЛЕНИЕ

 

Хитрости Microsoft относительно наименьшего метода для класса

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

Во-первых, преобразование указателя функции-члена от производного класса к базовому классу может изменить его размер! Следовательно, возможна потеря информации. Во-вторых, когда функция-член объявляется перед определением ее класса, компилятор вынужден вычислять, сколько места выделить для нее. Но он не может сделать это надежно, так как не знает сущность наследования класса до тех пор, пока он не определен. Компилятор вынужден угадывать. Если он угадает неверно в одном исходном файле, но правильно в другом, ваша программа породит непонятную ошибку во время выполнения. Поэтому Microsoft добавил несколько зарезервированных слов для своего компилятора: __single_inheritance, __multiple_inheritance и __virtual_inheritance. Они также добавили переключатель компилятора /vmg, который делает все MFP одинакового размера путем удержания пропущенных нулевых полей. На данном этапе игра становится грязной.

Документация подразумевает, что указание /vmg эквивалентно объявлению каждого класса с ключевым словом __virtual_inheritance. Но это не так. Вместо этого он использует даже большую структуру, которую мы назвали unknown_inheritance. Она также применяется всякий раз, когда нужно создать указатель функции-члена для класса, когда все это видно в предваряющем объявлении. Невозможно использовать их указатели __virtual_inheritance , так как они используют слабую оптимизацию. Ниже приведен применяемый ими алгоритм:

// Microsoft и Intel используют его в «Неизвестном» случае.
// Microsoft использует его, когда применяется опция /vmg
// В VC1.5 - VC6 эта структура нарушена! Смотрите ниже.
struct MicrosoftUnknownMFP{
   FunctionPointer m_func_address; // 64 бита для Itanium.
   int m_delta;
   int m_vtordisp;
   int m_vtable_index; // или 0, если нет виртуального наследования
};
 if (vindex=0) adjustedthis = this + delta
 else adjustedthis = this + delta + vtordisp + *(*(this + vtordisp) + vindex)
 CALL funcadr

В случае виртуального наследования значение vtordisp не хранится в указателе __virtual_inheritance! Компилятор жестко кодирует его в генерируемом ассемблерном коде, когда вызывается функция. Но вы должны знать это, чтобы работать с незавершенными типами. Поэтому имеются два типа указателей виртуального наследования. Но до VC7 случай неопределенного наследования содержал много ошибок. Поля vtordisp и vindex Всегда были нулевыми! Ужасающее следствие: в VC4-VC6 указание опции /vmg (без /vmm или /vms) могло привести к вызову неверной функции! Это было очень трудно отследить. В VC4 интегрированная среда разработки имела блок для выбора опции /vmg, но он был отключен. Мы полагаем, что кто-то в MS знал об этой ошибке, но она никогда не упоминалась в их базе знаний. В итоге данная ошибка была исправлена в VC7.

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

Теперь о CodePlay. Ранние версии Codeplay VectorC имели опции для совместимости компоновки с Microsoft VC6, GNU и Metrowerks. Но они всегда использовали метод Microsoft. Они переработали данный метод, just as I have, но не выявили случай неизвестного наследования или значение vtordisp. Их расчеты неявным образом (и неверно) предполагали, что vtordisp=0, поэтому в некоторых неопределенных случаях могла вызываться ошибочная функция. Однако планируемый выпуск Codeplay VectorC 2.2.1 устранил эти проблемы. Указатели функций-членов теперь совместимы с Microsoft или с GNU на уровне двоичного кода. Благодаря высокоэффективной оптимизации и значительному улучшению соответствия стандартам (частичная специализация шаблона, и т.д.) он становится довольно впечатляющим компилятором.

Что мы узнали из всего этого?

Теоретически, все эти производители могут радикально изменить свои методы представления MFP. На деле это очень маловероятно, так как может привести к разрушению большой части существующего кода. В MSDN есть очень старая статья, опубликованная Microsoft, которая объясняет детали реализации Visual C++ во время выполнения [JanGray]. Она была написана Дженом Греем, который фактически написал объектную модель MS C++ в 1990. Хотя эта статья ведет начало от 1994, она все еще адекватна, за исключением исправления ошибок, Microsoft не изменил ее за 15 лет. Аналогично, наш самый ранний компилятор (Borland C++ 3.0, (1990)) генерирует код, идентичный коду самого нового компилятора Borland, за исключением того, что 16-битные регистры заменены 32-битными регистрами.

Теперь вы знаете намного больше об указателях функций-членов. В чем смысл? Хотя эти реализации очень сильно различаются между собой, они имеет что-то общее и полезное: ассемблерный код, требуемый для вызова указателя функции-члена, идентичен, независимо от того, какой класс и какие параметры используются. Некоторые компиляторы применяют оптимизацию в зависимости от вида наследования класса, но когда класс вызывается для незавершенного типа, все данные оптимизации невозможны. Этот факт можно использовать, чтобы создать эффективные делегаты.