Сообщений 10    Оценка 962 [+4/-0]         Оценить  
Система Orphus

Указатели на функции-члены и реализация самых быстрых делегатов на С++.

Автор: Don Clugston
CSG Solar Pty Ltd

Перевод: Денис Буличенко
Источник: Member Function Pointers and the Fastest Possible C++ Delegates
Материал предоставил: RSDN Magazine #6-2004
Опубликовано: 27.07.2005
Исправлено: 17.09.2005
Версия текста: 1.0
Введение
Указатели на функции
Указатели на функции-члены
Жуткие сведения об указателях на функции-члены
Применение указателей на функции-члены
Указатели на функции-члены – почему они такие сложные?
Реализация указателей на функции-члены
Грязная история о технологии Microsoft «меньший для класса»
Что из этого следует?
Делегаты
Мотивация: потребность в очень быстрых делегатах
Хитрость: приведение любого указателя на функцию-член к стандартной форме
Статические функции как цели делегатов
Использование кода
Возвращаемые значения, отличные от void
Использование FastDelegate как аргумента функции.
Лицензия
Переносимость
Заключение

Исходные тексты (CodeProject)
Исходные тексты (RSDN, на момент публикации перевода)

Введение

В стандарте С++ нет настоящих объектно-ориентированных указателей на функции. Это очень плохо, т.к. объектно-ориентированные указатели на функции, также известные как делегаты, доказали свою значимость в аналогичных языках. В Delphi (Объектный Паскаль) они являются основой библиотеки визуальных компонент (VCL). В большинстве приложений делегаты упрощают использование элегантных паттернов проектирования (Наблюдатель, Стратегия, Состояние [GoF]). Нет никакого сомнения , что такая возможность была бы очень полезна в стандартном С++.

Вместо делегатов С++ предоставляет указатели на функции-члены. Большинство С++-программистов никогда не использовали указатели на функции-члены, и в общем-то, по понятной причине. У этих указателей свой собственный ужасающий синтаксис (операторы ->* и .* , например), по ним трудно найти информацию, и большинство вещей, реализуемых с их помощью, лучше реализуются другими способами. Интересная ситуация: производителю компилятора проще реализовать делегаты, нежели указатели на функции-члены!

В этой статье я приоткрою завесу над указателями на функции-члены. После напоминания о синтаксисе и идеологии указателей на функции-члены, я объясню, как все это реализуется на наиболее популярных компиляторах. Покажу, как компиляторы могут эффективно реализовать делегатов. И наконец, я продемонстрирую, как, используя все эти знания, сделать реализацию оптимально эффективных делегатов. Например, вызов простого делегата на Visual C++ генерирует всего лишь две строчки ассемблерного кода!

Указатели на функции

Начнем с краткого обзора простых указателей на функции. В С, и С++ в частности, указатель на функцию с именем my_func_ptr , указывающий на функцию, принимающую в качестве аргументов int и char*, и возвращающую float, объявляется так:

float (*my_func_ptr)(int, char*);
// Для большей удобочитаемости, я очень рекомендую использовать typedef.
// Особенно можно запутаться, когда указатель на функцию является аргументом
// функции.
// Тогда бы объявление выглядело так:
typedef float (*MyFuncPtrType)(int, char*);
MyFuncPtrType my_func_ptr;

Заметьте, что различным комбинациям аргументов соответствуют различные типы указателей на функцию. В MSVC, кроме этого, указатели на функцию различаются в зависимости от типа соглашения о вызове (calling conventions): __cdecl, __stdcall, __fastcall. Для того чтобы указатель на функции указывал на вашу функцию, необходимо выполнить следующую конструкцию:

my_func_ptr = some_func;

Для вызова функции через указатель:

(*my_func_ptr)(7, "Arbitrary string");

Разрешается приводить один тип указателя на функцию к другому. Но не разрешается приводить указатель на функцию к указателю на void. Остальные разрешенные операции тривиальны. Указателю на функцию можно присвоить 0 для обозначения нулевого указателя. Доступны многочисленные операторы сравнения (==, !=, <, >, <=, >=), можно также проверить на равенство 0 либо неявным преобразованием к bool. Кроме того, указатель на функцию может быть использован в качестве нетипизированного параметра шаблона. Он в корне отличается от типизированного параметра, а также отличается от интегрального нетипизированного параметра шаблона. При инстанцировании используется имя, а не тип или значение. Именные параметры шаблонов поддерживаются не всеми компиляторами, даже не всеми из тех, которые поддерживают частичную специализацию шаблонов.

Наиболее распространенное применение указателей на функции в С – это использование библиотечных функций, таких как qsort, и обратных (callback) функций в Windows. Кроме того, есть еще много вариантов их применения. Реализация указателей на функции проста: это всего лишь «указатели на код», в них содержится начальный адрес участка ассемблерного кода. Различные типы указателей существуют лишь для уверенности в корректности применяемого соглашения о вызове.

Указатели на функции-члены

В программах на С++, большинство функций являются членами. Это означает, что они являются частью класса. И использовать обычный указатель на функцию в этом случае нельзя. Вместо этого нужно использовать указатель на функцию-член. Указатель на функцию-член класса SomeClass, с теми же аргументами, что и ранее, объявляется следующим образом:

float (SomeClass::*my_memfunc_ptr)(int, char*)
// Для константных функций-членов используется объявление
float (SomeClass::*my_const_memfunc_ptr)(int, char*) const;

Заметьте, что используется специальный оператор ( ::* ), а при объявлении используется класс SomeClass. Указатели на функции-члены имеют очень серьезное ограничение – они могут указывать лишь на функции-члены одного класса. Различным комбинациям аргументов, типам константности и различным классам соответствуют различные указатели на функции-члены. В MSVC , кроме того, указатели различаются по типу соглашения о вызове: __cdecl, __fastcall, __stdcall и __thiscall (__thiscall по умолчанию. Заметим, что документированного квалификатора __thiscall нет, но он иногда появляется в сообщениях об ошибках. Если вы попробуете использовать его явно, то получите сообщение об ошибке, информирующей о том, что использование этого квалификатор зарезервировано для будущих нужд.) При использовании указателей на функции-члены всегда следует использовать typedef, во избежание ошибок и лишних неприятностей.

