Новый способ реализовать делегаты с помощью C++ - Внутренняя структура
ОГЛАВЛЕНИЕ
Внутренняя структура
Данный раздел показывает, как реализована эта библиотека, шаг за шагом.
Структура данных делегата
Структура данных делегата включает следующую информацию:
- Число и тип аргументов
- Тип возвращаемого значения
- Метод или свободная функция
- Соглашение о вызовах
- Адрес метода или функции, на которую указывает делегат
Замечание: Нам не нужно сохранять информацию об атрибутах "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 экземплярами делегатов: целевой экземпляр делегата должен использовать распределитель клонов как источник объекта-клона. Логика присваивания следующая:
- Сначала целевой делегат (левая сторона) освобождает свой объект, используя его текущий clone allocator.
- Вся информация источника (правая сторона) будет скопирована в цель, включая clone allocator. Фактически, это просто побитовая копия.
- Цель клонирует новый объект, который она хранит, используя новый clone allocator.
- И так далее для последующих присваиваний между экземплярами делегатов.
Замечание: В действительной реализации мы уже рассмотрели и устранили проблему присваивания самому себе, при котором источник и цель совпадают.
В ряде случаев нужно связать уже клонированный объект с экземпляром делегата. В таком случае нам нужно, чтобы делегат удалял объект автоматически, но не клонировал его снова. Чтобы достичь этого, при связывании объекта и его метода с делегатом нам нужно предоставить 2 дополнительных куска информации: первым является класс распределителя клонов; вторым является логическое значение, сообщающее, должен делегат клонировать объект или нет.
delegate.bind(
&object, &TheClass::a_method,
clone_option< heap_clone_allocator >(true));
Ослабленные делегаты
При использовании библиотек неослабленных делегатов типы параметров шаблона, передаваемые делегату, проверяются очень строго. Например, если мы присвоим функцию с прототипом int (*)(long)long (*)(int), компилятор выдаст ошибку, сообщающую, что присваивание недопустимо, так как int и long имеют различные типы. Такое преобразование безопасно, потому что оно соответствует трем следующим условиям: делегату с прототипом
- Число аргументов совпадает.
- Каждый соответствующий аргумент может быть непосредственно преобразован из аргумента делегата в аргумент целевой функции посредством компилятора.
- Возвращаемый тип может быть непосредственно преобразован из возвращаемого типа целевой функции в возвращаемый тип делегата посредством компилятора. Возвращаемый тип void является особым случаем: можно связать делегат, возвращающий void, с любым методом или функцией, удовлетворяющей двум вышеназванным условиям. Это примерно то же самое, что и вызов функции/метода, только не нужно беспокоиться о его возвращаемом значении.