Новый способ реализовать делегаты с помощью C++

ОГЛАВЛЕНИЕ

Решение проблем с некоторыми текущими реализациями делегатов в C++

Введение

Делегирование в C++ не является новой концепцией. Имеется много его реализаций в Code Project и в Интернете вообще. Наиболее полная статья написана Доном Клагстоном, в которой он показывает все трудности, с которыми люди сталкиваются при работе с указателем на метод. С другой стороны, Сергей Рязанов представляет очень простое решение, которое использует возможность "нестандартный параметр шаблона" современного языка C++. Для пользователей функции повышения (улучшения) Boost.Function должна быть наиболее известна. Здесь приводится сравнение этих реализаций.

 

Boost.Function

Сергей Рязанов

Дон Клагстон

Характеристики

Использование динамической памяти для хранения информации при связывании с методом и со связанным объектом.

Использует нестандартный параметр шаблона. Легкая для понимания.

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

Отпечаток памяти

Неэффективно из-за дорогостоящего выделения памяти в «куче».

Эффективно

Эффективно

Соответствие стандарту

Да

Да

Нет. Фактически это реализация недокументированных возможностей языка.

Портативный

Да

Нет. Не все компиляторы C++ поддерживают нестандартный параметр шаблона.

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

Синтаксис

Изящный

Не изящный, например:

SomeObject obj;

delegate d = delegate::from_member<SomeObject, &SomeObject::someMethod>(&obj);

Изящный

Совместимы ли делегаты?

Да

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

Да

Простота?

Ответ положительный, если вам легко дается программирование шаблонов в C++. В противном случае ответ может быть отрицательным.

Да. Он легко понятен, даже если читатели являются начинающими в программировании шаблонов.

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

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

Идеальный делегат должен быть совместимым со стандартом и портативным (Boost), эффективно использовать память, иметь изящный синтаксис, совместимость и простоту. Можно ли улучшить один из них до такой степени, чтобы он стал идеальным, или нужно создать новый? Да, мы собираемся создать новый делегат.


Предпосылки

Различные виды классов

// kind_of_class.cpp
// Этот файл демонстрирует различные виды указателя на члены класса

class dummy_base1 { };
class dummy_base2 { };

class dummy_s : dummy_base1 { };
// Дойдя сюда, компилятор распознает dummy_s как
// вид "одиночного наследования".
typedef void (dummy_s::*pointer_to_dummy_s)(void);
size_t size_of_single = sizeof(pointer_to_dummy_s);

class dummy_m : dummy_base1, dummy_base2 { };
// Дойдя сюда, компилятор распознает dummy_s как
// вид "множественного наследования".
typedef void (dummy_m::*pointer_to_dummy_m)(void);
size_t size_of_multi = sizeof(pointer_to_dummy_m);

class dummy_v : virtual dummy_base1 { };
// Дойдя сюда, компилятор распознает dummy_s как
// вид "виртуального наследования".
typedef void (dummy_v::*pointer_to_dummy_v)(void);
size_t size_of_virtual = sizeof(pointer_to_dummy_v);

class dummy_u;
// опережающая ссылка, неизвестная в этот момент
typedef void (dummy_u::*pointer_to_dummy_u)(void);
size_t size_of_unknown = sizeof(pointer_to_dummy_u);

void main()
{
    printf("%d\n%d\n%d\n%d", size_of_single, size_of_multi,
        size_of_virtual, size_of_unknown);
}

Если вы компилируете и запускаете пример с помощью VC++, используя настройки проекта по умолчанию, вывод будет такой:

4
8
12
16

Вывод вышеприведенного примера не будет идентичным при компиляции с помощью других компиляторов C++. Дополнительную информацию смотрите в разделе статьи Дона, озаглавленном "Реализации указателей на функции-члены класса". Очевидно, что указатель на метод класса "unknown (неизвестный)" всегда имеет самый большой размер, за исключением компиляторов. Отныне мы будем разделять классы C++ на 2 категории:

  • Известный класс (одиночный, множественный, Виртуальный)
  • Неизвестный класс (предваряющее объявление)

Различные виды методов одного и того же класса