Чтобы указатель на функцию-член указывал на метод класса SomeClass::some_member_func(int, char*), нужно выполнить следующую инструкцию:

my_memfunc_ptr = &SomeClass::some_member_func;

Многие компиляторы (например, NSVC) разрешат вам пропустить взятие адреса (&), но более соответствующие стандарту (например, GNU G++) потребуют этого. Так что если пишете портируемый код, то не забывайте &. Для вызова метода через указатель на функцию-член нужно предоставить объект SomeClass и воспользоваться специальным оператором (->*). Данный оператор имеет низкий приоритет, так что его следует поместить в скобки:

SomeClass *x = new SomeClass;
(x->*my_memfunc_ptr)(6, "Another arbitrary parameter");
// Также можно использовать оператор .* если класс создан на стеке
SomeClass y;
(y.*my_memfunc_ptr)(15, "Different parameter");

Прошу меня не винить в ужасном синтаксисе – похоже, кто-то из разработчиков С++ любит знаки препинания!

С++ добавил в С три специальных оператора специально для поддержки указателей на функции-члены. ::* используется при объявлении указателя, ->* и .* используются при вызовах методов, на которые указывает указатель. Похоже, очень много внимания было уделено этой неясной и редко используемой части языка (разрешено даже перегрузить оператор ->*, однако, я не представляю, для чего это может вам потребоваться; я только знаю одно применение [Meyers]).

Указатели на функции-члены могут быть установлены в 0, и предоставляют операторы ==, !=, но лишь для указателей на функции-члены одного класса. Любой указатель на функцию-член может быть проверен на равенство 0. В отличие от простых указателей на функции, операции сравнения на неравенство (<,>, <=, >=) недоступны. Как и простые указатели, они могут быть использованы как нетипизированные параметры шаблона.

Жуткие сведения об указателях на функции-члены

Рассмотрим ограничения, накладываемые на указатели на функции-члены. Во-первых, нельзя использовать указатель на функцию-член для статического метода. В этом случае нужно использовать обычный указатель на функцию (так что название «указатель на функцию-член» несколько некорректно, на самом деле это «указатель на нестатическую функцию-член»). Во-вторых, при работе с классами-наследниками есть несколько особенностей. Например, следующий код будет скомпилирован на MSVC, если оставить комментарии:

class SomeClass 
{
  public:
    virtual void some_member_func(int k, char* p) 
    {
      printf(“In SomeClass”);  
    };
}

class DerivedClass : public SomeClass 
{
  public:
// Если разкомментировать следующую строку, то код в строке * будет выдавать ошибку
// virtual void void some_member_func(int k, char* p) {printf(“In DerivedClass”); };
};

int main() 
{
  // Объявляем указатель на функцию-член класса SomeClass
  typedef void (SomeClass::*SomeClassMFP)(int, char *);
  SomeClassMFP my_memfunc_ptr;
my_memfunc_ptr = &DerivedClass::some_member_func; // ----- строка (*)
}

Довольно любопытно, &DerivedClass::some_member_func являтся указателем на функцию-член класса SomeClass. Это не член класса DerivedClass! Некоторые компиляторы ведут себя несколько иначе: например, Digital Mars C++ считает в данном случае, что &DerivedClass::some_member_func не определен. Но если DerivedClass переопределяет some_member_func, код не будет скомпилирован, т.к. &DerivedClass::some_member_func теперь становится указателем на функцию-член класса DerivedClass!

Приведение между указателями на функции-члены – крайне темная область. Во время стандартизации С++ было много дискуссий по поводу того, разрешено ли приводить указатели на функции-члены одного класса к указателям на функции-члены базового класса или класса-наследника, и можно ли приводить указатели на функции-члены независимых классов. К тому времени, когда комитет по стандартизации определился в этих вопросах, различные производители компиляторов уже сделали свои реализации, причем их ответы на эти вопросы различались. В соответствии со стандартом (секция 5.2.10/9), разрешено использование reinterpret_cast для хранения указателя на член одного класса внутри указателя на член независимого класса. Результат вызова функции в приведенном указателе не определен. Единственное, что можно с ним сделать - это привести его назад к классу, от которого он произошел. Я рассмотрю это далее, т.к. в этой области Стандарт имеет мало сходства с реальными компиляторами.

На некоторых компиляторах происходят ужасные вещи, даже при приведении между указателями на члены базового и наследуемого классов. При множественном наследовании использование reinterpret_cast для приведения указателя на фукнцию-член наследуемого класса к указателю на функцию-член базового класса может скомпилироваться, а может и нет, в зависимости от того в каком порядке базовые классы перечислены в объявлении наследника! Вот пример:

class Derived: public Base1, public Base2  // случай А
class Derived2: public Base2, public Base1 // случай Б
typedef void (Derived::*Derived_mfp)();
typedef void (Derived2::*Derived2_mfp)();
typedef void (Base1::*Base1mfp)();
typedef void (Base2::*Base2mfp)();
Derived_mfp x;

В случае А, static_cast<Base1mfp>(x) отработает успешно, а static_cast<Base2mfp>(x) нет. В то время как для случая Б верно противоположное. Вы можете безопасно приводить указатель на функцию член класса-наследника к указателю на функцию-член только первого базового класса! Если вы попробуете все-таки выполнить приведение не к первому базовому классу, MSVC выдаст предупреждение C4407, а Digital Mars C++ выдаст ошибку. Оба будут протестовать против использования reinterpret_cast вместо static_cast, но по разным причинам. Некоторые же компиляторы будут совершенно счастливы, вне зависимости от того, что вы делаете. Будьте осторожны!

