Быстрые делегаты C++
ОГЛАВЛЕНИЕ
Введение
Дон Клагстон хорошо описал указатель функции-члена и его поведение в статье: "Указатели функции-члена и наиболее быстрые делегаты C++". Вкратце, указатели функции-члена не могут быть присвоены указателю данных void *, поэтому нужно осторожно обращаться с указателем функции-члена как с данные (функтор). FastestDelegate Дона – это самый быстрый делегат, как он утверждает, но он использовал reinterpret_cast<>, чтобы обмануть компилятор, что он назвал 'хитрым трюком' в своей статье.
Позже Сергей Рязанов представил быстрый делегат C++, который он считал таким же быстрым, как и FastestDelegate Дона, но полностью совместимым со стандартами C++. Его метод состоит в использовании шаблонизированной статической функции-члена ('заглушки'), чтобы сохранять/восстанавливать информацию о вызове функции (возвращаемый тип, тип аргумента, cv-спецификатор и зависящее от платформы соглашение о вызове) во время компиляции. В реальности это не является чем-то новым, поскольку аналогичный подход уже был представлен Ричем Хикеем в его статье "Обратные вызовы в C++ с не-типовой параметр шаблона. К сожалению, не многие доступные для приобретения компиляторы поддерживают эту стандартную возможность C++. Поэтому его код в некотором смысле не является переносимым. использованием шаблонов-функторов", 1994. Рич называл такую статическую функцию-член функцией 'переходником'. Тем не менее, метод Сергея уникален, так как он передает информацию о вызове члена-функции через
Есть еще одна отличная статья о том, как реализовать делегаты C++: "Еще одна реализация обобщенных функторов на C++", автор Алексей Трунов. Он объясняет и всесторонне анализирует требования обобщенных функторов и проблемы существующих делегатов.
Кстати, эти рассматриваются как 'быстрые', так как они могут избежать выделения памяти кучи для хранения указателей членов-функций, в отличие от boost::function.
Очередной быстрый делегат C++ - что нового в нем?
В нашем быстром делегате нет ничего нового. Все возможности, показанные в этой статье, уже существуют в других реализациях.
- Быстрый делегат. (В большинстве случаев не используется выделение памяти кучи.)
- Поддерживает три вызываемых объекта (свободная функция, функция-член и функтор).
- Совместимы со стандартами C++, поэтому портативны (проверены в VC6, VC71, и DEV-C++ 4.9.9.2 (Mingw/gcc 3.4.2)).
- Совместимы со стандартной библиотекой шаблонов (STL), подходят для конструирования копий и способны присваиваться.
- Равенство, меньше чем, и больше чем объекты сравнения.
- Правильность Cv-спецификатора (const).
- Поддержка зависящих от платформы соглашений о вызове (__stdcall, __fastcall, __cdecl).
- Предпочтительный синтаксис, переносимый синтаксис, или даже смешанный.
- Смягчение проверки соответствия типов.
- static_assert во время компиляции.
- Сохраняет только указатель (ссылку) на связанный объект или копирует связанный объект внутри. (новое)
- Сохраняет интеллектуальный указатель на связанный объект внутри. (новое)
- Пользовательский распределитель памяти. (новое)
- Настройка поведения/функций делегата путем указания соответствующих макроопределений.
Но что если все эти возможности доступны в одном делегате? Не звучит ли это многообещающе? Давайте посмотрим!
Восстановление типа без выделения памяти кучи
Начнем с пересмотра метода Сергея. Нужно время, чтобы информация о типе вызываемого объекта стала доступна, когда указатель функции-члена присваивается делегату; поэтому необходим какой-то механизм сохранения информации о типе указателя функции-члена в нетипизированную универсальную форму и восстановления их всякий раз, когда делегат будет вызываться позже. Сергей использует шаблонизированную статическую функцию-член, называемую 'method_stub', для этой цели, и также применяет указатель члена-функции как параметр шаблона без типа. Эти два метода позволяют избежать выделения памяти кучи. Но обе эти совместимые со стандартом C++ возможности принимаются только относительно новыми компиляторами.
class delegate
{
public:
delegate()
: object_ptr(0)
, stub_ptr(0)
{}
template < class T, void (T::*TMethod)(int) >
static delegate from_method(T* object_ptr)
{
delegate d;
d.object_ptr = object_ptr;
d.stub_ptr = &method_stub< T, TMethod >; // #1
return d;
}
void operator()(int a1) const
{
return (*stub_ptr)(object_ptr, a1);
}
private:
typedef void (*stub_type)(void* object_ptr, int);
void* object_ptr;
stub_type stub_ptr;
template < class T, void (T::*TMethod)(int) >
static void method_stub(void* object_ptr, int a1)
{
T* p = static_cast< T* >(object_ptr);
return (p->*TMethod)(a1); // #2
}
};
Согласно нашему опыту, старые компиляторы, такие как VC6, плохо справляются с шаблонизированными статическими функциями-членами. В то же время использование статической функции-члена вложенного шаблонного класса нормально принимается теми компиляторами. Поэтому мы изменили код так, как показано ниже:
class delegate
{
public:
delegate()
: object_ptr(0)
, stub_ptr(0)
{}
template < class T, void (T::*TMethod)(int) >
static delegate from_method(T* object_ptr)
{
delegate d;
d.object_ptr = object_ptr;
d.stub_ptr = &delegate_stub_t< T, TMethod >::method_stub; // #1
return d;
}
void operator()(int a1) const
{
return (*stub_ptr)(object_ptr, a1);
}
private:
typedef void (*stub_type)(void* object_ptr, int);
void* object_ptr;
stub_type stub_ptr;
template < class T, void (T::*TMethod)(int) >
struct delegate_stub_t
{
static void method_stub(void* object_ptr, int a1)
{
T* p = static_cast< T* >(object_ptr);
return (p->*TMethod)(a1); // #2
}
};
};
Одна проблема решена, но нам еще нужен механизм ' совместимый со стандартами C++ и переносимый', который может заменить параметр шаблона без типа указателя функции-члена. TMethod нужно сохранить в форме без типа в классе делегата. Но как мы знаем из статьи Дона, размер указателя функции-члена меняется в соответствии с особенностями наследования класса, к которому он принадлежит, а также в соответствии с производителями компиляторов. Поэтому динамическое выделение памяти неизбежно, если в противном случае мы не решим использовать буфер действительно огромного размера. (Анализ Дона показал, что максимальный размер указателя функции-члена примерно равен 20 ~ 24 байт, но мы не хотим вынести отказ, говоря, что "этот класс делегатов работает, только если размер указателя функции-члена меньше или равен 24 байтам".)
Если вы читали статью Рича "Обратные вызовы в C++ с использованием шаблонов-функторов ", вы поймете, что то, что мы делаем здесь, является копией того, что он сделал 10 лет назад. Но после прочтения статьи Алексея "Еще одна реализация обобщенных функторов в C++ - Статья о реализации обобщенных функторов в C++", мы осознали, что его метод меташаблона, чтобы заставить класс вести себя по-разному в зависимости от размера указателя функции-члена, можно применить здесь, и с помощью него мы сможем создать что-то эффективно работающее.
// версия с частичной специализацией
template < bool t_condition, typename Then, typename Else > struct If;
template < typename Then, typename Else >
struct If < true, Then, Else > { typedef Then Result; };
template < typename Then, typename Else >
struct If < false, Then, Else > { typedef Else Result; };
// версия с вложенной структурой шаблонов
template < bool t_condition, typename Then, typename Else >
struct If
{
template < bool t_condition_inner > struct selector;
template < > struct selector < true > { typedef Then Result; };
template < > struct selector < false > { typedef Else Result; };
typedef typename selector < t_condition >::Result Result;
};
Путем использования If < > меташаблона можно сделать так, чтобы указатель функции-члена сохранялся во внутреннем буфере, размер которого можно определить во время компиляции, и если размер указателя меньше размера внутреннего буфера; но если размер указателя функции-члена окажется больше размера внутреннего буфера, мы сможем выделить память кучи любого необходимого размера. И компилятор будет автоматически решать, что именно использовать.
class delegate
{
public:
delegate()
: object_ptr(0)
, stub_ptr(0), fn_ptr_(0), is_by_malloc(false)
{}
~delegate()
{
if(is_by_malloc)
{
is_by_malloc = false;
::free(fn_ptr_); fn_ptr_ = 0;
}
}
template < class T >
struct fp_by_value
{
inline static void Init_(delegate & dg,
T* object_ptr, void (T::*method)(int))
{
typedef void (T::*TMethod)(int);
dg.is_by_malloc = false;
new (dg.buf_) TMethod(method);
}
inline static void Invoke_(delegate & dg, T* object_ptr, int a1)
{
typedef void (T::*TMethod)(int);
TMethod const method = *reinterpret_cast < TMethod const * > (dg.buf_);
return (object_ptr->*method)(a1);
}
};
template < class T >
struct fp_by_malloc
{
inline static void init_(delegate & dg, T* object_ptr,
void (T::*method)(int))
{
typedef void (T::*TMethod)(int);
dg.fn_ptr_ = ::malloc(sizeof(TMethod));
dg.is_by_malloc = true;
new (dg.fn_ptr_) TMethod(method);
}
inline static void invoke_(delegate & dg, T* object_ptr, int a1)
{
typedef void (T::*TMethod)(int);
TMethod const method = *reinterpret_cast < TMethod const * > (dg.fn_ptr_);
return (object_ptr->*method)(a1);
}
};
template < class T >
struct select_fp_
{
enum { condition = sizeof(void (T::*)(int) <= size_buf) };
typedef fp_by_value<T> Then;
typedef fp_by_malloc<T> Else;
typedef typename If < condition, Then, Else >::Result type;
};
template < class T >
void from_method(T* object_ptr, void (T::*method)(int), int)
{
select_fp_<T>::type::Init_(*this, object_ptr, method);
this->object_ptr = object_ptr;
stub_ptr = &delegate_stub_t < T >::method_stub;
}
void operator()(int a1) const
{
return (*stub_ptr)(*this, object_ptr, a1);
}
private:
enum { size_buf = 8 };
typedef void (*stub_type)(delegate const & dg, void* object_ptr, int);
void* object_ptr;
stub_type stub_ptr;
union
{
void * fn_ptr_;
unsigned char buf_[size_buf];
};
bool is_by_malloc;
template < class T >
struct delegate_stub_t
{
inline static void method_stub(delegate const & dg, void* object_ptr, int a1)
{
T* p = static_cast< T* >(object_ptr);
return select_fp_<T>::type::invoke_(dg, p, a1);
}
};
};
Теперь у нас есть 'совместимая со стандартами C++ и переносимая' замена для указателя функции-члена Сергея в качестве нетипированного параметра шаблона. Были добавлены 2 или 3 уровня косвенной адресации, но компилятор со спусковой встроенной оптимизацией может исключить их и выработать код, эквивалентный коду Сергея.
Кроме того, теперь у нас есть двоичное представление указателя функции-члена как внутренней структуры данных, поэтому становится возможным сравнивать ее с другими. Иными словами, мы можем использовать делегаты в контейнерах STL, которая требует, чтобы ее компоненты имели возможность сравнения.
Размер внутреннего буфера можно настроить путем указания соответствующего макроопределения перед включением заголовочного файла. Мы выбрали 8 байт как размер по умолчанию в соответствии с размером таблицы указателей функций-членов из статьи Дона. (Мы в основном используем MSVC, и никогда не использовали виртуальное наследование, поэтому 8 байт для нас достаточно, но вы можете изменить размер буфера по умолчанию.)
Заглушка менеджера клонирования объектов
В предыдущей версии только указатель (ссылка) на связанный объект мог быть сохранен в делегате для указателя функции-члена (связывание по аргументу), чтобы ее можно было вызвать, когда делегат будет вызван позже. Мы решили добавить поддержку клонирования связанного объекта в делегате, чтобы указатель функции-члена мог быть вызван из внутренней копии связанного объекта, а не по указателю на связанный объект.
Только когда информация о типе указателя функции-члена или связанного с ним объекта будет доступна, тогда указатель функции-члена будет связан с делегатом. Поэтому необходимо сохранение типа заглушки для клонирования и разрушения объекта. Аналогичная статическая функция-член вложенного шаблонного класса, как показано выше, используемая для вызова указателя функции-члена, может быть снова использована здесь.
typedef void * (*obj_clone_man_type)(delegate &, void const *);
obj_clone_man_type obj_clone_man_ptr_;
template < typename T >
struct obj_clone_man_t
{
inline static void * typed_obj_manager_(delegate & dg, void const * untyped_obj_src)
{
T * typed_obj_src =const_cast < T * >
(static_cast < T const * > (untyped_obj_src)); typed_obj_src;
if(dg.obj_ptr_)
{
T * typed_obj_this = static_cast < T * > (dg.obj_ptr_);
delete typed_obj_this;
dg.obj_ptr_ = 0;
}
if(0 != typed_obj_src)
{
T * obj_new = new T(*typed_obj_src);
dg.obj_ptr_ = obj_new;
}
T * typed_obj = static_cast < T * > (dg.obj_ptr_); typed_obj;
return typed_obj;
}
}; // шаблон<имя-типа T> struct obj_clone_man_t
obj_clone_man_ptr_ = &obj_clone_man_t<T>::typed_obj_manager_;
Объект может быть успешно клонирован или разрушен путем использования obj_clone_man_ptr_, как показано ниже:
delegate dg1, dg2;
// копирует клонированный внутри объект dg2 в dg1
(*dg1.obj_clone_man_ptr_)(dg1, dg2.obj_ptr_);
// разрушает клонированный внутри объект dg1
(*dg1.obj_clone_man_ptr_)(dg1, 0);
Размер связанного объекта неизвестен, и он может иметь размер от нескольких байт до нескольких сотен байт, или даже больше. Поэтому выделение/освобождение памяти кучи неизбежно. Это противоречит требованию создания быстрых делегатов, так как главной целью использования быстрых делегатов было избегание использования памяти кучи любой ценой. (Пользовательский распределитель памяти, который будет описан позже, может помочь смягчить эту проблему.)
Мы решили включить функцию клонирования в наш делегат для поддержки интеллектуальных указателей. В отличие от C#, в C++ нет встроенного «сборщика мусора», но у нас есть интеллектуальные указатели. Чтобы работать с интеллектуальными указателями, нужно иметь возможность копировать или разрушать экземпляр интеллектуального указателя безопасным для типа способом (другими словами, должен вызываться надлежащий оператор присваивания или деструктор интеллектуального указателя). Для этой цели у нас уже есть функция-заглушка. Но есть еще два необходимых условия, которые нужно выполнить. (Идея заимствована из boost::mem_fn.)
- Функция get_pointer(), которая принимает ссылку или постоянную ссылку на интеллектуальный указатель и возвращает указатель на сохраненный целевой объект, должна находиться в подходящем пространстве имен (включая зависящую от аргументов проверку).
- Класс интеллектуального указателя должен предоставлять открытый интерфейс (typedef) element_type. (std::auto_ptr<T>, boost::shared_ptr, и его «брат или сестра», loki::smartPtr предоставляет этот публичный интерфейс.)
Следующие две версии get_pointer() реализованы в нашем делегате по умолчанию:
namespace fd
{
template<class T> inline
T * get_pointer(T * p)
{
return p;
}
template<class T> inline
T * get_pointer(std::auto_ptr<T> & p)
{
return p.get();
}
} // пространство имен fd
boost::shared_ptr определяет get_pointer() для себя в пространстве имен boost. Те компиляторы, которые реализуют проверку Кениг (зависимая от аргументов проверка), такие как VC71 или выше, GCC3.x.x.x, смогут увидеть определение, так что наш делегат может распознать и поддержать его без добавления каких-либо дополнительных строк кода. Но для тех компиляторов, которые не реализуют преобразование (проверку) аргументов должным образом или не имеют ее вовсе, мы можем создать соответствующий get_pointer() в пространстве имен fd.
#if defined(_MSC_VER) && _MSC_VER < 1300
// Даже если get_pointer определен в boost/shared_ptr.hpp, VC6 не
// реализует проверку Кениг (зависимую от аргументов проверку), то есть не может найти определение,
// Поэтому мы определяем get_pointer в явном виде в пространстве имен fd, чтобы решить проблему с плохим компилятором
namespace fd
{
template<class T>
T * get_pointer(boost::shared_ptr<T> const & p)
{
return p.get();
}
}
#endif // #если определен(_MSC_VER) && _MSC_VER < 1300
Использование кода
Предпочтительный синтаксис и переносимый синтаксис
#include "delegate.h"
// Предпочтительный синтаксис
fd::delegate < void (int, char *) > dg;
#include "delegate.h"
// Переносимый синтаксис
fd::delegate2 < void, int, char * > dg;
Предпочтительный синтаксис может быть принят только относительно новыми совместимыми со стандартами C++ компиляторами, такими как VC7.1 или выше, или GNU C++ 3.XX, в то время как переносимый синтаксис должен поддерживаться большинством компиляторов (полагаем, что наш быстрый делегат может быть легко перенесен в другие компиляторы без значительных проблем, так как он работал даже в печально известном VC6). (Замечание: Наш делегат был проверен только в VC6, VC7.1, и DEV-C++ 4.9.9.2 (Mingw/gcc 3.4.2).)
Если поддерживаются предпочтительный и переносимый синтаксисы, то можно использовать оба синтаксиса в комбинации (копирование, сравнение, создание копий, присваивание и так далее). Поэтому все примеры кода в дальнейшем будут показаны в переносимом синтаксисе.
Обертывание трех вызываемых сущностей
Вызываемая сущность имеет вызов функции operator (), и в число трех вызываемых сущностей входят:
- Свободные функции (включая статические функции-члены),
- Функции-члены, и
- Функторы (функции-объекты).
Эти три вызываемые сущности могут быть присвоены fd::delegate довольно простым образом, как boost::function, за исключением функторов.
// ======================================================================
// пример целевых вызываемых сущностей
// ======================================================================
class CBase1
{
public:
void foo(int n) const { }
virtual void bar(int n) { }
static void foobar(int n) { }
virtual void virtual_not_overridden(int n) { }
};
// ------------------------------
class CDerived1 : public CBase1
{
std::string name_;
public:
explicit CDerived1(char * name) : name_(name) { }
void foo(int n) const
{ name_; /*делаем что-то с name_ или с этим указателем*/ }
virtual void bar(int n) { name_; /* делаем что-то с name_ или с этим указателем */ }
static void foobar(int n) { }
void foofoobar(CDerived1 * pOther, int n)
{ name_; /* делаем что-то с name_ или с этим указателем */ }
};
// ------------------------------
void hello(int n) { }
void hellohello(CDerived1 * pDerived1, int n) { }
// ------------------------------
struct Ftor1
{ // функтор, не запоминающий состояние
void operator () (int n)
{ /*если нет состояния, то этот указатель не используется*/ }
};
struct Ftor2
{ // функтор, запоминающий состояние
string name_;
explicit Ftor2(char * name) : name_(name) { }
void operator () (int n)
{ name_; /* делаем что-то с name_ или с этим указателем */ }
};
Свободные функции
// создание копии
fd::delegate1 < void, int > dg1(&::hello);
fd::delegate1 < void, int > dg2 = &CBase1::foobar;
dg1(123);
dg2(234);
// присваивание
fd::delegate < void, int > dg3;
dg3 = &CDerived1::foobar;
dg3(345);
Функции-члены (Адаптер функции-члена)
CBase1 b1; CDerived1 d1("d1");
// создание копии
fd::delegate2 < void, CBase1 *, int > dg1(&CBase1::foo); // адаптер указателя
fd::delegate2 < void, CBase1 &, int > dg2 = &CBase1::bar; // адаптер ссылки
dg1(&b1, 123);
dg2(b1, 234);
// присваивание
fd::delegate2 < void, CDerived1 *, int > dg3;
dg3 = &CDerived1::foo;
dg3(&d1, 345);
Это может быть не тем, чего вы хотели достигнуть. Вы можете хотеть объявить делегат так: fd::delegate1 <void, int >, а не так: fd::delegate2 < void, CBase1 *, int >. В таком случае это называется связыванием аргументов для функции-члена и будет описано позже.
// взято из сетевой документации boost::function
template < typename P >
R operator()(cv-quals P& x, Arg1 arg1, Arg2 arg2, ..., ArgN argN) const
{
return (*x).*mf(arg1, arg2, ..., argN);
}
Интересно, что функцию-член можно адаптировать и вызвать, как показано выше. Хотя мы использовали boost::function таким образом, возникала иллюзия, что таким же образом вызывается сырой указатель функции-члена, и мы даже пробовали это сделать (компилятор сообщил об ошибке). Это специальное условие, и такая иллюзия вызывается огромным количеством внутреннего кода. Мы назвали это адаптером функции-члена.
Функторы (функции-объекты)
Ftor2 f2("f2");
// создание копии
bool dummy = true;
fd::delegate1 < void, int > dg1(f2, dummy);
// сохраняем внутри клонированный функтор
fd::delegate1 < void, int > dg2(&f2, dummy);
// сохраняем только указатель на функтор
dg1(123); // (внутренняя копия f2).operator ()(123);
dg2(234); // (&f2)->operator ()(234);
// присваивание ( специальный оператор <<= )
fd::delegate1 < void, int > dg3, dg4, dg5, dg6;
dg3 <<= f2; // сохраняем внутри клонированный функтор
dg4 <<= &f2; // сохраняем только указатель на функтор
dg5 <<= Ftor1(); // сохраняем внутри клонированный функтор
dg6 <<= &Ftor1(); // сохраняем только указатель на временный функтор
dg3(345); // (внутренняя копия f2).operator ()(345);
dg4(456); // (&f2)->operator () (456);
dg5(567); // (внутренняя копия временного Ftor1).operator ()(567);
dg6(678); // (&временный Ftor1, который уже
// был разрушен)->operator ()(678); Ошибка времени исполнения!
Сначала мы не рассматривали включение поддержки функторов. Когда мы изменили план (после завершения копирования кода для соглашения о вызовах), мы пытались реализовать нормальный оператор присваивания (operator =) для функтора, но это вызвало слишком много проблем с неоднозначностью перегруженных функций. Мы почти отказались от этого и планировали заставить пользователя реализовать это, например, так:
Ftor1 f1;
fd::delegate2 < void, Ftor1 *, int > dg1(&Ftor1::operator ());
dg1(&f1, 123);
Мы думали, что вам понравится operator <<= вместо вышеприведенного.
Делегат не может быть делегатом без чего-то такого, что он представляет. То есть обернутая целевая вызываемая сущность должна быть в достоверном состоянии, когда вызывается делегат. Это поведение несколько отличается от того, как boost::function присваивается функтору. По умолчанию boost::functionboost::ref or boost::crefВ предыдущей версии наш делегат только сохранял ссылку (указатель) на присваиваемый целевой функтор. Если это сохраняющий текущее состояние функтор, то вызывающий отвечает за то, чтобы сохранять целевой функтор незатронутым при вызове (точно такая же идея применяется к вызываемому объекту, позже связываемому с функцией-членом). внутри клонирует целевой функтор (выделение памяти кучи), если в противном случае не используется явно.
Но в новой версии добавлена функция клонирования связанного объекта, поэтому синтаксис operator <<=bool (логический) как второй аргумент для функтора. был изменен, чтобы отличить сохраненную версию и клонированную версию ссылки (указателя). Также выше приведен специальный конструктор копии, который принимает формальный параметр
Связывание аргументов функции-члена
CBase1 b1;
// создание копии
fd::delegate1 < void, int > dg1(&CBase1::foo, b1);
// сохраняем внутри клонированный связанный объект
fd::delegate1 < void, int > dg2(&CBase1::foo, &b1);
// сохраняем указатель на связанный объект
dg1(123); // (внутренняя копия b1).foo(123);
dg2(234); // (&b1)->foo(123);
// связывание члена
fd::delegate1 < void, int > dg3, dg4;
dg3.bind(&CBase1::bar, b1);
// сохраняем внутри клонированный связанный объект
dg4.bind(&CBase1::bar, &b1);
// сохраняем указатель на связанный объект
dg3(345); // (внутренняя копия b1).bar(345);
dg4(456); // (&b1)->bar(456);
// fd::bind() вспомогательная функция
fd::delegate1 < void, int > dg5 = fd::bind(&CBase1::foo, b1, _1);
// сохраняем внутри клонированный связанный объект
fd::delegate1 < void, int > dg6 = fd::bind(&CBase1::foo, &b1, _1);
// сохраняем указатель на связанный объект
dg5(567); // (внутренняя копия b1).foo(567);
dg6(678); // (&b1)->foo(678);
Указатель функции-члена должен вызываться для вызываемого объекта такого же типа. Вызываемый объект связан как ссылка (указатель), поэтому при вызове делегата он должен быть в достоверном состоянии. Вызывающий отвечает за то, чтобы сохранять целевой функтор незатронутым при вызове.
В новой версии связанный объект может быть клонирован внутри, или даже можно привязать интеллектуальный указатель, чтобы автоматически управлять памятью.
std::auto_ptr<CBase1> spb1(new CBase1);
fd::delegate1 < int, int > dg1;
dg1.bind(&CBase1::foo, spb1);
dg1(123);
// get_pointer(внутренняя копия spb1)->foo(123);
boost::shared_ptr<CBase1> spb2(new CBase1);
fd::delegate1 < int, int > dg2(&CBase1::foo, spb2);
dg2(234);
// get_pointer(внутренняя копия spb2)->foo(234);
Вспомогательная функция fd::bind() скопирована из идеи Джоди Хагинса для FastestDelegate Дона. Она позволяет легко переносить код с boost::function и boost::bind.
#include < boost/function.hpp >
#include < boost/bind.hpp >
using boost::function1;
using boost::bind;
CBase1 b1;
function1 < void, int > fn = bind( &CBase1::foo, &b1, _1 );
Вышеприведенный код можно легко преобразовать в:
#include "delegate.h"
using fd::delegate1;
using fd::bind;
CBase1 b1;
delegate1 < void, int > fn = bind( &CBase1::foo, &b1, _1 );
Но учтите, что метка-заполнитель _1 не работает здесь как boost::_1. Это всего лишь метка-заполнитель.
fd::make_delegate() вспомогательная функция
Она может быть полезна при передаче делегата как параметра функции.
typedef fd::delegate1 < void, int > MyDelegate1;
typedef fd::delegate2 < void, CDerived *, int > MyDelegate2;
void SomeFunction1(MyDelegate1 myDg) { }
void SomeFunction2(MyDelegate2 myDg) { }
CBase1 b1; CDerived1 d1("d1");
// версия свободной функции
SomeFunction1(fd::make_delegate(&::hello));
SomeFunction2(fd::make_delegate(&::hellohello);
// версия адаптера функции-члена
SomeFunction1(fd::make_delegate((CBase1 *)0, &CBase1::foobar));
SomeFunction2(fd::make_delegate((CDerived *)0, &CDerived1::foo));
// версия связывания аргументов функции-члена
SomeFunction1(fd::make_delegate(&CBase1::foo, &b1));
SomeFunction2(fd::make_delegate(&CDerived1::foofoobar, &d1);
SomeFunction1(fd::make_delegate(&CBase1::foo, b1));
SomeFunction2(fd::make_delegate(&CDerived1::foofoobar, d1);
Но версия адаптера функции-члена fd::make_delegate() должна трактоваться как отличная от другой версии fd::make_delegate(), и для этого есть причина.
Функция члена CBase1::virtual_not_overridden в этом примере – это публичная функция-член, и производный класс не заменяет ее. Так как это публичная функция-член, можно ссылаться на указатель функции-члена как на запись (обозначение) 'CDerived1::virtual_not_overridden'. Но когда эта запись указателя функции-члена передается к какой-либо автоматической шаблонной выводящей функции, такой как fd::make_delegate() в качестве аргумента, типом шаблона автоматически назначается 'CBase1::virtual_not_overridden', а не 'CDerived1::virtual_not_overridden'. Поэтому делегат, созданный из fd::make_delegate(), станет типом fd::delegate2 < void, CBase1 *, int >, в то время как нам нужен тип fd::delegate2 < void, CDerived1 *, int >. Вот почему нужно явно передавать типированный пустой указатель в качестве первого аргумента вспомогательной функции make_delegate() в этом случае. Аналогичная идея используется в облегченной проверке соответствия типов позже.
Сходства и различия
typedef fd::delegate1 < void, int > MyDelegate;
CBase1 b1, b2; CDerived1 d1("d1");
// ----------------------------------------------------------------------
MyDelegate dg1(&CBase1::foo, &b1);
MyDelegate dg2 = &::hello;
if(dg1 == dg2)
cout << "dg1 equals to dg2" << endl;
else
cout << "dg1 does not equal to dg2" << endl;
if(dg1 > dg2)
{
cout << "dg1 is greater than dg2" << endl;
dg1(123);
}
else if(dg1 < dg2)
{
cout << "dg2 is greater than dg1" << endl;
dg2(234);
}
// ----------------------------------------------------------------------
MyDelegate dg3 = dg1;
MyDelegate dg4(&CBase1::foo, &b2);
// указатель функции и связанный с ним указатель вызываемого объекта,
// сохраняемые в dg1, совпадают с теми, которые сохраняются в dg3
if(0 == dg1.compare(dg3))
{ // эта проверка возвращает истину
dg3(345);
}
if(0 == dg1.compare(dg3, true))
{ // эта проверка также возвращает истину
dg3(456);
}
// ----------------------------------------------------------------------
// указатель функции, сохраненный в dg1, совпадает с сохраненным в dg4,
// но связанные с ними указатели на вызываемый объект не совпадают
if(0 == dg1.compare(dg4))
{ // эта проверка возвращает истину
dg4(567);
}
if(0 == dg1.compare(dg4, true))
{ // эта проверка возвращает ложь
dg4(678);
}
// ----------------------------------------------------------------------
if(dg2 != 0)
{ // эта проверка возвращает истину
cout << "dg2 is not empty" << endl;
}
if(dg2)
{ // эта проверка возвращает истину
cout << "dg2 is not empty" << endl;
}
if(!!dg2)
{ // эта проверка возвращает истину
cout << "dg2 is not empty" << endl;
}
if(!dg2.empty())
{ // эта проверка возвращает истину
cout << "dg2 is not empty" << endl;
}
// ----------------------------------------------------------------------
dg1.swap(dg2);
MyDelegate(dg2).swap(dg1); // dg1 = dg2;
MyDelegate().swap(dg1); // dg1.clear();
dg2.clear();
dg3 = 0;
// ----------------------------------------------------------------------
if(dg3.empty())
{
try
{
dg3(789);
}
catch(std::exception & e) { cout << e.what() << endl; }
//исключение 'вызов пустого делегата'
}
// ----------------------------------------------------------------------
CBase1 * pBase = 0;
// намеренное связывание нулевого вызываемого объекта
dg3.bind(&CBase1::foo, pBase);
try
{
FD_ASSERT( !dg3.empty() );
dg3(890);
}
// исключение 'вызов функции-члена при отсутствующем объекте'
catch(std::exception & e) { cout << e.what() << endl; }
Сравнение двух делегатов означает сравнение адреса ячейки памяти, сохраненного внутри указателя функции, и это на самом деле не означает ничего особенного. Но это позволяет использовать наш делегат в контейнере STL без проблем. Так как это 'быстрый' делегат в 'большинстве' случаев, нам не нужно сильно беспокоиться о снижении производительности при копировании делегата внутрь контейнера STL согласно семантике значений.
Правильность const
CBase1 b1;
CBase1 const cb1;
// --------------------------------------------------
// связывание аргументов
MyDelegate dg1(&CBase1::foo, &b1);
MyDelegate dg2(&CBase1::foo, &cb1);
MyDelegate dg3(&CBase1::bar, &b1);
// ошибка компиляции! постоянная функция-член
// не может быть вызвана для непостоянного объекта
// MyDelegate dg4(&CBase1::bar, &cb1);
dg1(123);
dg2(234);
dg3(345);
// --------------------------------------------------
// адаптер функции-члена
fd::delegate2<INT, int *, CBase1> dg4(&CBase1::foo);
fd::delegate2<INT, int *, CBase1> dg5(&CBase1::bar);
fd::delegate2<INT, int *, CBase1 const> dg6(&CBase1::foo);
// ошибка компиляции! непостоянная функция-член
// не может использоваться для постоянного адаптера функции-члена
// fd::delegate2<INT, int *, CBase1 const> dg7(&CBase1::bar);
dg4(&b1, 456);
// ошибка компиляции! постоянный объект не может использоваться для
// непостоянного адаптера функции-члена
// dg4(&cb1, 456);
dg5(&b1, 567);
// ошибка компиляции! постоянный объект не может использоваться для
// непостоянного адаптера функции-члена
// dg5(&cb1, 567);
dg6(&b1, 678);
dg6(&cb1, 678);
Зависящее от платформы соглашение о вызове
Соглашение о вызове не является стандартной возможностью C++, но его нельзя игнорировать, так как оно используется в Win32 API и COM API. С точки зрения реализации оно заключается в скучном повторении одного и того же кода. По умолчанию, ни одно из зависящих от платформы соглашений о вызове не действует. Чтобы включить его, нужно указать соответствующее макроопределение перед включением "delegate.h".
- FD_MEM_FN_ENABLE_STDCALL – чтобы включить поддержку __stdcall для функции-члена
- FD_MEM_FN_ENABLE_FASTCALL - чтобы включить поддержку __fastcall для функции-члена
- FD_MEM_FN_ENABLE_CDECL - чтобы включить поддержку __cdecl для функции-члена
- FD_FN_ENABLE_STDCALL - чтобы включить поддержку __stdcall для свободной функции
- FD_FN_ENABLE_FASTCALL - чтобы включить поддержку __fastcall для свободной функции
- FD_FN_ENABLE_PASCAL – чтобы включить поддержку Pascal для свободной функции
- (Замечание) Поддержка соглашений о вызове работает только в MSVC на данный момент из-за недостаточного понимания gcc нами.
Смягчение проверки соответствия типов
Типы параметров шаблона, передаваемых делегату, проверяются намного строже, чем нужно в реальности. Нам может потребоваться работать с группой функций int (*)(int) и функций int (*)(long) совместно. Когда эти функции присваиваются в fd::delegate1 < int, int >, компилятор выдаёт ошибку для функций int (*)(long),int' и 'long' являются различными типами. говорящую, что их присваивание невозможно, так как '
Определив макрос FD_TYPE_RELAXATION перед включением "delegate.h" можно включить смягчение проверки соответствия типов. В двух словах, функция (свободная функция, функция-член или функтор) может быть присвоена или связана с fd::delegate во всех случаях, когда выполняются три следующих условия:
- Число аргументов согласовано.
- Каждый соответствующий аргумент заведомо может быть преобразован (из аргумента делегата в аргумент целевой функции) компилятором.
- Возвращаемый тип заведомо может быть преобразован (из возвращаемого типа делегата в возвращаемый тип целевой функции, и наоборот) в компилятор.
Если любое из данных условий не выполняется, компилятор сообщит об этом (предупреждение и/или сообщения об ошибке во время компиляции).
CBase1 b1;
//
// int CBase1::foo(int) const;
// int CBase1::bar(int);
//
fd::delegate1 < int, long > dg1(&CBase1::foo, &b1);
dg1(123);
Вышеприведенное определение делегата теоретически эквивалентно следующему определению функции:
CBase1 b1;
int fd_delegate1_dg1(long l)
{
return b1.foo(l);
}
fd_delegate1_dg1(123);
Это показывает, почему должны выполняться три условия для fd::delegate, чтобы он работал в режиме смягченной проверки типов.
// предупреждение компиляции! :преобразование 'return' : из '' to '', возможна потеря данных
fd::delegate1 < float, long > dg2(&CBase1::bar, &b1);
эквивалентно:
float fd_delegate1_dg2(long l)
{
// предупреждение компиляции! : возможна потеря данных
return b1.bar(l);
}
Возвращаемый тип 'int' может быть преобразован в возвращаемый тип 'float' без проблем, но преобразование возвращаемого типа 'float' в возвращаемый тип 'int' вызовет предупреждение о возможной потере данных.
// ошибка компиляции! : невозможно преобразовать параметр 3 из 'char *' в 'int'
fd::delegate1 < int, char * > dg3(&CBase1::foo, &b1);
эквивалентно:
int fd_delegate1_dg3(char * ch)
{
// ошибка компиляции! : невозможно преобразовать параметр 'ch' из 'char *' в 'int'
return b1.foo(ch);
}
и компилятор выдаст ошибку, так как 'char *' заведомо нельзя преобразовать в 'int'.
CDerived1 d1("d1");
//
// class CDerived1 : public CBase1 { };
//
fd::delegate2 < int, CDerived1 *, long > dg5(&CBase1::bar);
эквивалентно:
int fd_delegate2_dg5(CDerived1 * pd1, long l)
{
return pd1->bar(l);
}
и выброс от 'CDerived1 *' в 'CBase1 *' всегда безопасен, поэтому преобразование заведомо возможно.
fd::make_delegate() для режима смягченной проверки соответствия типов
[Устаревшее] Когда определяется FD_TYPE_RELAXATION, у наборов fd::make_delegate() включается поддержка этого режима. Так как fd::make_delegate() не может догадаться, какой тип делегата необходимо создать, вызывающий должен задать пустой указатель типа делегата как первый аргумент fd::make_delegate(). Аналогичная идея используется в версии адаптера функции-члена fd::make_delegate(),[Устарело] описанной раньше.
Целью использования make_delegate() является автоматическое выведение параметра шаблона, поэтому нет смысла использовать make_delegate, если дополнительная информация о типе обязательно предоставляется в качестве первого аргумента. Так как эта функция плохо работает в плохих компиляторах, таких как VC6, она удалена в новой версии.
static_assert (поддержка отладки)
Когда делегат присваивается или связывается с указателем функции, компилятор генерирует соответствующий вызов функции operator () во время компиляции. Если есть предупреждение или ошибка несоответствия типов данных, бывает сложно отследить, в каком месте она возникла. Компилятор с развитой логикой, такой как VC7.1, имеет удобную функцию отслеживания данных предупреждений или ошибок с подробной информацией о типе шаблона в пределах исходного кода пользователя, но VC6 не имеет этой возможности. (VC6 обычно предоставляет два уровня отслеживания.) Поэтому мы попытались поместить static_assert (FD_STATIC_ASSERT, FD_PARAM_TYPE_CHK) в как можно большее число мест, чтобы было легче отследить источник ошибок/предупреждений в исходном коде пользователя.
Поддержка пользовательского распределителя памяти
В новой версии наш делегат может использовать сервисы из любого пользовательского распределителя памяти, когда нужно выделить или освободить память для сохранения указателя функции-члена, размер которого больше размера внутреннего буфера, или для сохранения клонированного связанного объекта. std::allocator< void > использует память кучи, что является расточительным и очень медленным методом. Используя блок фиксированного размера (участок памяти), распределитель памяти может значительно повысить производительность для маленьких объектов по сравнению с использованием установленного по умолчанию std::allocator< void >. Конечно, степень выгоды от использования пользовательского распределителя будет изменяться в зависимости от деталей реализации используемого пользовательского распределителя.
Мы включили fd::util::fixed_allocator, который выделяет большой участок памяти сразу же для последующего использования маленькими объектами. Он был реализован на основе нескольких статей, найденных в CodeProject. Вы можете применять любой понравившийся вам пользовательский распределитель памяти.
Заключение
Если вас беспокоит только скорость, определите FD_DISABLE_CLONE_BOUND_OBJECT (дополнительные свободные 4 байта на делегат будут сэкономлены как бонус) и сохраните указатель на версию связанного объекта только функций-членов; иначе вы можете использовать интеллектуальный указатель на связанный объект и пользовательский распределитель памяти для настройки производительности путем нахождения равновесия между скоростью и надежностью. Поведение и функции делегата полностью настраиваются путем использования нужного макроса (смотрите файл "config.hpp").
Для тех, кто хочет увидеть детали реализации после макрорасширения, мы также включили руководство, описывающее, как извлечь упрощенную версию из полной версии.
Определение
namespace fd
{
// ----------------------------------------------------------------------
class bad_function_call;
class bad_member_function_call;
// ======================================================================
//
// класс delegateN (переносимый синтаксис)
//
// ======================================================================
template < typename R,typename T1,typename T2,...,typename TN,
typename Alloocator = std::allocator < void > ,
size_t t_countof_pvoid = 2 >
class delegateN;
// ----------------------------------------------------------------------
// по умолчанию c'tor
delegateN< R, T1, T2, ..., TN >::delegateN();
// ----------------------------------------------------------------------
// копируем c'tor для 0
delegateN< R, T1, T2, ..., TN >::delegateN(implClass::clear_type const *);
// ----------------------------------------------------------------------
// копируем c'tor
delegateN< R, T1, T2, ..., TN >::delegateN(delegateN< R, T1,
T2, ..., TN > const & other);
// ----------------------------------------------------------------------
// функция копирует c'tor
delegateN< R, T1, T2, ..., TN >::delegateN(R (*fn)(T1, T2, ..., TN);
// ----------------------------------------------------------------------
// адаптер функции-члена копирует c'tors
// ,в которых T1 заведомо можно преобразовать в U * или U &
delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T2, T3, ..., TN));
// ,в которых T1 заведомо можно преобразовать в один из
// U * или U const * или U & или U const &
delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T2, T3, ..., TN) const);
// ----------------------------------------------------------------------
// связывание аргументов функции-члена копирует c'tors
delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T1, T2, ..., TN), T & obj);
delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T1, T2, ..., TN) const, T & obj);
delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T1, T2, ..., TN), T * obj);
delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T1, T2, ..., TN) const, T * obj);
// ----------------------------------------------------------------------
// функтор копирует c'tors
template< typename Functor >
delegateN< R, T1, T2, ..., TN >::delegateN(Functor & ftor, bool/* dummy*/);
template< typename Functor >
delegateN< R, T1, T2, ..., TN >::delegateN(Functor * ftor, bool/* dummy*/);
// ----------------------------------------------------------------------
// присваивание из 0
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator = (implClass::clear_type const *);
// ----------------------------------------------------------------------
// оператор присваивания
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator =
(delegateN< R, T1, T2, ..., TN > const & other);
// ----------------------------------------------------------------------
// оператор присваивания функции
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator = (R (*fn)(T1, T2, ..., TN);
// ----------------------------------------------------------------------
// операторы присваивания адаптера функции-члена
// ,в которых T1 можно заведомо преобразовать в U * или U &
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T2, ..., TN));
// ,где T1 можно заведомо преобразовать в один из U * или U const * или U & или U const &
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T2, ..., TN) const);
// ----------------------------------------------------------------------
// операторы присваивания связываемых аргументов функции-члена
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T1, T2, ..., TN), T & obj);
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T1, T2, ..., TN) const, T & obj);
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T1, T2, ..., TN), T * obj);
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T1, T2, ..., TN) const, T * obj);
// ----------------------------------------------------------------------
// операторы присваивания функтора
template< typename Functor >
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator <<= (Functor & ftor);
template< typename Functor >
delegateN< R, T1, T2, ..., TN > &
delegateN< R, T1, T2, ..., TN >::operator <<= (Functor * ftor);
// ----------------------------------------------------------------------
// оператор вызова
result_type operator ()(T1 p1, T2 p2, ..., TN pN) const;
// ----------------------------------------------------------------------
// обмен местами
void delegateN< R, T1, T2, ..., TN >::swap(delegateN & other);
// ----------------------------------------------------------------------
// очистка
void delegateN< R, T1, T2, ..., TN >::clear();
// ----------------------------------------------------------------------
// очищение
bool delegateN< R, T1, T2, ..., TN >::empty() const;
// ----------------------------------------------------------------------
// сравнение с 0
bool operator == (implClass::clear_type const *) const;
bool operator != (implClass::clear_type const *) const;
// ----------------------------------------------------------------------
// сравнение
int compare(delegateN const & other, bool check_bound_object = false) const;
// операторы сравнения
bool operator == (delegateN< R, T1, T2, ..., TN > const & other) const;
bool operator != (delegateN< R, T1, T2, ..., TN > const & other) const;
bool operator <= (delegateN< R, T1, T2, ..., TN > const & other) const;
bool operator < (delegateN< R, T1, T2, ..., TN > const & other) const;
bool operator >= (delegateN< R, T1, T2, ..., TN > const & other) const;
bool operator > (delegateN< R, T1, T2, ..., TN > const & other) const;
// ======================================================================
// класс делегата (предпочтительный индекс)
// ======================================================================
template< typename R,typename T1,typename T2,...,typename TN,
typename Allocator = std::allocator< void >,size_t t_countof_pvoid = 2 >
class delegate< R (T1, T2, ..., TN), Allocator, t_countof_pvoid >;
//
// такой же набор членов-функций, что и для fd::delegateN в переносимом синтаксисе
//
// ======================================================================
// fd::make_delegate()
// ======================================================================
// make_delegate для функции
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
make_delegate(R (*fn)(T1, T2, ..., TN));
// ----------------------------------------------------------------------
// make_delegate для адаптера функции-члена
template< typename R,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T *, T2, ..., TN >
make_delegate(T *, R (U::*mfn)(T2, ..., TN));
template< typename R,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T *, T2, ..., TN >
make_delegate(T *, R (U::*mfn)(T2, ..., TN) const);
// ----------------------------------------------------------------------
// make_delegate для связанных аргументов функции-члена
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
make_delegate(R (U::*mfn)(T1, T2, ..., TN), T & obj);
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
make_delegate(R (U::*mfn)(T1, T2, ..., TN) const, T & obj);
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
make_delegate(R (U::*mfn)(T1, T2, ..., TN), T * obj);
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
make_delegate(R (U::*mfn)(T1, T2, ..., TN) const, T * obj);
// ======================================================================
// fd::bind()
// ======================================================================
// связывание для связанных аргументов функции-члена
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
bind(R (U::*mfn)(T1, T2, ..., TN), T & obj, ...);
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
bind(R (U::*mfn)(T1, T2, ..., TN) const, T & obj, ...);
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
bind(R (U::*mfn)(T1, T2, ..., TN), T * obj, ...);
template< typename R,typename T1,typename T2,...,typename TN,typename U,typename T >
delegateN< R, T1, T2, ..., TN >
bind(R (U::*mfn)(T1, T2, ..., TN) const, T * obj, ...);
// ======================================================================
// fd::get_pointer()
// ======================================================================
template< typename T >
T * get_pointer(T * p);
template< typename T >
T * get_pointer(std::auto_ptr< T > & p);
// ----------------------------------------------------------------------
namespace util
{
// ======================================================================
// пользовательские распределители памяти (управляемый политиками)
// ======================================================================
// распределитель фиксированных блоков памяти
template< typename T >
class fixed_allocator;
// обычный распределитель памяти( эквивалентно std::allocator< T > )
template< typename T >
class std_allocator;
} // использование пространства имен
// ----------------------------------------------------------------------
} // пространство имен fd
Загрузить исходный код - 32.6 KB
Загрузить демо-проект (для VC6, VC71, и DEV-C++ 4.9.9.2) - 127 KB