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

ОГЛАВЛЕНИЕ

Обстоятельное руководство об указателях функций-членов и реализация делегатов, которая генерирует только два кода операции ассемблера!

Введение

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

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

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

 

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

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

float (*my_func_ptr)(int, char *);
// Чтобы сделать это более понятным, рекомендуется использовать переименование типов.
// Код становится запутанным, когда
// указатель функции – это параметр функции.
// Объявление могло бы выглядеть таким образом:
typedef float (*MyFuncPtrType)(int, char *);
MyFuncPtrType my_func_ptr;

Учтите, что есть различные типы указателя функции для каждой комбинации аргументов. В MSVC также есть различные типы для каждого из трех различных соглашений о вызовах: __cdecl, __stdcall и __fastcall. Вы заставляете ваш указатель функции указывать на функцию float some_func(int, char *) таким образом:

 my_func_ptr = some_func;

Когда вам нужно вызвать сохраненную функцию, вы делаете так:

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

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

В C указатели функций чаще всего используются в качестве параметров для библиотечных функций типа qsort и как обратные вызовы для функций Windows и т.д. Они также имеют много других применений. Реализация указателей функций простая: они являются "указателями кода": они содержат начальный адрес процедуры на языке ассемблера. Различные типы указателей функций существуют только для того, чтобы убедиться, что применяется правильное соглашение о вызове.

 

Указатели функций-членов

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

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

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

Вы можете заставить ваш указатель функции ссылаться на функцию float SomeClass::some_member_func(int, char *) таким образом:

my_memfunc_ptr = &SomeClass::some_member_func;
 // Это синтаксис для операторов:
 my_memfunc_ptr = &SomeClass::operator !;
 // Нет пути получить адрес

Некоторые компиляторы (например, MSVC 6 и 7) позволят вам опустить &, даже если это не соответствует стандартам и создает путаницу. Более совместимые со стандартами компиляторы (например, GNU G++ и MSVC 8 (он же VS 2005)) требуют этого, поэтому вы должны указывать данный символ. Чтобы вызвать указатель функции-члена, вы должны создать экземпляр SomeClass, используя специальный оператор ->*. Этот оператор имеет низкий приоритет, поэтому его нужно заключить в круглые скобки:

  SomeClass *x = new SomeClass;
  (x->*my_memfunc_ptr)(6, "Another Arbitrary Parameter");
// Вы также можете использовать оператор .*, если ваш класс находится в стеке.
  SomeClass y;
  (y.*my_memfunc_ptr)(15, "Different parameters this time");

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

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


 

Странности указателей функций-членов

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

class SomeClass {
 public:
    virtual void some_member_func(int x, char *p) {
       printf("In SomeClass"); };
};

class DerivedClass : public SomeClass {
 public:
 // Если раскомментировать следующую строку, код в строке (*) не будет работать!
//    virtual void some_member_func(int x, 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!

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

В стандартах есть еще одно интересное правило: вы можете объявить указатель функции-члена перед определением класса. Вы даже можете вызвать функцию-член данного незавершенного типа! Это будет обсуждаться позже в статье. Учтите, что некоторые компиляторы не поддерживают эту возможность (ранний MSVC, ранний CodePlay, LVMM).

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

 

 

Использование указателей функций-членов

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