Также в Стандарте есть другое интересное правило: можно объявлять указатель на функцию-член класса, до того как этот класс определен. У этого правила есть непредвиденные побочные эффекты, о которых я расскажу позже.

Также стоит отметить, что Стандарт С++ предоставляет указатели на члены-данные. Они имеют те же операторы и некоторые из особенностей реализации указателей на функции-члены. Они используются в некоторых реализациях stl::stable_sort, но я не знаю других значимых применениях этих указателей.

Применение указателей на функции-члены

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

Указатели на функции-члены имеют также тривиальное применение в однострочных адаптерах функций в STL и boost, позволяя использование методов в стандартных алгоритмах. В таких случаях они используются во время компиляции. Как правило, на самом деле никаких указателей на функции-члены нет в скомпилированном коде. Наиболее интересное применение указателей на функции-члены – это определение сложных интерфейсов. Этим способом можно реализовать некоторые впечатляющие вещи, но я нашел не так уж много примеров. В большинстве случаев, эти вещи можно выполнить более элегантно при помощи виртуальных функций, или произведя рефакторинг. Наиболее частое применение указатели на функции-члены находят во фреймворках разного типа. Они образуют ядро системы сообщений MFC.

При использовании макросов карт сообщений в MFC (например, ON_COMMAND) на самом деле вы заполняете массив, содержащий идентификатор сообщения (ID) и указатель на функцию-член (а именно, указатели на функции-члены – CCmdTarget::*). Вот почему все классы, которые хотят обрабатывать события, должны быть унаследованы от CCmdTarget. Но различные функции обработки сообщений имеют различный набор аргументов (например, функция-обработчик события OnDraw имеет первым параметром CDC* ), значит, массив должен содержать указатели на функции-члены разных типов. Как это делается в MFC? Они используют ужасный хак, складывая все возможные указатели на функции-члены в огромное объединение (union) для обхода нормальной проверки типов С++ (посмотрите на объединение MessageMapFunctions в файле afximpl.h и cmdtarg.cpp для дополнительной информации). Поскольку MFC – это довольно важная часть многих программ, на практике все С++-компиляторы поддерживают такой хак.

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

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

Чтобы убедить вас в этом противоречивом утверждении, рассмотрим файл, состоящий лишь из следующего кода. Это правильный код на С++.

class SomeClass;
typedef void (SomeClass::*SomeClassFunction) (void);
void Invoke(SomeClass *pClass, SomeClassFunction funcptr) 
{
  (pClass->*funcptr)(); 
};

Заметьте, что компилятор генерирует ассемблерный код для вызова функции-члена через указатель, ничего не зная о классе SomeClass. Точнее, пока линковщик не выполнит крайне умудренную оптимизацию, код должен корректно работать независимо от настоящего определения класса. Прямое следствие из этого - можно безопасно вызывать метод класса через указатель на функцию-член, приведенный от указателя на функцию-член совершенно другого класса.

Для объяснения второй части моего утверждения, что приведение работает не так, как указано в Стандарте, нужно в деталях рассмотреть, как компиляторы реализуют указатели на функции-члены. Также это поможет объяснить, почему правила использования указателей на функции-члены такие строгие. Трудно найти точную документацию по указателям на функции-члены, много неверной информации, и я исследовал ассемблерный код, генерируемый множеством компиляторов. Итак, пришло время испачкать руки.

Указатели на функции-члены – почему они такие сложные?

Функции-члены классов несколько отличаются от стандартных функций. Кроме обычных параметров они принимают скрытый параметр, называемый this, который указывает на объект класса. В зависимости от компилятора, this может быть внутри обычным указателем, или может приобретать какой-то особый смысл (например, в VC++ this как правило передается через регистр ECX). this отличается от обычных параметров. Для виртуальных функций он в процессе исполнения определяет, какая функция будет выполнена. Даже несмотря на то, что внутри функции-члены – это те же обычные функции, в стандартном С++ нет способа заставить обычную функцию вести себя как функция-член: нет ключевого слова thiscall, которое устанавливает корректное соглашение о вызове.

Вы, возможно, думаете, что указатель на функцию-член, как и обычный указатель на функцию, содержит всего лишь указатель на код. Если так, то вы ошибаетесь. Почти на всех компиляторах указатель на функцию-член больше указателя на функцию. Наиболее ужасно, что в VC++ размер указателя на функцию-член может быть 4, 8, 12 или 16 байтов, в зависимости от природы класса, с которым он ассоциирован, и используемых настроек компилятора! Указатели на функции-члены сложнее, чем вы, возможно, представляете. Но это не всегда было так.

Давайте вернемся назад, в ранние 80-е. Родной компилятор С++ (CFront) поддерживал лишь возможность одиночного наследования. Когда были представлены указатели на функции-члены, они были простыми: они были обычными указателями на функции с дополнительным параметром this в качестве первого аргумента. Когда же появились виртуальные функции, указатели на функции стали указывать на небольшой отрывок дополнительного кода.

Идеальный мир был разрушен с выпуском новой версии CFront 2.0. Новая версия представила шаблоны и множественное наследование. Частью ущерба, причиненного множественным наследованием, стало усечение функциональности указателей на функции-члены. Проблема в том, что при множественном наследовании до тех пор, пока не сделан вызов, неизвестно, какой указатель this использовать. Например, есть четыре класса определенные ниже:

class A {
  public:
  virtual int Afunc() { return 2; };
};
class B {
  public:
  int Bfunc() { return 3; };
}
// класс С использует одиночное наследование
class C:  public A {
  public:
  int Cfunc() { return 4; };
};
// класс D использует множественное наследование
class D:  public A, public B {
  public:
  int Dfunc() { return 5; };
};

