Быстрые делегаты 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 байт для нас достаточно, но вы можете изменить размер буфера по умолчанию.)