  1. надуманные примеры, демонстрирующие синтаксис начинающим программистам в C++, и
  2. реализации делегатов!

Они также имеют очевидные способы применения в коротких адаптерах функций в библиотеках STL и Boost, позволяющих вам применять функции-члены в стандартных алгоритмах. В таких случаях они используются во время компиляции; обычно в скомпилированном коде на самом деле нет указателей функций. Самое интересное применение указателей функций-членов – это определение составных интерфейсов. Таким способом можно делать впечатляющие вещи, но мы не нашли много примеров. Большей частью эти задачи можно выполнить более элегантно с помощью виртуальных функций или путем перестройки задачи. Чаще всего указатели функций-членов применяются в различных видах сред разработки приложений. Они формируют ядро системы обмена сообщениями MFC.

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

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

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

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

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

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

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

 

Почему указатели функций-членов настолько сложные?

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

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

Давайте вернемся в начало 1980х. Когда исходный компилятор C++ (CFront) был разработан, он имел только единичное наследование. Когда были введены указатели функций-членов, они были простыми: они были только лишь указателями функций, имеющими дополнительный параметр this в качестве первого аргумента. Когда были задействованы виртуальные функции, указатель функции ссылался на небольшой кусок кода-переходника. (Дискуссия о comp.lang.c++.moderated установила, что CFront на самом деле не использовал переходники, он был намного менее элегантным. Но он мог использовать этот метод и делал вид, что применяет его, что делает последующую дискуссию более легкой для понимания.)

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

class A {
 public:
       virtual int Afunc() { return 2; };
};

class B {
 public:
      int Bfunc() { return 3; };
};

// C – это класс с единичным наследованием, производный только от A
class C: public A {
 public:
     int Cfunc() { return 4; };
};

// D использует множественное наследование
class D: public A, public B {
 public:
    int Dfunc() { return 5; };
};

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

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

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

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


 

Реализации указателей функций-членов

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

Компилятор

Параметры

int

DataPtr

CodePtr

Single

Multi

Virtual

Unknown

MSVC

 

4

4

4

4

8

12

16

MSVC

/vmg

4

4

4

16#

16#

16#

16

MSVC

/vmg /vmm

4

4

4

8#

8#

--

8

Intel_IA32

 

4

4

4

4

8

12

16

Intel_IA32

/vmg /vmm

4

4

4

4

8

--

8

Intel_Itanium

 

4

8

8

8

12

16

20

G++

 

4

4

4

8

8

8

8

Comeau

 

4

4

4

8

8

8

8

DMC

 

4

4

4

4

4

4

4

BCC32

 

4

4

4

12

12

12

12

BCC32

/Vmd

4

4

4

4

8

12

12

WCL386

 

4

4

4

12

12

12

12

CodeWarrior

 

4

4

4

12

12

12

12

XLC

 

4

8

8

20

20

20

20

DMC

small

2

2

2

2

2

2

2

 

medium

2

2

4

4

4

4

4

WCL

small

2

2

2

6

6

6

6

 

compact

2

4

2

6

6

6

6

 

medium

2

2

4

8

8

8

8

 

large

2

4

4

8

8

8

8

# Или 4,8, или 12, если ключевое слово __single/ __multi/ __virtual_inheritance используется.

Компиляторы - Microsoft Visual C++ 4.0 до 7.1 (.NET 2003), GNU G++ 3.2 (исполняемые файлы MingW, http://www.mingw.org/), Borland BCB 5.1 (http://www.borland.com/), Open Watcom (WCL) 1.2 (http://www.openwatcom.org/), Digital Mars (DMC) 8.38n (http://www.digitalmars.com/), Intel C++ 8.0 for Windows IA-32, Intel C++ 8.0 для Itanium (http://www.intel.com/), IBM XLC для AIX (Power, PowerPC), Metrowerks Code Warrior 9.1 для Windows (http://www.metrowerks.com/) и Comeau C++ 4.3 (http://www.comeaucomputing.com/). Данные для Comeau применимы для всех поддерживаемых ими 32-битных платформ (x86, Alpha, SPARC, и т.д.). 16-битные также были протестированы в 4ч конфигурациях DOS (крошечная, компактная, средняя и большая), чтобы показать влияние указателей данных и кода различных размеров. MSVC также был проверен с параметром (/vmg), который дает "полную универсальность для указателей на члены". (Если у вас есть компилятор от производителя, который не упомянут здесь, сообщите нам. Результаты для процессоров не-x86 особенно важны.)

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



Стабильные компиляторы

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

struct BorlandMFP { // также используется в Watcom
   CODEPTR m_func_address;
   int delta;
   int vindex; // или 0, если нет виртуального наследования
};
if (vindex==0) adjustedthis = this + delta;
else adjustedthis = *(this + vindex -1) + delta
CALL funcadr

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

Многие другие компиляторы используют такое же вычисление, часто с небольшим переупорядочиванием структуры.

// Metrowerks CodeWarrior использует немного иной вариант.
// Он использует эту структуру даже в режиме Встроенного C++, в котором
// множественное наследование не допускается!
struct MetrowerksMFP {
   int delta;
   int vindex; // или -1, если нет виртуального наследования
   CODEPTR func_address;
};

// Ранняя версия SunCC, вероятно, использовала другое упорядочивание:
struct {
   int vindex; // или 0, если невиртуальное наследование
   CODEPTR func_address; // или 0, если виртуальная функция
   int delta;
};

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

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

struct DigitalMarsMFP { // Почему все не делают это таким способом?
   CODEPTR func_address;
};

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

// GNU g++ использует запутанную оптимизацию пространства, также применяемую в IBM VisualAge и XLC.
struct GnuMFP {
   union {
     CODEPTR funcadr; // всегда четные
     int vtable_index_2; //  = vindex*2+1, всегда нечетные
   };
   int delta;
};
adjustedthis = this + delta
if (funcadr & 1) CALL (* ( *delta + (vindex+1)/2) + 4)
else CALL funcadr

Метод G++ хорошо документирован, поэтому он был приянт многими другими производителями, включая компиляторы IBM VisualAge и XLC, новые версии 64-битных компиляторов Open64, Pathscale EKO и Metrowerks. Упрощенная схема, используемая ранними версиями GCC, также широко распространена. SGI теперь снял с производства компиляторы MIPSPro и Pro64, и старый компилятор Apple MrCpp, применявшие этот метод. (Учтите, что компилятор Pro64 стал компилятором Open64 с открытым исходным кодом).

struct Pro64MFP {
     short delta;
     short vindex;
     union {
       CODEPTR funcadr; // если vindex==-1
       short __delta2;
     } __funcadr_or_delta2;
   };
// если vindex==0, то это пустой указатель на член.
 

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

// Компиляторы, использующие синтаксический анализатор EDG (Comeau, Portland Group, Greenhills, и т.д.)
struct EdisonMFP{
    short delta;
    short vindex;
    union {
     CODEPTR funcadr; // если vindex=0
     long vtordisp;   // если vindex!=0
    };
};
if (vindex==0) {
   adjustedthis=this + delta;
   CALL funcadr; 
} else {
   adjustedthis = this+delta + *(*(this+delta+vtordisp) + vindex*8);
   CALL *(*(this+delta+funcadr)+vindex*8 + 4);
};

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


 

Хитрости 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-битными регистрами.

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


Делегаты

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

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

    mov ecx, [this]
    call [pfunc]

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

Любопытно, что в C# и в других языках .NET делегаты, видимо, в десятки раз медленнее, чем вызов функции (MSDN). Возможно, причина этого – уборка мусора и требования безопасности .NET. Недавно Microsoft добавил "единую модель событий" в Visual C++ с ключевыми словами __event, __raise, __hook, __unhook, event_source и event_receiver. Вероятно, это функция ужасна. Она полностью нестандартная, имеет неприятный синтаксис и даже не выглядит как C++, при этом генерирует неэффективный код.

Потребность в очень быстрых делегатах

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

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

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

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

Представленный здесь код C++ генерирует оптимальный ассемблерный код почти во всех обстоятельствах. Более того, вызов одноцелевого делегата выполняется так же быстро, как и обычный вызов функции. Здесь вообще отсутствуют непроизводительные затраты. Минус только в том, что для достижения этого нужно выйти за пределы стандартного C++. Мы использовали знание указателей функций-членов, чтобы заставить работать данные делегаты. Эффективные делегаты возможны в любом компиляторе C++, если вы очень осторожны и не против использования некоторого специфичного для компилятора кода в ряде случаев.

 

Преобразование любых указателей функций-членов в стандартную форму

Ядро нашего кода – это класс, преобразующий произвольный указатель класса и произвольный указатель функции-члена в обобщенный указатель класса и в обобщенную функцию-член. C++ не имеет типа 'обобщенная функция-член', поэтому мы преобразовываем к функциям-членам неопределенного класса CGenericClass.

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

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

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

Для Microsoft's (and Intel's) нестандартных 12-байтовых указателей виртуального наследования Microsoft (и Intel) используется еще один прием, основанный на идее, придуманной Джоном Длугозцом. Важная особенность указателей функций-членов Microsoft/Intel, которую мы использовали, состоит в том, что член CODEPTR всегда вызывается, независимо от значений других членов. (Это не верно для других компиляторов, например, GCC, который получает адрес функции из таблицы, если вызывается виртуальная функция.) Длугозц решил создать фальшивый указатель функции-члена, в котором codeptr ссылается на тестовую функцию, возвращающую указатель 'this', который был использован. Когда вы вызываете данную функцию, компилятор выполняет все вычисления за вас, используя скрытое значение vtordisp.

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

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


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

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

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

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

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

Примечание: почему первый метод вообще работает? Если вы внимательно рассмотрите алгоритм, используемый каждым компилятором при вызове указателя функции-члена, вы увидите, что во всех этих компиляторах при использовании не виртуальной функции с одиночным наследованием (то есть delta=vtordisp=vindex=0) указатель экземпляра не применяется для вычисления того, какую функцию вызывать. Поэтому даже с помощью бессмысленного указателя экземпляра будет вызвана правильная функция. Внутри этой функции получаемый указатель this будет равен garbage + delta = garbage. (другими словами, бессмыслица на входе, немодифицированная бессмыслица на выходе!) При этом можно преобразовать нашу бессмыслицу обратно в указатель функции. Данный метод не будет работать, если вызыватель статической функции является виртуальной функцией.


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

Исходный код состоит из реализации FastDelegate (FastDelegate.h) и демонстрационного файла .cpp, поясняющего синтаксис. Чтобы использовать их в MSVC, создайте пустое консольное приложение и добавьте в него два данных файла. В GNU просто наберите "g++ demo.cpp" в командной строке.

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

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

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

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