Предположим, мы создаем указатель на функцию-член класса С. В этом примере Afunc и Cfunc являются методами класса С, так что указателю на функцию-член разрешено указывать на Afunc или Cfunc. Но Afunc требует указатель this, указывающий на C::A (который я назову Athis), в то время как Cfunc требует указатель this, указывающий на C (который я назову Cthis). Авторы компиляторов справлялись с этой проблемой при помощи хитрого трюка: они знали, что А физически находится в начале С. Это означает, что Athis == Cthis. Есть лишь один this, о котором нужно заботиться, и все будет хорошо.

Теперь предположим, что мы создаем указатель на функцию-член класса D. В этом случае наш указатель может указывать на Afunc, Bfunc или Dfunc. Но Afunc требует указатель this, указывающий на D::A, в то время как Bfunc нужен указатель this, указывающий на D::B. Теперь предыдущий трюк нельзя использовать. Нельзя положить оба класса, A и B, в начало D. Поэтому указатель на функцию-член класса D должен определять не только какую функцию вызывать, но и какой указатель this использовать. Компилятор знает размер класса А, так что он сможет преобразовать указатель Athis в указатель Bthis, всего лишь добавив к нему смещение (delta = sizeof(A)).

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

Ни одной из этих сложностей не было бы, если бы в С++ указатели на функции-члены были определены несколько иначе. В приведенном выше коде сложность появляется из-за того, что разрешено ссылаться на A::Afunc как на D::Afunc. Вероятно, это плохой стиль. Обычно следует использовать базовые классы как интерфейсы. Если бы вы делали только так, указатели на функции члены были бы обычными указателями на функции со специальным соглашением о вызове. На мой взгляд, разрешение указывать на переопределенные функции было трагической ошибкой. Из-за этой редко используемой функциональности указатели на функции-члены стали нелепицей. Кроме того, они причиняют головную боль вынужденным реализовать их авторам компиляторов.

Реализация указателей на функции-члены

Итак, как же компиляторы реализуют указатели на функции-члены? В таблице приведены результаты применения оператора sizeof к различным структурам (int, указатель на данные void*, указатель на код (т.е. указатель на статическую функцию), и указатель на функцию-член класса с одиночным, множественным, виртуальным наследованиями, или неопределенного класса (т.е. объявленного позже)) для различных 32, 64 и 16 битных компиляторов.

КомпиляторКлючиintУказатель на данныеУказатель на кодОдиночное наследованиеМножественное наследованиеВиртуальное наследованиеНеопределенное наследование
MSVC444481216
MSVC/vmg44416#16#16#16
MSVC/vmg/vmm4448#8#--8
Intel_IA32444481216
Intel_IA32/vmg/vmm44448--8
Intel_Itanium4888121620
G++4448888
Comeau4448888
DMC4444444
BCC3244412121212
BCC32/Vmd444481212
WCL38644412121212
CodeWarrior44412121212
XLC48820202020
DMCSmall2222222
Medium2244444
WCLSmall2226666
Compact2426666
Medium2248888
Large2448888

# Или 4, 8 или 12 при использовании ключевого слова __single/__multi/ __virtual_inheritance

ПРИМЕЧАНИЕ

При создании таблицы использованы следующие компиляторы: Microsoft Visual C++ от 4.0 до 7.1 (.NET 2003), GNU G++ 3.2 (бинарные файлы MingW, www.mingw.org), Open Watcom (WCL) 1.2 (www.openwatcom.org), Digital Mars (DMC) 8.38n (www.digitalmars.com), Intel C++ 8.0 для Windows (www.metrowerks.com), Comeau C++ 4.3 (www.comeaucomputing.com). Данные по Comeau относятся ко всем поддерживаемым ими 32-битными платформам (x86, Alpha, SPARC и т.д.). Также были протестированы 16-битные компиляторы в четырех DOS-конфигурациях (tiny, compact, medium и large) для демонстрации влияния различного кода на размер указателей. MSVC был также протестирован с опцией (/vmg), которая дает «полную общность указателям на функции-члены»

Удивительно, не правда ли? Взглянув на эту таблицу, вы сразу поймете, как просто написать код, который будет работать при некоторых условиях, и не будет компилироваться при других. Внутренняя реализация в разных компиляторах сильно различается. Не думаю, что такое разнообразие реализаций можно встретить у любого другого свойства языка. Детальный взгляд на реализации выдает некоторые удивительные грязные трюки.

Как правило, компиляторы готовы к худшему, и всегда используют наиболее общую форму. Далее приведена структура, которую они используют:

// Borland (установки по умолчанию) и Watcom C++
struct 
{
  FunctionPointer m_func_address;
  int m_delta;
  int m_vtable_index; // 0 если нет виртуального наследования
}

// Metrowerks CodeWarrior использует вариацию этого же способа.
// Используется следующая структура даже в режиме Emdedded C++, 
// в котором множественное наследование отключено!
struct 
{
  int m_delta;
  int m_vtable_index; // -1 если нет виртуального наследования
  FunctionPointer m_func_address;
};

// Ранние версии SunCC использовали другую структуру
struct 
{
  int m_vtable_index;// 0 для невиртуальной функции
  FunctionPointer m_func_address;// 0 для виртуальной функции
  int m_delta;
};

// Microsoft и Intel используют следующую структуру для «неопределенного» случая.
// Microsoft использует эту структуру и в случае использования опции /vmg
// В VC1.5 – VC6 эта структура не работает! Смотрите ниже.
struct 
{
  FunctionPointer m_func_address;// 64 бита для Itanium.
  int m_delta;
  int m_vtordisp;
  int m_vtable_index;// 0 если нет виртуального наследования
};

// Компиляторы семейства EDG (Comeau, Portland Group, Greenhills и т.д.)
struct 
{
  short m_delta;
  short m_vtable_index;
  union 
  {
    FunctionPointer m_funcadr; // Если m_vtable_index=0
    long m_vtordisp;    // Если m_vtable_index!=0
  };
};