Ниже указаны несколько атрибутов (модификаторов), которые влияют на методы класса:

  • Соглашение о вызовах: __cdecl, __stdcall, __fastcall, __thiscall. Обратитесь к статье «Передача параметров и соглашение об именах» за более подробной информацией. C++ позволяет, но игнорирует соглашения о вызовах для объявлений методов
  • Число и типы аргументов
  • Тип возвращаемого значения
  • Постоянные методы против непостоянных
  • Виртуальные методы против невиртуальных

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

// __thiscall, непостоянный, 1 аргумент
typedef void (CKnownClass::*method_type_1)(int );

// __cdecl, непостоянный, 2 аргумента, возвращает CString
typedef CString (__cdecl CKnownClass:: method_type_2)(int, CString );

// __fastcall, постоянный, 3 аргумента, возвращает void*
typedef void* (__fastcall CKnownClass::* method_type_3)(
    int, CString, void*) const;

void main()
{
    printf("%d\n%d\n%d", sizeof(method_type_1),
        sizeof(method_type_2),
        sizeof(method_type_3));
}

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

Ниже указаны несколько атрибутов (модификаторов), которые влияют на свободные функции или статические методы:

  • Соглашение о вызовах: __cdecl, __stdcall, __fastcall
  • Число и тип аргументов
  • Тип возвращаемого значения

Аналогично, атрибуты не влияют на размер указателей на свободные функции. Размер должен быть равен 4 байтам на всех 32-битных платформах и должен быть равен 8 на всех 64-битных платформах.


Внутренняя структура

Данный раздел показывает, как реализована эта библиотека, шаг за шагом.

Структура данных делегата

Структура данных делегата включает следующую информацию:

  1. Число и тип аргументов
  2. Тип возвращаемого значения
  3. Метод или свободная функция
  4. Соглашение о вызовах
  5. Адрес метода или функции, на которую указывает делегат

Замечание: Нам не нужно сохранять информацию об атрибутах "const" и "virtual".

Чтобы описать пункты 1 и 2, этот делегат будет реализован как шаблон C++, точно так же, как и в других реализациях. Например:

// Boost
typedef boost::function2<void, int, char*> BoostDelegate;

// Дон Клагстон
typedef fastdelegate::FastDelegate2<int, char*, void> DonDelegate;

// Сергей Рязанов
typedef srutil::delegate2<void, int, char*> SRDelegate;

// И реализация
typedef sophia::delegate2<void, int, char*> SophiaDelegate;

Следующий пример кода раскрывает пункты 3, 4 и 5:

class delegate2 // <void (int, char*)>
{
protected:
    class _never_exist_class;

    typedef void (_never_exist_class::*thiscall_method)(int, char*);
    typedef void (__cdecl _never_exist_class::*cdecl_method)(int, char*);
    typedef void (__stdcall _never_exist_class::*stdcall_method)(int, char*);
    typedef void (__fastcall _never_exist_class::*fastcall_method)(int, char*);
    typedef void (__cdecl *cdecl_function)(int, char*);
    typedef void (__stdcall *stdcall_function)(int, char*);
    typedef void (__fastcall *fastcall_function)(int, char*);

    enum delegate_type
    {
        thiscall_method_type,
        cdecl_method_type,
        stdcall_method_type,
        fastcall_method_type,
        cdecl_function_type,
        stdcall_function_type,
        fastcall_function_type,
    };

    class greatest_pointer_type
    {
        char never_use[sizeof(thiscall_method)];
    };

    delegate_type m_type;
    _never_exist_class* m_p;
    greatest_pointer_type m_fn;

public:
    void operator()(int i, char* s)
    {
        switch(m_type)
        {
        case thiscall_method_type:
            return (m_p->*(*(thiscall_method*)(&m_fn)))(i, s);
        case cdecl_function_type:
            return (*(*(cdecl_function*)(&m_fn)))(i, s);
        default:
            // Это просто пример, не описывается реализация для всех случаев
            throw;
        }
    }

    static int compare(const delegate2& _left, const delegate2& _right)
    {
        // первое, сравниваем указатели
        int result = memcmp(&_left.m_fn, &_right.m_fn, sizeof(_left.m_fn));
        if(0 == result)
        {
            // второе, сравниваем объекты
            result = ((char*)_left.m_p) - ((char*)_right.m_p);
        }
        return result;
    }