 using namespace fastdelegate;

int main(void)
{
    // Поддерживаются делегаты с числом параметров не больше 8.
    // Здесь приведен случай функции, не имеющей типа.
    // Мы объявляем делегат и присоединяем его к 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(&c, &CDerivedClass::TrickyVirtualFunction);
  // ...Но в таких случаях вы должны использовать базовый класс как
  // интерфейс. Следующая строка вызывает точно такую же функцию.
    funclist[8].bind(&c, &COtherClass::TrickyVirtualFunction);

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

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

 


Непустые возвращаемые значения

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

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

Для решения данных проблем мы использовали два приема:

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

Все экземпляры FastDelegate0 должны быть заменены на FastDelegate0<>. Это можно выполнить с помощью глобального поиска и замены во всех ваших файлах. Это изменение делает синтаксис более наглядным: объявление любого типа void FastDelegate теперь вынлядет аналогично объявлению функции, за исключением того, что () заменяется на <>. Если это изменение вам не нравится, можно изменить заголовочный файл: поместить определение FastDelegate0<> внутри отдельного пространства имен newstyle { и } typedef newstyle::FastDelegate0<> FastDelegate0;. Вы также должны будете изменить соответствующую функцию MakeDelegate.

Передача FastDelegate в качестве параметра функции

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

// Принимает любую функцию с такой сигнатурой: 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 ); // для функций- не членов класса или статических функций

Естественный синтаксис и совместимость с Boost (новинка версии 1.4)

Jody Hagins усовершенствовал классы FastDelegateN, реализовав для них такой же привлекательный синтаксис, что и для новых версий Boost.Function и Boost.Signal. В компиляторах с частичной специализацией шаблонов вы можете записать FastDelegate< int (char *, double)> вместо FastDelegate2<char *, double, int>. Если ваш код должен компилироваться в VC6, VC7.0 или в Borland, вы должны использовать старый переносимый синтаксис. Мы убедились, что старый и новый синтаксис на 100% эквиваленты и могут использоваться поочередно.

Jody также создал вспомогательную функцию bind, позволяющую быстро преобразовывать код, написанный для Boost.Function и Boost.Bind, в FastDelegate. Это позволяет вам быстро определять, насколько повысится производительность вашего кода, если вы перейдете на FastDelegate. Это можно найти в "FastDelegateBind.h". Если у нас есть код:

      using boost::bind;
      bind(&Foo:func, &foo, _1, _2);

мы можем заменить "using" на using fastdelegate::bind, при этом все должно работать правильно. Предупреждение: аргументы bind игнорируются! На самом деле никакое связывание не выполняется. Поведение эквивалентно boost::bind только в тривиальном (самом обычном) случае, когда используются только базовые аргументы-заполнители _1, _2, _3, и т.д.. Будущие версии смогут поддерживать boost::bind должным образом.


Упорядоченные операторы сравнения (новинка версии 1.4)

FastDelegates одного и того же типа теперь могут сравниваться с помощью <, >, <=, >=. Указатели функций-членов не поддерживают эти операторы, но их можно смоделировать с помощью простого двоичного сравнения путем использования memcmp(). Итоговая строгая квазиупорядоченность физически бессмысленна и зависима от компилятора, но она позволяет сохранять их в упорядоченных контейнерах, таких как std:set.

Класс DelegateMemento (новинка версии 1.4)

Новый класс DelegateMemento был создан, чтобы дать возможность иметь несопоставимые коллекции делегатов. В каждый класс FastDelegate были добавлены два дополнительных члена:

const DelegateMemento GetMemento() const;
void SetMemento(const DelegateMemento mem);

DelegegateMemento могут быть скопированы и сопоставлены друг с другом (==, !=, >, <, >=, <=), что позволит сохранить их в любом упорядоченном или неупорядоченном контейнере. Их можно использовать как замену для объединения указателей функций в C. Как и для объединения несопоставимых типов, вы обязаны убедиться в том, что вы используете один и тот же тип согласованно. Например, если вы получили DelegateMemento из FastDelegate2 и сохранили его в FastDelegate3, вероятно, ваша программа вызовет ошибку во время выполнения, когда вы вызовете ее. В будущем мы можем добавить режим отладки, использующий оператор typeid для обеспечения соблюдения безопасности типов. DelegateMemento в основном предназначены для использования в других библиотеках, а не в общем коде пользователя. Важная сфера применения – обмен сообщениями в Windows, где динамический std::map<MESSAGE, DelegateMemento> может заменить статические карты сообщений, найденные в MFC и WTL. Но это уже тема другой статьи.

Неявное преобразование в bool (новинка версии 1.5)

Теперь вы можете использовать синтаксис if (dg) {...} (где dg – это быстрый делегат) как альтернативу if (!dg.empty()), if (dg!=0) или даже if (!!dg). Если вы просто используете код, вы должны знать только то, что он работает правильно во всех компиляторах и случайно не выполняет другие действия.

Реализация была сложнее, чем ожидалось. Простое предоставление оператора bool опасно, так как он позволяет вам писать нечто вроде int a = dg; хотя вы, вероятно, имели в виду int a = dg();. Решение – использование преобразования в указатель данных закрытого члена вместо преобразования в bool. К сожалению, это может запутать синтаксис if (dg==0), и некоторые компиляторы реализуют указатели членов-данных с ошибками, поэтому нам пришлось использовать пару приемов. Один метод, который другие использовали раньше, - это разрешить сравнение с целыми числами, и ASSERT, если целое число не равно нулю. Вместо этого мы применяем более подробный метод. Единственный побочный эффект состоит в том, что сравнения с указателями функций становятся более оптимальными! Упорядоченные сравнения с константой 0 не поддерживаются (но действительно сравнение с указателем функции, который случайно оказывается пустым).


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

Так как делегаты основаны на поведении, не определенном стандартами, мы тщательно проверяли код во множестве компиляторов. Это в большей степени переносимый, чем стандартный код, так как многие компиляторы не полностью подчиняются стандартам. Широкая известность делает их безопасными. Ведущие производители компиляторов и несколько членов комитета о стандартах C++ знакомы с методами, представленными здесь (во многих случаях ведущие производители компиляторов связались со мной по поводу статьи). Есть незначительный риск, что производители выполнят такое изменение, которое непоправимо разрушит код. Например, никакие изменения не нужны для обеспечения поддержки первых 64-битных компиляторов Microsoft. Codeplay даже использовал FastDelegates для внутреннего тестирования своего компилятора VectorC.

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

  • Microsoft Visual C++ 6.0, 7.0 (.NET), 7.1 (.NET 2003) и 8.0 (2005) Beta (включая /clr 'управляемый C++').
  • {Скомпилированы и скомпонованы, также проверен ассемблерный код, но не запущены} Microsoft 8.0 Beta 2 для Itanium и для AMD64.
  • GNU G++ 2.95, 3.0, 3.1, 3.2 и 3.3 (Linux, Solaris и Windows (MingW, DevCpp, Bloodshed)).
  • Borland C++ Builder 5.5.1 и 6.1.
  • Digital Mars C++ 8.38 (x86, 32-битный и 16-битный, Windows все модели памяти DOS).
  • Intel C++ для Windows (x86) 8.0 и 8.1.
  • Metrowerks CodeWarrior для Windows 9.1 (в режимах C++ и EC++).
  • CodePlay VectorC 2.2.1 (Windows, Playstation 2). Ранние версии не поддерживаются.
  • Portland Group PGI Workstation 5.2 for Linux, 32-bit.
  • { Скомпилированы и скомпонованы, также проверен ассемблерный код, но не запускаются} Comeau C++ 4.3 (x86 NetBSD).
  • { Скомпилированы и скомпонованы, также проверен ассемблерный код, но не запускаются} Intel C++ 8.0 и 8.1 для Itanium, Intel C++ 8.1 для EM64T/AMD64.

Ниже описано состояние других компиляторов C++, которые, как мы знаем, все еще используются:

  • Open Watcom WCL: Будет работать, как только шаблоны функций-членов будут добавлены в компилятор. Ядро кода (преобразование между указателями функций-членов) работает в WCL 1.2.
  • LVMM: Ядро кода работает, но в данный момент компилятор имеет слишком много ошибок.
  • IBM Visual Age и XLC: Должно работать, так как IBM заявляет, что есть 100% совместимость на уровне двоичного кода с GCC.
  • Pathscale EKO: Должно работать, так как совместим на двоичном уровне с GCC.
  • Все компиляторы, использующие синтаксический анализатор EDG (GreenHills, Apogee, WindRiver, etc.), также должны работать.
  • Paradigm C++: Неизвестно, но кажется, что это просто перекомпонованная ранняя версия компилятора Borland.
  • Sun C++: Неизвестно.
  • Compaq CXX: Неизвестно.
  • HP aCC: Неизвестно.

Заключение

Начав объяснение с нескольких строк кода, мы написали в итоге огромное руководство по неясной части языка. Мы также выявили незарегистрированные ошибки и несовместимости в шести популярных компиляторах. Это слишком большой объем работы для двух строк ассемблерного кода!

Надеемся, что мы прояснили некоторые заблуждения относительно мрачного мира указателей функций-членов и делегатов. Мы увидели, что указатели функций-членов имеют столько странностей, так как они по-разному реализованы в различных компиляторах. Мы также увидели, что делегаты не являются сложными высокоуровневыми конструкциями, и в реальности они очень просты. Надеемся, мы убедили вас, что они должны быть частью языка. есть реальный шанс, что какая-либо форма непосредственной поддержки делегатов компиляторами будет добавлена в C++, когда стандарты C++0x будут выпущены.

Согласно нашим знаниям, ни одна из предыдущих реализаций делегатов в C++ не была так эффективна и проста в применении, как представленный здесь FastDelegates. Надеемся, что они будут вам полезны.