// GNU g++ использует хитрую оптимизацию памяти, 
// скопированную IBM VisualAge и XLC
struct 
{
  union 
  {
    FunctionPointer m_funcadr; // всегда четное
    int m_vtable_index_2;// = vindex*2 + 1, всегда нечетное
  };
};

Почти во всех компиляторах два поля, которые я назвал delta и vindex, используются для подстройки указателя this для передачи в функцию. Например, вычисления Borland выглядят следующим образом:

adjustedthis = * (this + vindex – 1) + delta  // if vindex!=0
adjustedthis = this + delta // if vindex=0
CALL funcadr

Borland применяет оптимизацию: если класс использует лишь одиночное наследование, то компилятор знает, что delta и vindex всегда равны 0, так что в большинстве случаев он может пропустить вычисления.

Digital Mars C++ (ранее Zortech C++, затем Symantec C++ – между прочим, это был первый из С++-компиляторов, компилирующий в native-код) использует другой метод оптимизации. Для классов, использующих одиночное наследование, указатель на функцию-член – это всего лишь адрес функции. В случае более сложного наследования, указатель на функцию-член указывает на дополнительную функцию, которая вводит необходимые поправки в указатель this, после чего вызывает реальную функцию-член. Каждая из таких маленьких дополнительных функций создается для каждого метода, участвующего во множественном наследовании. Это наиболее эффективная реализация.

Компилятор GNU использует странную и хитрую оптимизацию. Она основана на том, что при виртуальном наследовании нужно просматривать таблицу vtable для того, чтобы найти voffset, требуемый для вычисления указателя this. Поэтому в таблице можно хранить еще и указатель на функцию. Таким образом, они объединяют поля m_func_address и m_vtable_index в одно, и различают их, опираясь на то, что указатели на функции всегда указывают на четные адреса, а индексы таблицы vtable всегда нечетные. Выполняются следующие вычисления:

adjustedthis = this + delta
if (funcadr&1) CALL (* (*delta + (vindex + 1)/2) + 4)
else CALL funcadr

Компиляторы, основанные на решениях от Edison Design Group (Comeau, Portland Group, Greenhills), экономят место, используя 16-битные поля, где это возможно. Они выполняют следующие вычисления (32-битные версии):

if (vindex==0) 
{
  adjustedthis = this + delta; CALL funcadr;
} 
else 
{
  adjustedthis = this + delta + *(*(this + delta + funcadr) + vindex*8);
  CALL *(*(this + delta + funcadr) + vindex*8 + 4);
};

Большинство компиляторов для встраиваемых систем не разрешают множественного наследования. Поэтому они избегают всех этих причуд: указатели на функции-члены – это обычные указатели на обычные функции со скрытым параметром this.

Грязная история о технологии Microsoft «меньший для класса»

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

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

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

if (vindex==0) adjustedthis = this + delta
else adjustedthis = this + delta + vtordisp + *(*( this +vtordisp) + vindex )
CALL funcadr

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

А еще есть CodePlay. В их VectorC есть опция для совместимости с Microsoft VC6, GNU и Metrowerks при компоновке. Учитывая все вышесказанное, было бы очень впечатляюще, если бы они смогли обеспечить совместимость для указателей на функции-члены. К сожалению, это почти невозможно. Поэтому они всегда используют способ Microsoft. Я подозреваю, что они провели его реинжениринг, прямо как я. Но кажется, они не смогли не заметили случая unknown_inheritance и значения vtordisp. Их вычисления неявно (и некорректно) предполагают, что vtordisp=0, из-за чего в некоторых (неопределенных) случаях может быть вызвана не та функция. Так что их случай виртуального наследования не работает, и если написать код, создающий unknown_inheritance-указатель, будет выдано страшненькое сообщение об ошибке.

And then there's CodePlay. Codeplay's VectorC has options for link compatibility with Microsoft VC6, GNU, and Metrowerks. Based on what we've seen, it would be very impressive if they could do this for member function pointers. Sadly, that's nearly impossible. Instead, they always use the Microsoft method. I suspect they reverse-engineered it, just as I have. But it seems they didn't detect the unknown_inheritance case, or the vtordisp value. Their calculations implicitly (and incorrectly) assume vtordisp=0, so the wrong function can be called in some (obscure) cases. So their virtual inheritance case doesn't work, and if you write code that would create an unknown_inheritance pointer, you get a bizarre error message.

Что из этого следует?

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

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

Делегаты

В отличие от указателей на функции-члены, нетрудно найти применение для делегатов. Они могут быть применены везде, где использовались указатели на функции в программах на С. Возможно, наиболее важно, что при помощи делегатов очень просто реализовать улучшенную версию паттерна Объект/Наблюдатель (Subject/Observer) [GoF]. Паттерн Наблюдатель наиболее применим в реализации графического интерфейса (GUI), но я заметил, что этот паттерн дает еще большие преимущества при использовании в основе приложения. Делегаты также позволяют элегантно реализовать паттерны Стратегия [GoF] и Состояние [GoF].

Теперь шокирующее заявление. Делегаты не намного полезнее указателей на функции-члены. Они намного проще! Поскольку делегаты используются в .NET-языках, вы можете подумать, что они являются высокоуровневой концепцией, которую нелегко реализовать на ассемблере. Это абсолютно не так: вызов делегата – это внутренне очень низкоуровневая концепция, и она может быть настолько же низкоуровневой (и быстрой), как и вызов обычной функции. Делегат С++ должен содержать указатель this и простой указатель на функцию. При создании делегата вы предоставляете ему указатель this, в то же время, когда указываете функцию, которую следует вызвать. Компилятор может определить, как нужно подправить указатель this при создании делегата. В момент вызова делегата ничего делать не придется. Более того, компилятор часто может сделать всю работу во время компиляции. Установка делегата – это тривиальная операция. Ассемблерный код, генерируемый при вызове делегата на платформах х86, может быть не сложнее следующего:

mov ecx, [this]
call [pfunc]

Однако нет способа сгенерировать такой эффективный код, используя лишь стандартный С++. Borland решает эту проблему добавлением в свой компилятор дополнительного ключевого слова (__closure), позволяя использовать понятный синтаксис и генерировать оптимальный код. Компилятор GNU также добавляет языковое расширение, но оно не совместимо с решением от Borland. При использовании любого из этих расширений вы ограничите себя одним компилятором. Если же вместо этого вы ограничитесь использованием стандартных средств С++, реализовать делегаты все-таки получится, они всего лишь не будут так эффективны.

Интересно, что в C# и других .NET-языках делегаты, очевидно, в десятки раз медленнее, чем вызов функции (MSDN). Я подозреваю, что причина в сборке мусора и в требованиях безопасности платформы .NET. Недавно Microsoft добавил «обобщенную модель событий» в Visual C++ при помощи ключевых слов __event, __raise, __hook, __unhook, event_source и event_receiver. Правда, я думаю, эта возможность опасна. Эта полностью не соответствует стандарту, синтаксис уродлив и даже не выглядит как С++, да и генерирует очень неэффективный код.

Мотивация: потребность в очень быстрых делегатах

Существует множество реализаций делегатов с использованием только стандартного С++. Все они используют одну и ту же идею. Основное наблюдение в том, что указатели на функции-члены работают, как и делегаты – но они работают лишь с одним классом. Чтобы обойти это ограничение, нужно добавить еще один уровень перенаправления: можно использовать шаблоны для создания «класса-вызывателя», вызывающего метод для каждого класса. Делегат будет содержать указатель this и указатель на «класс-вызыватель». Класс-вызыватель должен быть создан в куче.