    // конструктор из функции __cdecl
    delegate2(void (__cdecl *fn)(int, char*))
    {
        m_type = cdecl_function_type;
        m_p = 0;
        reinterpret_cast<cdecl_function_type&>(m_fn) = fn;
        // заполняем резервные биты нулями для дальнейшего сравнения
        memset((char*)(&m_fn) + sizeof(fn), 0, sizeof(m_fn) - sizeof(fn));
    }

    // конструктор из метода __thiscall
    template<class T> delegate2(T* p, void (T::*fn)(int, char*))
    {
        m_type = thiscall_method_type;
        m_p = reinterpret_cast<_never_exist_class*>(p);

        ///////////////////////////////////////////////////////////
        // Мы хотим выполнить следующее присваивание
        // m_fn = fn
        // Но как это сделать соответствующим стандарту и портативным способом?
        // Ниже приведен ответ
        ///////////////////////////////////////////////////////////

        // опережающая ссылка
        class _another_never_exist_class_;
        typedef void (
            _another_never_exist_class_::*large_pointer_to_method)(
            int, char*);
           
        COMPILE_TIME_ASSERT(sizeof(
            large_pointer_to_method)==sizeof(greatest_pointer_type ));

        // Теперь сообщаем компилятору, что класс '_another_never_exist_class_'
        // является классом 'T' (шаблона)
        class _another_never_exist_class_ : public T {};
       
        reinterpret_cast<large_pointer_to_method&>(m_fn) = fn;

        // Дважды проверяем, чтобы убедиться, что компилятор его не изменяет
        COMPILE_TIME_ASSERT(
            sizeof(large_pointer_to_method)==sizeof(greatest_pointer_type ));
    }
};

Как было показано, мы заставили компилятор преобразовать указатель на метод известного класса в указатель на метод неизвестного класса. Другими словами, мы преобразовали указатель на метод из его наименьшего формата в его наибольший формат. Таким образом, мы имеем единый формат для всех видов указателей на функции/методы. В итоге, сравнение между экземплярами делегатов легко выполнимо. Это просто вызов стандартной функции C memcmp.

Делаем делегаты более быстрыми и расширяемыми

В вышеприведенном коде есть 2 проблемы:

  • Первая - оператор "switch... case" вынуждает эту реализацию выполняться немного медленнее, чем другие.
  • Вторая - если мы хотим расширить делегат так, чтобы он имел больше возможностей - т.е. поддерживал механизмы счетчика ссылок как интеллектуальные указатели или интерфейс COM – нам нужно больше места для хранения этой информации.

Полиморфизм может быть решением. Однако, характеристики делегата вида “один размер подходит всем" – это главная причина его существования. Из-за этого все методы и операторы шаблонов класса делегата могут быть сделаны невиртуальными. Здесь многие вспомнят так называемый "Стратегический шаблон проекта". Да, это также наш выбор. Однако остаются еще вопросы, которые нужно рассмотреть:

  • Использование "Стратегического шаблона проекта" вызывает непроизводительные затраты при вызове делегата: Пользовательское приложение передает параметры делегату; делегат передает параметры своей стратегии, и стратегия снова передает параметры реальному методу или функции. Но если все аргументы имеют простые типы, такие как char, long, int, указатель и ссылка, то компилятор автоматически генерирует оптимизированный код, который исключает такие непроизводительные затраты.
  • Стратегия или делегат должны хранить данные? Данные здесь означают указатель на объект (_never_exist_class* m_p) и указатель на адрес метода или функции (greatest_pointer_type m_fn). Если делегат хранит данные, он должен передавать данные стратегии. Такие операции подавляют оптимизацию кода компилятором. Если стратегия хранит данные, объект стратегии должен создаваться динамически. Это влечет за собой дорогостоящее выделение памяти (операции new, delete).

Две проблемы разрешаются, если мы применяем немного измененный Стратегический шаблон проекта:

  • Чтобы позволить компилятору оптимизировать код, мы помещаем данные в стратегию. Замечание: Помещение данных в стратегию заставляет ее выглядеть как шаблон проекта моста, но это не очень важно.
  • Чтобы избежать динамического выделения памяти, мы встроим весь объект стратегии в объект делегата вместо обычного хранения указателя на него.

Как реализованы стратегии?

