Быстрые делегаты C++ - Восстановление типа без выделения памяти кучи
ОГЛАВЛЕНИЕ
Восстановление типа без выделения памяти кучи
Начнем с пересмотра метода Сергея. Нужно время, чтобы информация о типе вызываемого объекта стала доступна, когда указатель функции-члена присваивается делегату; поэтому необходим какой-то механизм сохранения информации о типе указателя функции-члена в нетипизированную универсальную форму и восстановления их всякий раз, когда делегат будет вызываться позже. Сергей использует шаблонизированную статическую функцию-член, называемую '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 байт для нас достаточно, но вы можете изменить размер буфера по умолчанию.)