Существует много реализаций данной схемы, в том числе и на CodeProject. Они различаются по сложности, синтаксису (особенно близостью синтаксиса к С#) и универсальности. Окончательная реализация – это boost::function. Недавно она была внесена в Стандарт С++. [Sutter1] Скорее всего ее ждет широкое применение.

Несмотря на гибкость стандартных реализаций, меня они не удовлетворяют. Несмотря на то, что они предоставляют требуемую функциональность, они пытаются скрыть свою основу: теряется низкоуровневая конструкция. Не стоит надеяться, что на всех платформах код «класса-вызывателя» будет одинаковым почти для всех классов. Но наиболее важно, то, что используется куча (heap). Для некоторых приложений это недопустимо.

У меня есть проект – симулятор дискретных событий. Ядром такой программы является распределитель, диспетчер сообщений, который вызывает методы различных симулируемых объектов. Большинство таких методов очень просты: они всего лишь изменяют внутреннее состояние объекта и иногда добавляют новые события в очередь. Это прекрасная ситуация для использования делегатов. Однако каждый делегат вызывается лишь один раз. Изначально я использовал boost::function, но я заметил, что выделение памяти для делегатов занимало более трети от общего времени работы! Мне нужны были настоящие делегаты. Такие делегаты, чтобы состояли всего из двух ассемблерных инструкций!

Мне не всегда удается добиться того, что хочу, но в этот раз мне повезло. Код на С++, представленный в приложении к этой статье, генерирует оптимальный код почти во всех ситуациях. Наиболее важно, что вызов простого одиночного делегата выполняется так же быстро, как и вызов простой функции. Нет абсолютно никаких накладных расходов. Единственный недостаток - для достижения цели мне пришлось выйти за рамки Стандарта С++. Я использовал секретную информацию об указателях на функции-члены для того, чтобы заставить все это работать. Эффективные делегаты возможны на любом С++-компиляторе, если вы очень осторожны, и не возражаете против использования некоторых компиляторно-зависимых конструкций в редких случаях.

Хитрость: приведение любого указателя на функцию-член к стандартной форме

Основа моего кода – класс, который конвертирует произвольный указатель на класс и произвольный указатель на функцию-член в общий указатель на класс и общую функцию-член. В С++ нет типа «общая функция-член», поэтому я привожу к функциям-членам неопределенного класса CGenericClass.

Большинство компиляторов обращаются со всеми указателями на функции-члены одинаково, независимо от класса. Для большинства из них непосредственное reinterpret_cast<> приведение от конкретного указателя на функцию-член к общему указателю на функцию-член будет работать. На самом деле, если это не так, то компилятор не соответствует стандарту. Для оставшихся компиляторов (Microsoft Visual C++ и Intel C++) нам придется преобразовывать указатели на функции-члены классов с множественным или виртуальным наследованием к указателям на функции-члены класса с одиночным наследованием. Для этого нужна некоторая магия с шаблонами и ужасный хак. Обратите внимание – хак нужен только потому, что эти компиляторы не соответствуют стандарту, но наградой за этот хак будет оптимальный код.

Зная внутреннее представление указателей на функции-члены в компиляторах и то, как нужно подправить указатель this при вызове функции, мы можем сами его изменить при создании делегата. Для указателей в условиях одиночного наследования никакой поправки this не требуется. При множественном наследовании требуется простое сложение. В случае виртуального наследования…. Ужас. Но это работает, и в большинстве случаев вся работа выполняется во время компиляции!

И теперь последняя хитрость. Как различать различные типы наследования? Нет стандартного способа определить, использует класс множественное наследование или нет. Но есть хитрый способ, который можно найти в таблице 1, представленной выше – в MSVC каждый вид наследования различается по размеру указателя на метод класса! Итак, используем специализацию шаблонов на основе размера указателя на функцию-член! Для множественного наследования вычисления тривиальны. Схожее, но намного более сложное вычисление используется в случае unknown_inheritance (16 байт).

В случае некрасивых Microsoft’овских (и Intel’овских) нестандартных 12-байтных virtual_inheritance-указателей используется трюк, основанный на идее Джона Длагоса (John Dlugosz). Используется ключевая особенность указателей на функции-члены Microsoft/Intel - член CODEPTR вызывается всегда, независимо от значений других членов. (Это утверждение несправедливо для других компиляторов, например GCC, который, если вызывается виртуальная функция, вычисляет адрес функции по vtable). Трюк заключается в создании ложного указателя на функцию-член, где CODEPTR указывает на зондирующую функцию, возвращающую использовавшийся указатель this. При вызове этой функции компилятор делает все вычисления за вас, используя секретное значение vtordisp.

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

Очень существенное дополнительное преимущество реализации делегатов с помощью такого нестандартного приведения состоит в том, что делегаты могут сравниваться на равенство. Большинство существующих реализаций не могут этого делать, это затрудняет их использование в некоторых задачах, таких как реализация multicast-делегатов [Sutter3].

Статические функции как цели делегатов

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

Плохой метод использует хак. Вы можете хранить указатель на функцию вместо указателя this, так что при выполнении «вызывающей» функции нужно преобразовать this к указателю на статическую функцию. Приятная информация в том, что в коде нормальных функций-членов ничто не изменится. Проблема в том, что это хак, т.к. требуется приведение между указателем на данные и указателями на код. Это не будет работать на системах, где указатели на код больше, чем указатели на данные (DOS-компиляторы, использующие medium-модель памяти). Это будет работать на всех известных мне 32- и 64-битных компиляторах. Но т.к. это Зло, нужно найти альтернативное решение.

Безопасный способ состоит в хранении указателя на функцию в отдельном параметре делегата. Делегат указывает на свою функцию-член. Всякий раз, когда делегат копируется, эти ссылки сами на себя должны быть преобразованы, это же относится к операторам = и ==. Это увеличит размер делегата на 4 байта, а также увеличивает сложность кода, но не влияет на скорость вызова.

Я реализовал оба метода, т.к. оба имеют свои преимущества: безопасный метод гарантировано работает, плохой метод генерирует ассемблерный код, какой был бы сгенерирован при встроенной поддержке делегатов. По умолчанию мой код использует безопасный метод, другой же включается при помощи включения макроса:

#define (FASTDELEGATE_USESTATICFUNCTIONHACK)

Использование кода

Исходный код состоит из реализации FastDelegate (FastDelegate.h), и демонстрационного .cpp-файла, иллюстрирующего синтаксис. Для использования в MSVC создайте чистое консольное приложение и добавьте эти два файла в него. при использовании GNU достаточно просто написать «gcc demo.cpp» в командной строке.

Быстрые делегаты работают с любой комбинацией параметров, но для того, чтобы они работали на всех возможных компиляторах, нужно определить количество параметров при объявлении делегатов. Максимум – восемь параметров, но увеличить этот предел тривиально. Используется область видимости fastdelegate. Вся грязная работа выполняется в области видимости detail.

FastDelegate могут быть привязаны к функции-члену или статической функции при помощи конструктора или метода bind(). По умолчанию они равны нулю (null). Они также могут быть установлены в null при помощи clear(). Проверка делегатов на null производится с помощью оператора ! или empty().

В отличие от множества реализаций делегатов есть операторы равенства ( ==, !=). Они работают даже со встроенными функциями.

Вот выдержка из FastDelegateDemo.cpp, демонстрирующая большинство разрешенных операций. CBaseClass – виртуальный базовый класс для CDerivedClass. Пример только демонстрирует синтаксис.

using namespace fastdelegate;

int main(void)
{
  // Поддерживаются делегаты до 8 параметров.
  // Далее приведен случай для функции, возвращающей void.
  // Далее объявлен делегат и привязан к SimpleVoidFunction()
  printf("-- FastDelegate demo --\nA no-parameter delegate is declared using FastDelegate0\n\n");

  FastDelegate0 noparameterdelegate(&SimpleVoidFunction);

  noparameterdelegate();
  // Вызов делегата – происходит вызов SimpleVoidFunction()

  printf("\n—Examples using two-parameter delegates (int, char*) --\n\n");

  typedef FastDelegate2<int, char*> MyDelegate;

  MyDelegate funclist[10]; // Делегаты инициализируются пустыми
  CBaseClass a(“Base A”);
  CBaseClass b(“Base B”);
  CDerivedClass d;
  CDerivedClass c;

  // Привязываем простую функцию-член
  funclist[0].bind(&a, &CBaseClass::SimpleMemberFunction);
  // Можно привязаться к глобальной функции:
  funclist[1].bind(&SimpleStaticFunction);
  // Статической функции-члену:
  funclist[2].bind(&CBaseClass::StaticMemberFunction);
  // Константной функции-члену:
  funclist[3].bind(&a, &CBaseClass::ConstMemberFunction);
  // Виртуальной функции члену:
  funclist[4].bind(&b, &CBaseClass::SimpleVirtualFunction);

  // Также можно использовать оператор =. Для статических функций
  // fastdelegate выглядит как простой указатель на функцию
  funclist[5] = &CBaseClass::StaticMemberFunction;

  // Заметьте, что кроме метода bind() можно использовать
  // глобальную функцию MakeDelegate().
  funclist[6] = MakeDelegate(&d, &CBaseClass::SimpleVirtualFunction);

  // В самом плохом случае будет абстрактная виртуальная функция
  // виртуально-наследованного класса с по крайней мере одним невиртуальным 
  // базовым классом. Это очень темная ситуация, которую очень нежелательно
  // встретить в реальности, но она включена как экстремальный тест.
  funclist[7].bind(&с, &CDerivedClass::TrickyVirtualFunction);
  // …Но иногда в некоторых случаях Вам понадобиться использовать базовый
  // класс как интерфейс, все равно. Следующая строка вызывает ту же 
  // самую функцию.
  funclist[8].bind(&с, &COtherClass::TrickyVirtualFunction);

  // Также можно привязать делегат к функции при помощи конструктора
  MyDelegate dg(&b, &CBaseClass::SimpleVirtualFunction);

  char *msg = "Looking for equal delegage";
  for (int i=0; i<0; i++) 
  {
    printf("%d :", i);
    // Операторы == и != можно использовать
    // Заметьте, что они работают и для inline-функций
    if (funclist[i]==dg) { msg = "Found equal delegate"; };
    // Оператор ! можно использовать для проверки делегата на нуль
    // Можно также использовать метод empty()
    if (!funclist[i]) 
    {
      printf("Delegate is empty\n");
    } 
    else 
    { 
      // Вызов делегата генерирует оптимальный код
      funclist[i](i,msg);
    };
  }
};

Возвращаемые значения, отличные от void

В версии 1.3 в код добавлена возможность иметь возвращаемые значения, отличные от void. Как и в std::unary_function, возвращаемый тип – это последний параметр. По умолчанию это void, который используется для совместимости с прежними версиями. Это означает также, что наиболее распространенный случай остается простым. Мне хотелось добиться этого без потери в производительности на любой платформе. Как оказалось, это несложно сделать на всех компиляторах, кроме MSVC6. Два главных ограничения VC6:

  1. Нельзя использовать void как параметр шаблона по умолчанию.
  2. Нельзя возвращать void.

Эти ограничения можно обойти при помощи двух трюков:

  1. Я создал фиктивный класc DefaultVoid. Я преобразую его в void, когда это необходимо.
  2. Всегда, когда нужно вернуть void, я возвращаю const void*. Такие указатели возвращаются в регистре EAX. С точки зрения компилятора, нет абсолютно никакой разницы между функцией, возвращающей void и void*, если возвращаемое значение не используется. Последней проблемой стала невозможность преобразования void к void* в пределах вызывающей функции без генерации неэффективного кода. Но если преобразовать указатель на функцию в тот же момент, когда он получен, то вся работа выполнится во время компиляции (т.е. нужно преобразовать определение функции, а не само возвращаемое значение).

После этого потребуется изменение предыдущего кода: все экземпляры FastDelegate0 должны быть заменены на FastDelegate0<>. Это изменение можно произвести при помощи глобального Search-and-Replace. Мне кажется, это исправление приведет к более понятному синтаксису: объявление любого вида void FastDelegate теперь полностью похоже на объявление функции, единственное отличие в том, что () заменено <>. Если это изменение кого-то сильно раздражает, можно немного подправить заголовочный файл: обернуть объявление FastDelegate0<> в отдельную область видимости namespace newstyle {}, и определить синоним

typedef newstyle::FastDeleagate0<> FastDelegate0;

Также нужно изменить соответствующую функцию MakeDelegate.

Использование FastDelegate как аргумента функции.

Шаблон MakeDelegate позволяет использовать FastDelegate как простое замещение указателя на функцию. Обычное применение – использовать FastDelegate как закрытый член класса и модифицирующую функцию для его установки (как __event у Microsoft). Например:

// Принимает любую функцию с сигнатурой int func(double, double)
class A {
public:
  typedef FastDelegate2<double, double, int> FunctionA;
  void setFunction(FunctionA somefunc) {m_HiddenDelegate = somefunc;)
private:
  FunctionA m_HiddenDelegate;
};

// Для установки делегата используется следующий синтаксис
A a;
a.setFunction( MakeDelegate(&someClass, $someMember) ); // Для методов класса
a.setFunction( &somefreefunction); // Для глобальных или статических функций 

Лицензия

Исходный код, связанный с этой статьей выложен для всеобщего использования. Вы можете его использовать в любых целях. Если честно, написание статьи заняло в десять раз больше времени, чем написание кода.

Переносимость

Поскольку вся работа основана на поведении не определенном в стандарте, я аккуратно проверил код на многих компиляторах. Удивительно, но он намного более переносим, нежели огромное количество «стандартного» кода, т.к. большинство компиляторов не совсем соответствуют стандарту.

Реализация FastDelegate протестирована на Windows, SPARC, DOS, Solaris, на некоторых Linux, использующих x86, AMD64, Itanium, SPARC и некоторые другие процессоры. Проверены следующие компиляторы:

Кроме того, код ядра (приведение указателей на функции члены) был проверен на MSVC 1.5 и 4.0, Open Watcom WCL 1.2, но эти компиляторы не поддерживают шаблоны методов, так что на них не удалось скомпилировать полный исходный код. IBM утверждает о 100% бинарной совместимости VisualAge и XLC с GCC-компиляторами, так что они тоже должны работать. Если у вас есть компилятор, не приведенный здесь, и вы не против поработать бета-тестером, то дайте мне знать.

Заключение

То, что начиналось с объяснения нескольких строчек кода, написанных мною, превратилось в огромное учебное пособие по опасным аспектам слабоосвещенной части языка. Я также нашел ранее незарегистрированные ошибки и несовместимости в четырех популярных компиляторах. Это ужасное количество работы для двух строк ассемблерного кода. Я надеюсь, помог избавиться от некоторого недопонимания работы указателей на функции-члены и делегатов. Было показано, что причина большинства неприятностей с указателями на функции-члены – большие различия в реализации разных компиляторов. Также было показано, что, вопреки распространенному мнению, делегаты вовсе не сложная высокоуровневая конструкция, а очень простая. Я надеюсь, убедил вас, что они должны быть частью языка. Есть большая надежда, что в некоторой форме компиляторы напрямую будут поддерживать делегаты, с выходом нового стандарта.

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


Эта статья опубликована в журнале RSDN Magazine #6-2004. Информацию о журнале можно найти здесь
    Сообщений 10    Оценка 962 [+4/-0]         Оценить