Фактическая реализация стратегий использует шаблоны. Чтобы читатели могли во всем разобраться, приводится следующий пример кода:

class delegate_strategy // <void (int, char*)>
{
protected:
    class _never_exist_class;

    typedef void (_never_exist_class::*thiscall_method)(int, char*);
    typedef void (__cdecl _never_exist_class::*cdecl_method)(int, char*);
    typedef void (__stdcall _never_exist_class::*stdcall_method)(int, char*);
    typedef void (__fastcall _never_exist_class::*fastcall_method)(int, char*);
    typedef void (__cdecl *cdecl_function)(int, char*);
    typedef void (__stdcall *stdcall_function)(int, char*);
    typedef void (__fastcall *fastcall_function)(int, char*);

    class greatest_pointer_type
    {
        char never_use[sizeof(thiscall_method)];
    };

    _never_exist_class* m_p;
    greatest_pointer_type m_fn;

public:

        // чистая виртуальная функция
    virtual void operator()(int, char*) const
    {
        throw exception();
    }
};

class delegate_cdecl_function_strategy : public delegate_strategy
{
    // конкретная стратегия
    virtual void operator()(int i, char* s) const
    {
        return (*(*(cdecl_function*)(&m_fn)))(i, s);
    }

public:

    // конструктор
    delegate_cdecl_function_strategy(void (__cdecl *fn)(int, char*))
    {
        m_p = 0;
        reinterpret_cast<cdecl_function_type&>(m_fn) = fn;
        // заполняем резервные биты нулями для дальнейшего сравнения
        memset((char*)(&m_fn) + sizeof(fn), 0, sizeof(m_fn) - sizeof(fn));
    }
};

class delegate_thiscall_method_strategy : public delegate_strategy
{
    // конкретная стратегия
    virtual void operator()(int i, char* s) const
    {
        return (m_p->*(*(thiscall_method*)(&m_fn)))(i, s);
    }

public:

    // конструктор
    template<class T> delegate_thiscall_method_strategy(
        T* p, void (T::*fn)(int, char*))
    {
        m_p = reinterpret_cast<_never_exist_class*>(p);

        ///////////////////////////////////////////////////////////
        // Мы хотим выполнить следующее присваивание
        // m_fn = fn
        // Но как это сделать соответствующим стандарту и портативным способом?
        // Ниже приведен ответ

        ///////////////////////////////////////////////////////////

        // опережающая ссылка
        class _another_never_exist_class_;
        typedef void (
           _another_never_exist_class_::*large_pointer_to_method)(int, char*);
           
        COMPILE_TIME_ASSERT(sizeof(
            large_pointer_to_method)==sizeof(greatest_pointer_type ));

        // Теперь сообщаем компилятору, что класс '_another_never_exist_class_'
        // является 'T' классом (шаблоном)
        class _another_never_exist_class_ : public T {};
       
        reinterpret_cast<large_pointer_to_method&>(m_fn) = fn;

        // Дважды проверяем, чтобы убедиться, что компилятор его не изменяет
       
        COMPILE_TIME_ASSERT(sizeof(
            large_pointer_to_method)==sizeof(greatest_pointer_type ));
    }
};

class delegate2 // <void (int, char*)>
{
protected:
    char m_strategy[sizeof(delegate_strategy)];

    const delegate_strategy& strategy() const
    {
        return *reinterpret_cast(&m_strategy);
    }

public:
    // конструктор функции __cdecl
    delegate2(void (__cdecl *fn)(int, char*))
    {
        new (&m_strategy) delegate_cdecl_function_strategy(fn);
    }

    // конструктор
    template<class T>
        delegate2(T* p, void (T::*fn)(int, char*))
    {
        new (&m_strategy) delegate_thiscall_method_strategy(p, fn);
    }

    // Синтаксис 01: (*delegate)(param...)
    delegate_strategy const& operator*() const throw()
    {
        return strategy();
    }

    // Синтаксис 02: delegate(param...)
    // Замечание: синтаксис 02 может работать медленнее, чем синтаксис 01 в некоторых случаях
    void operator()(int i, char* s) const
    {
        return strategy()(i, s);
    }
};

Поддержка управления временем существования объекта

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

  • Что случится, если метод расположен внутри DLL, но эта DLL уже выгружена из области памяти процесса? У нас нет способа разрешать такие ситуации, поэтому мы просто игнорируем данную проблему.
  • Что случится, если объект каким-то образом будет удален из-за ошибки разработчика? Самый простой ответ таков: Разработчики должны сами заботиться о том, чтобы не допустить этой ошибки. Однако, ручное управление объектом всегда трудоемкое, порождает ошибки и снижает производительность труда разработчика. Поэтому мы попытались найти простой, но достаточно хороший способ для этой цели. Он приведен ниже:

Boost представляет концепции Clonable & Clone Allocator (клонируемый и распределитель клонов). Хотя это не очень гибкое решение для многих целей, его простота не делает эту библиотеку делегатов сложной. Поэтому данная библиотека использует концепции Boost и демонстрирует возможности следующих полезных классов Clone Allocator.

  • Класс "http://www.boost.org/libs/ptr_container/doc/reference.html#class-view-clone-allocator">view_clone_allocator является распределителем, который ничего не делает. Он аналогичен классу с таким же именем в Boost. Когда создается экземпляр делегата, если мы не передадим ему специальный распределитель, будет использовать этот по умолчанию.
  • Класс "http://www.boost.org/libs/ptr_container/doc/reference.html#class-heap-clone-allocator">heap_clone_allocator, который тоже аналогичен классу с таким же именем в Boost. Он использует динамическое распределение памяти и копирует конструктор, чтобы клонировать связанный объект.
  • Класс com_autoref_clone_allocator предоставлен для обеспечения поддержки COM интерфейса. Также он должен работать с любыми объектами класса, которые реализуют 2 метода AddRef & Release правильным образом.

Нужно запомнить одно правило при выполнении присваиваний между 2 экземплярами делегатов: целевой экземпляр делегата должен использовать распределитель клонов как источник объекта-клона. Логика присваивания следующая:

  1. Сначала целевой делегат (левая сторона) освобождает свой объект, используя его текущий clone allocator.
  2. Вся информация источника (правая сторона) будет скопирована в цель, включая clone allocator. Фактически, это просто побитовая копия.
  3. Цель клонирует новый объект, который она хранит, используя новый clone allocator.
  4. И так далее для последующих присваиваний между экземплярами делегатов.

Замечание: В действительной реализации мы уже рассмотрели и устранили проблему присваивания самому себе, при котором источник и цель совпадают.

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

delegate.bind(
    &object, &TheClass::a_method,
    clone_option< heap_clone_allocator >(true));

Ослабленные делегаты

При использовании библиотек неослабленных делегатов типы параметров шаблона, передаваемые делегату, проверяются очень строго. Например, если мы присвоим функцию с прототипом int (*)(long)long (*)(int), компилятор выдаст ошибку, сообщающую, что присваивание недопустимо, так как int и long имеют различные типы. Такое преобразование безопасно, потому что оно соответствует трем следующим условиям: делегату с прототипом

  • Число аргументов совпадает.
  • Каждый соответствующий аргумент может быть непосредственно преобразован из аргумента делегата в аргумент целевой функции посредством компилятора.
  • Возвращаемый тип может быть непосредственно преобразован из возвращаемого типа целевой функции в возвращаемый тип делегата посредством компилятора. Возвращаемый тип void является особым случаем: можно связать делегат, возвращающий void, с любым методом или функцией, удовлетворяющей двум вышеназванным условиям. Это примерно то же самое, что и вызов функции/метода, только не нужно беспокоиться о его возвращаемом значении.

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

Следующий фрагмент кода показывает использование этой библиотеки:

using namespace sophia;

// Базовый класс с виртуальным методом
struct BaseClass
{
   virtual int virtual_method(int param) const
   {
       printf("We are in BaseClass: (param = %d)\n", param);
       return param;
   }

   char relaxed_method(long param)
   {
       printf("We are in relaxed_method: (param = %d)\n", param);
       return 0;
   }
};

// класс с виртуальным наследованием
struct DerivedClass : public virtual BaseClass {
   virtual int virtual_method(int param) const
   {
       printf("We are in DerivedClass: (param = %d)\n", param);
       return param;
   }
};

void Test()
{
   // Предполагаем, что у нас есть несколько объектов
   DerivedClass object;

   // объявление делегатов
   typedef sophia::delegate0<DWORD> MyDelegate0;
   typedef sophia::delegate1<int, int> MyDelegate1;
   typedef sophia::delegate4<void, int, int, long, char> AnotherDelegateType;

   // определение размера экземпляра делегата
   printf("sizeof(delegate) = %d\n", sizeof(AnotherDelegateType));

   // Конструктор
   MyDelegate0 d0(&GetCurrentThreadId);
   MyDelegate1 d1(&object, &DerivedClass::virtual_method);
   MyDelegate1 d2; // нулевой делегат
   AnotherDelegateType dNull;

   // Сравниваем делегаты между собой, даже если они имеют разные типы
   assert(d2 == dNull);

   // связываем со свободной функцией или методом
   d0.bind(&GetCurrentThreadId);
   d0 = &GetCurrentThreadId;
   d2.bind(&object, &DerivedClass::virtual_method);

   // снова сравниваем после связывания
   assert(d2 == d1);

   // освобождаем делегат
   d2 = NULL; // or
   d2.clear();

   // вызов с помощью синтаксиса 01
   d1(1000);

   // вызов с помощью синтаксиса 02
   // этот синтаксис быстрее, чем синтаксис 01
   (*d1)(10000);

   // делегат RELAXED
   d1.bind(&object, &DerivedClass::relaxed_method);
   (*d1)(10000);

   // обмен между двумя делегатами
   d2.swap(d1); // now d1 == NULL

   // выполняем нулевой/пустой делегат
   assert(d1.empty());
   try
   {
       d1(100);
   }
   catch(sophia::bad_function_call& e)
   {
       printf("Exception: %s\n    Try again: ", e.what());
       d2(0);
   }

   // управление временем существования объекта
   // Случай 1: мы хотим, чтобы объект был клонирован
   d1.bind(&object, &DerivedClass::virtual_method,
       clone_option<heap_clone_allocator>(true));

   // управление временем существования объекта
   // Случай 2: мы не хотим, чтобы объект клонировался при связывании
   for(int i=0; i<100; ++i)
   {
       DerivedClass* pObject = new DerivedClass();
       d1.bind(pObject, &DerivedClass::virtual_method,
           clone_option<heap_clone_allocator>(false));
       d1(100);
   }

}

Сравнение производительности

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

  • Первое, параметры передаются делегату.
  • Второе, параметры передаются Stub, Invoker или Стратегии (эта библиотека).
  • Третье, параметры передаются реальному методу или функции.

Этот делегат показывает способ убрать один слой:

  • Синтаксис 01 использует 2 дополнительных слоя, так же, как и другие
    D(param1, param2)
  • Синтаксис 02 использует оператор разыменования, чтобы непосредственно вызвать объект стратегии
    (*D)(param1, param2)

Если мы передаем простые типы данных (char, int, указатель) любой из перечисленных реализаций делегатов, компилятор сгенерирует оптимизирующий код. Так что их скорости вызова не сильно различаются между собой. Но если мы передаем сложные типы данных, которые используют конструктор и деструктор, то 2й синтаксис будет выполняться быстрее любого другого.

Интересные особенности

  • Эта статья показывает способ преобразования  различных форматов указателя на метод/свободную функцию в унифицированный формат; преобразование выполняется совместимым со стандартом способом. В результате сравнение между экземплярами делегатов выполняется легко.
  • Стратегический шаблон проекта с небольшим изменением делает этот делегат быстрым и расширяемым.
  • Люди с небольшим опытом в программировании шаблонов на C++ могут понять исходный код.

Что дальше?

Групповой делегат

В этой статье обсуждался делегат типа Singlecast. Есть другой тип, так называемый делегат Multicast, внутренне являющийся коллекцией других делегатов. Когда мы вызываем Multicast, все делегаты Singlecast в коллекции также вызываются один за другим. В основном Multicast используется для реализации Шаблона проекта наблюдателя (Observer Design Pattern).

Более переносимый

Здесь приведен список компиляторов, с которыми тестировалась библиотека:

  • VC++ 7.1
  • VC++ 2005
  • Mingw32-gcc-3.4.5
Автор: Quynh Nguyen

Загрузить исходный код и демо-проект - 9.7 KB