65.9K
CodeProject 正在变化。 阅读更多。
Home

快速 C++ 委托

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (46投票s)

2006年3月2日

CPOL

19分钟阅读

viewsIcon

220739

downloadIcon

3661

一种快速 C++ 委托的实现,它具有可移植性并符合 C++ 标准。

引言

Don Clugston 在他的文章“成员函数指针和最快的 C++ 委托”中,以一种组织非常好的方式解释了成员函数指针及其行为。简而言之,成员函数指针不能被赋值给一个 void * 数据指针,因此需要特别小心地将成员函数指针作为数据(仿函数)来处理。正如 Don 所宣称的,他的 FastestDelegate 是可能的最快委托,但他利用了 reinterpret_cast<> 来欺骗编译器,他自己在文章中称之为“可怕的黑客技术”。

不久之后,Sergey Ryazanov 提出了一个快速的 C++ 委托,他发现这个委托和 Don 的 FastestDelegate 一样快,而且完全符合 C++ 标准。他的技术是使用一个模板化的静态成员函数('stub')在编译时存储/恢复函数调用信息(返回类型、参数类型、cv-限定符以及平台特定的调用约定)。实际上,这并不是新技术,因为 Rich Hickey 在他 1994 年的文章“在 C++ 中使用模板仿函数实现回调”中已经介绍过类似的方法。Rich 将这种静态成员函数称为“thunk”函数。然而,Sergey 的技术是独特的,因为他通过非类型模板参数来传递成员函数调用信息。不幸的是,没有多少商用编译器能够支持这个真正的 C++ 标准特性。因此,从这个意义上说,他的代码并不是真正可移植的。

在 CodeProject 上还有一篇关于如何实现 C++ 委托的优秀文章。它就是 Aleksei Trunov 的“C++ 中又一种广义仿函数的实现”。他详细地解释和分析了广义仿函数的需求以及现有委托存在的问题。他的文章启发了我开始实现自己的快速委托,我将在下文中对此进行解释。

顺便说一下,这些技术被认为是“快速的”,因为它们能够避免为存储成员函数指针而进行堆内存分配,这与 boost::function 不同。

又一个快速 C++ 委托?那么,有什么新东西吗?

不,我的快速委托里没有任何新东西。我将在本文中展示的所有特性在其他人的实现中已经存在了。

  1. 快速委托。(在“大多数”情况下没有堆内存分配。)
  2. 支持三种可调用实体(自由函数、成员函数和仿函数)。
  3. 符合 C++ 标准,因此可移植(已在 VC6、VC71 和 DEV-C++ 4.9.9.2 (Mingw/gcc 3.4.2) 中测试)。
  4. 与 STL 容器兼容,可拷贝构造和赋值。
  5. 相等、小于和大于比较。
  6. Cv-限定符 (const) 正确性。
  7. 支持平台特定的调用约定 (__stdcall, __fastcall, __cdecl)。
  8. 首选语法、可移植语法,甚至混合使用。
  9. 类型检查松弛。
  10. 编译时 static_assert
  11. 既可以只存储绑定对象的指针(引用),也可以在内部克隆绑定对象。(新)
  12. 在内部存储指向绑定对象的智能指针。(新)
  13. 自定义内存分配器。(新)
  14. 通过定义相关宏来自定义委托的行为/特性。

但是,如果这些特性都集中在一个实现里呢?这听起来是不是很诱人?让我们来看看吧!

无需堆内存分配的类型恢复

让我们从回顾 Sergey 的技术开始。只有在成员函数指针被赋给委托时,被调用对象的类型信息才是可用的;因此,需要某种机制将成员函数指针的类型信息存储为一种非类型的通用形式,然后在每次调用委托时恢复它。Sergey 为此使用了一个名为 '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 应该以非类型形式存储在委托类中。但是,正如我们都从 Don 的伟大文章中学到的,成员函数指针的大小根据其所属类的继承特性以及编译器厂商的不同而变化。因此,除非我们决定使用一个非常巨大的缓冲区,否则动态内存分配是“不可避免的”。(Don 的分析表明,成员函数指针的最大大小约为 20 ~ 24 字节,但我不想加一个免责声明说“这个委托类仅在成员函数指针大小小于或等于 24 字节时才有效”。)

如果你读过 Rich 的文章“在 C++ 中使用模板仿函数实现回调”,你会意识到我现在所做的只是重复他 10 年前做过的事情。但是,在我读了 Aleksei 的文章“C++ 中又一种广义仿函数的实现 - 一篇关于 C++ 中广义仿函数实现的文章”后,我意识到他用来让类根据成员函数指针的大小表现出不同行为的元模板(meta meta-template)技术可以应用在这里,我或许可以创造出一些很棒的东西。

// partial specialization version

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; };
// nested template structure version

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++ 标准且可移植”的替代方案,来代替 Sergey 的以成员函数指针作为非类型模板参数的方法。虽然增加了两到三级的间接引用,但具有良好内联优化的编译器可以将其优化掉,生成与 Sergey 的代码等效的代码。

除此之外,我们现在有了一个作为内部数据结构的成员函数指针的二进制表示,这样就可以将它与其他指针进行比较。换句话说,我们可以在需要其元素具备比较能力的 STL 容器中使用委托。(请参见附带的演示项目。)

内部缓冲区的大小可以通过在包含头文件之前定义一个适当的宏来自定义。根据 Don 文章中的成员函数指针大小表,我选择了 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;
    }

  };  // template<typename 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;

// copy the internally cloned object of dg2 into dg1

(*dg1.obj_clone_man_ptr_)(dg1, dg2.obj_ptr_);

// destroy the internally cloned object of dg1

(*dg1.obj_clone_man_ptr_)(dg1, 0);

绑定对象的大小是未知的,它可以小到几个字节,也可以大到几百个字节,甚至更多。因此,堆内存的分配/释放是不可避免的。这与“快速委托”的设计标准相冲突,因为使用快速委托的主要目的是不惜一切代价避免使用堆内存。(稍后将提到的自定义内存分配器可能会在缓解这个问题上发挥不错的作用。)

实际上,我决定在我的委托中引入克隆功能是为了支持智能指针。与 C# 不同,C++ 没有内置的垃圾回收器,但我们有智能指针。为了与智能指针一起工作,它需要能够以类型安全的方式复制或销毁智能指针实例(换句话说,必须调用智能指针的适当赋值运算符或析构函数)。我们已经有了一个“存根”函数来服务于此目的。但是,还需要满足另外两个先决条件/条件。(这个想法借鉴自 boost::mem_fn。)

  1. 必须在合格的命名空间中(包括参数依赖查找)提供一个名为 get_pointer() 的函数,该函数接受智能指针的引用或常量引用,并返回指向存储的目标对象的指针。
  2. 智能指针类必须公开一个 element_type 的公共接口(typedef)。(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();
  }

}  // namespace fd

boost::shared_ptrboost 命名空间中为自己定义了 get_pointer()。因此,那些实现了 Koenig 查找(参数依赖查找)的编译器,如 VC71 或更高版本,GCC3.x.x.x,将能够看到该定义,从而我的委托无需添加任何额外代码就能识别并支持它。但对于那些没有正确实现参数查找,或者根本没有这个功能的编译器,我们可以通过在 fd 命名空间中提供适当的 get_pointer() 来帮助它们。

#if defined(_MSC_VER) && _MSC_VER < 1300
// Even thogh get_pointer is defined in boost/shared_ptr.hpp, VC6 doesn't seem to

// implement Koenig Lookup (argument dependent lookup) thus can't find the definition,

// So we define get_pointer explicitly in fd namesapce to help the poor compiler

namespace fd
{
  template<class T>
    T * get_pointer(boost::shared_ptr<T> const & p)
  {
    return p.get();
  }
}
#endif  // #if defined(_MSC_VER) && _MSC_VER < 1300

使用代码

首选语法和可移植语法

#include "delegate.h"
// Preferred Syntax

fd::delegate < void (int, char *) > dg;
#include "delegate.h"
// Portable Syntax

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 (),三种可调用实体是

  • 自由函数(包括静态成员函数),
  • 成员函数,以及
  • 仿函数(函数对象)。

这三种可调用实体可以以与 boost::function 非常相似的方式赋给 fd::delegate,除了仿函数。

// ======================================================================

// example target callable entities

// ======================================================================


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_; /*do something with name_ or this pointer*/ }
  virtual void bar(int n) { name_; /*do something with name_ or this pointer*/ }
  static void foobar(int n) { }
  void foofoobar(CDerived1 * pOther, int n)
 { name_; /*do something with name_ or this pointer*/ }
};

// ------------------------------


void hello(int n) { }
void hellohello(CDerived1 * pDerived1, int n) { }

// ------------------------------


struct Ftor1
{ // stateless functor

  void operator () (int n)
 { /*no state nor using this pointer*/ }
};

struct Ftor2
{ // functor with state

  string name_;
  explicit Ftor2(char * name) : name_(name) { }
  void operator () (int n)
 { name_; /*do something with name_ or this pointer*/ }
};

自由函数

// copy-constructed

fd::delegate1 < void, int > dg1(&::hello);
fd::delegate1 < void, int > dg2 = &CBase1::foobar;

dg1(123);
dg2(234);

// assigned

fd::delegate < void, int > dg3;
dg3 = &CDerived1::foobar;

dg3(345);

成员函数(成员函数适配器)

CBase1 b1; CDerived1 d1("d1");

// copy-constructed
fd::delegate2 < void, CBase1 *, int > dg1(&CBase1::foo); // pointer adapter
fd::delegate2 < void, CBase1 &, int > dg2 = &CBase1::bar; // reference adapter


dg1(&b1, 123);
dg2(b1, 234);

// assigned

fd::delegate2 < void, CDerived1 *, int > dg3;
dg3 = &CDerived1::foo;

dg3(&d1, 345);

或许,这可能不是你想要实现的效果。你可能希望声明一个像这样的委托:fd::delegate1 < void, int >,而不是:fd::delegate2 < void, CBase1 *, int >。如果是这样,这被称为成员函数的“参数绑定”,稍后会讲到。

// excerpted from boost::function online document
template < typename P >
R operator()(cv-quals P& x, Arg1 arg1, Arg2 arg2, ..., ArgN argN) const
{
  return (*x).*mf(arg1, arg2, ..., argN);
}

非常有趣的是,成员函数可以像上面那样被适配和调用。当我以这种方式使用 boost::function 时,我几乎产生了一种错觉,认为原始的成员函数指针也可以用同样的方式调用,实际上,我甚至尝试过(然后编译器向我抱怨了 :P)。这是一种特殊的供给,它涉及到大量的内部编码来营造这种错觉。我将称之为“成员函数适配器”。

仿函数(函数对象)(已更新)

Ftor2 f2("f2");

// copy-constructed

bool dummy = true;
fd::delegate1 < void, int > dg1(f2, dummy);
// store the cloned functor internally

fd::delegate1 < void, int > dg2(&f2, dummy);
// store only the pointer to the functor


dg1(123);  // (internal copy of f2).operator ()(123);

dg2(234);  // (&f2)->operator ()(234);


// assigned ( special operator <<= )

fd::delegate1 < void, int > dg3, dg4, dg5, dg6;
dg3 <<= f2;       // store the cloned functor internally

dg4 <<= &f2;      // store only the pointer to the functor

dg5 <<= Ftor1();  // store the cloned functor internally

dg6 <<= &Ftor1(); // store only the pointer to the functor which is temporary


dg3(345);  // (internal copy of f2).operator ()(345);

dg4(456);  // (&f2)->operator () (456);

dg5(567);  // (internal copy of temporary Ftor1).operator ()(567);

dg6(678);  // (&temporary Ftor1 that has been
           // already destroyed)->operator ()(678); Runtime error!

一开始我没有考虑加入对仿函数的支持。当我改变计划时(甚至在我完成了调用约定的繁琐无聊的代码复制之后),我尝试为仿函数实现普通的赋值运算符 (operator =),但这给我带来了很多重载函数歧义的问题。所以,我几乎放弃了这项支持,并计划强制用户自己实现,类似这样

Ftor1 f1;
fd::delegate2 < void, Ftor1 *, int > dg1(&Ftor1::operator ());
dg1(&f1, 123);

我真傻!我希望你会对使用 operator <<= 而不是上面的方式感到满意。

一个委托不能没有它所代表的东西。也就是说,在调用委托时,被包装的目标可调用实体必须处于有效状态。这种行为与 boost::function 从仿函数赋值的方式有些不同。默认情况下,boost::function 会在内部克隆目标仿函数(堆内存分配),除非明确使用了 boost::refboost::cref在之前的版本中,我的委托只存储指向所赋值目标仿函数的引用(指针)。所以,如果它是一个有状态的仿函数,调用者有责任保持目标仿函数完好无损以便被调用(完全相同的思想也适用于后面为成员函数绑定的被调用对象)。

但是在新版本中,我添加了克隆绑定对象的功能,因此 operator <<= 的语法已经改变,以区分存储引用(指针)的版本和克隆版本。此外,上面还展示了一个特殊的拷贝构造函数,它接受一个虚拟的 bool 作为仿函数的第二个参数。

成员函数参数绑定 (已更新)

CBase1 b1;

// copy-constructed
fd::delegate1 < void, int > dg1(&CBase1::foo, b1);
// storing the cloned bound object internally

fd::delegate1 < void, int > dg2(&CBase1::foo, &b1);
// storing the pointer to the bound object

dg1(123); // (internal copy of b1).foo(123);

dg2(234); // (&b1)->foo(123);


// bind member

fd::delegate1 < void, int > dg3, dg4;
dg3.bind(&CBase1::bar, b1);
// storing the cloned bound object internally

dg4.bind(&CBase1::bar, &b1);
// storing the pointer to the bound object

dg3(345); // (internal copy of b1).bar(345);

dg4(456); // (&b1)->bar(456);


// fd::bind() helper function

fd::delegate1 < void, int > dg5 = fd::bind(&CBase1::foo, b1, _1);
// storing the cloned bound object internally

fd::delegate1 < void, int > dg6 = fd::bind(&CBase1::foo, &b1, _1);
// storing the pointer to the bound object

dg5(567); // (internal copy of 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(internal copy of spb1)->foo(123);

boost::shared_ptr<CBase1> spb2(new CBase1);
fd::delegate1 < int, int > dg2(&CBase1::foo, spb2);
dg2(234);
// get_pointer(internal copy of spb2)->foo(234);

辅助函数 fd::bind() 复制自 Jody Hagins 为 Don 的 FastestDelegate 贡献的想法/代码。它使得从 boost::functionboost::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");

// free function version

SomeFunction1(fd::make_delegate(&::hello));
SomeFunction2(fd::make_delegate(&::hellohello);

// member function adapter version

SomeFunction1(fd::make_delegate((CBase1 *)0, &CBase1::foobar));
SomeFunction2(fd::make_delegate((CDerived *)0, &CDerived1::foo));

// member function argument binding version

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 成员函数是一个 public 成员函数,并且派生类没有重写它。由于它是一个 public 成员函数,将其成员函数指针表示为 '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);

// both function pointer and its bound callee object pointer
// stored in dg1 is the same as those stored in dg3

if(0 == dg1.compare(dg3))
{  // this test return true

  dg3(345);
}
if(0 == dg1.compare(dg3, true))
{  // this test return true as well

  dg3(456);
}

// ----------------------------------------------------------------------


// function pointer stored in dg1 is the same as that stored in dg4

// but their bound callee object pointers are not the same

if(0 == dg1.compare(dg4))
{  // this test return true

  dg4(567);
}
if(0 == dg1.compare(dg4, true))
{  // this test return fail

  dg4(678);
}

// ----------------------------------------------------------------------


if(dg2 != 0)
{   // this test return true

  cout << "dg2 is not empty" << endl;
}

if(dg2)
{   // this test return true

  cout << "dg2 is not empty" << endl;
}

if(!!dg2)
{ // this test return true

  cout << "dg2 is not empty" << endl;
}

if(!dg2.empty())
{ // this test return true

  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; }
  // 'call to empty delegate' exception

}

// ----------------------------------------------------------------------


CBase1 * pBase = 0;
// binding null callee object on purpose

dg3.bind(&CBase1::foo, pBase);
try
{
  FD_ASSERT( !dg3.empty() );
  dg3(890);
}
 // 'member function call on no object' exception

catch(std::exception & e) { cout << e.what() << endl; }

比较两个委托意味着比较内部存储的函数指针的内存地址,这并没有什么特别的意义。但是,使其成为可能,让我的委托可以在 STL 容器中无缝使用。由于它在“大多数”情况下是一个“快速”委托,我们不需要过分担心委托在 STL 容器内通过值语义进行复制时性能会下降。

const 正确性

CBase1 b1;
CBase1 const cb1;

// --------------------------------------------------
// argument binding


MyDelegate dg1(&CBase1::foo, &b1);
MyDelegate dg2(&CBase1::foo, &cb1);
MyDelegate dg3(&CBase1::bar, &b1);
// compile error! const member function can
// not be called on non-const object

// MyDelegate dg4(&CBase1::bar, &cb1);


dg1(123);
dg2(234);
dg3(345);

// --------------------------------------------------

// member function adapter


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);
// compile error! non-const member function
// can not be used for const member function adapter

// fd::delegate2<INT, int *, CBase1 const> dg7(&CBase1::bar);


dg4(&b1, 456);
// compile error! const object cannot be used
// non-const member function adapter

// dg4(&cb1, 456);

dg5(&b1, 567);
// compile error! const object cannot be used
// non-const member function adapter

// 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 支持

(备注)由于我对 gcc 的理解不足,目前调用约定支持仅在 MSVC 中有效。

类型检查松弛

传递给委托的模板参数类型检查非常严格,但这在现实生活中可能过于严格了。可能存在这样一种情况,我们希望将一堆 int (*)(int) 函数和 int (*)(long) 函数一起处理。当这些函数被赋值给 fd::delegate1 < int, int > 时,编译器会对 int (*)(long) 函数报错,称其不能被赋值,因为 'int' 和 'long' 是不同的类型。

通过在包含 "delegate.h" 之前定义 FD_TYPE_RELAXATION 宏,可以启用类型检查松弛。简而言之,只要满足以下三个条件,一个函数(自由函数、成员函数或仿函数)就可以被赋值或绑定到 fd::delegate

  1. 参数数量匹配,
  2. 每个匹配的参数都可以被编译器进行平凡转换(从委托的参数到目标函数的参数),
  3. 返回类型可以被编译器进行平凡转换(从委托的返回类型到目标函数的返回类型,“反之亦然”)。

如果以上任何条件无法满足,编译器将会报错(编译时警告和/或错误消息)。

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 需要满足这三个条件才能工作。

// compile warning! : 'return' : conversion from '' to '', possible loss of data

fd::delegate1 < float, long > dg2(&CBase1::bar, &b1);

等同于

float fd_delegate1_dg2(long l)
{
  // compile warning! : possible loss of data

  return b1.bar(l);
}

一个 'int' 返回类型到 'float' 返回类型可以无缝转换,但一个 'float' 返回类型到 'int' 返回类型会发出一个“可能丢失数据”的警告。

// compile error! : cannot convert parameter 3 from 'char *' to 'int'

fd::delegate1 < int, char * > dg3(&CBase1::foo, &b1);

等同于

int fd_delegate1_dg3(char * ch)
{
  // compile error! : cannot convert parameter 'ch' from 'char *' to '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 > 使用堆内存,这被认为是代价高昂且速度很慢的。为小对象使用固定大小块(chunk)内存分配器,可以比仅使用默认的 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;

  // ======================================================================
  //
  // class delegateN (Portable Syntax)
  //
  // ======================================================================


  template < typename R,typename T1,typename T2,...,typename TN,
       typename Alloocator = std::allocator < void > , 
                               size_t t_countof_pvoid = 2 >
  class delegateN;

  // ----------------------------------------------------------------------
  // default c'tor

  delegateN< R, T1, T2, ..., TN >::delegateN();

  // ----------------------------------------------------------------------
  // copy c'tor for 0

  delegateN< R, T1, T2, ..., TN >::delegateN(implClass::clear_type const *);

  // ----------------------------------------------------------------------


  // copy c'tor

  delegateN< R, T1, T2, ..., TN >::delegateN(delegateN< R, T1, 
                                      T2, ..., TN > const & other);

  // ----------------------------------------------------------------------
  // function copy c'tor

  delegateN< R, T1, T2, ..., TN >::delegateN(R (*fn)(T1, T2, ..., TN);

  // ----------------------------------------------------------------------


  // member function adapter copy c'tors

  //  ,where T1 can be trivially converted to either U * or U &

  delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T2, T3, ..., TN));

  //  ,where T1 can be trivially converted to one
  //         of  U * or U const * or U & or U const &

  delegateN< R, T1, T2, ..., TN >::delegateN(R (U::*mfn)(T2, T3, ..., TN) const);

  // ----------------------------------------------------------------------


  // member function argument binding copy 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);

  // ----------------------------------------------------------------------


  // functor copy 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*/);

  // ----------------------------------------------------------------------


  // assignment from 0

  delegateN< R, T1, T2, ..., TN > &
  delegateN< R, T1, T2, ..., TN >::operator = (implClass::clear_type const *);

  // ----------------------------------------------------------------------


  // assignment operator

  delegateN< R, T1, T2, ..., TN > &
  delegateN< R, T1, T2, ..., TN >::operator = 
      (delegateN< R, T1, T2, ..., TN > const & other);

  // ----------------------------------------------------------------------


  // function assignment operator

  delegateN< R, T1, T2, ..., TN > &
  delegateN< R, T1, T2, ..., TN >::operator = (R (*fn)(T1, T2, ..., TN);

  // ----------------------------------------------------------------------


  // member function adapter assignment operators


  //  ,where T1 can be trivially converted to either U * or U &

  delegateN< R, T1, T2, ..., TN > &
  delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T2, ..., TN));

  //  ,where T1 can be trivially converted to one
  //   of  U * or U const * or U & or U const &

  delegateN< R, T1, T2, ..., TN > &
  delegateN< R, T1, T2, ..., TN >::operator = (R (U::*mfn)(T2, ..., TN) const);

  // ----------------------------------------------------------------------


  // member function argument binding assignment operators

  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);

  // ----------------------------------------------------------------------


  // functor assignment operators

  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);

  // ----------------------------------------------------------------------


  // invocation operator

  result_type operator ()(T1 p1, T2 p2, ..., TN pN) const;

  // ----------------------------------------------------------------------


  // swap

  void delegateN< R, T1, T2, ..., TN >::swap(delegateN & other);

  // ----------------------------------------------------------------------


  // clear

  void delegateN< R, T1, T2, ..., TN >::clear();

  // ----------------------------------------------------------------------


  // empty

  bool delegateN< R, T1, T2, ..., TN >::empty() const;

  // ----------------------------------------------------------------------


  // comparison for 0

  bool operator == (implClass::clear_type const *) const;

  bool operator != (implClass::clear_type const *) const;

  // ----------------------------------------------------------------------


  // compare

  int compare(delegateN const & other, bool check_bound_object = false) const;

  // comparison operators

  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;

  // ======================================================================

  // class delegate (Preferred Syntax)

  // ======================================================================


  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 >;

  //
  // the same set of member functions as fd::delegateN of Portable Syntax
  //


  // ======================================================================
  // fd::make_delegate()
  // ======================================================================


  // make_delegate for function

  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 for member function adapter

  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 for member function argument binding

  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()

  // ======================================================================


  // bind for member function argument binding

  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
  {
    // ======================================================================

    // custom memory allocators (policy driven)

    // ======================================================================


    // fixed block memory allocator

    template< typename T >
    class fixed_allocator;

    // standard memory allocator ( equivalent to std::allocator< T > )

    template< typename T >
    class std_allocator;

  }  // namespace util


  // ----------------------------------------------------------------------


} // namespace fd

参考文献

  • [Hickey]. 在 C++ 中使用模板仿函数实现回调 - 总结了现有的回调方法及其弱点,然后描述了一种基于模板仿函数的灵活、强大且易于使用的回调技术。('1994)
  • [Peers]. C++ 中的回调 - 一篇基于 Rich Hickey 的文章,用以说明实现回调所用的概念和技术。
  • [Clugston]. 成员函数指针和最快的 C++ 委托 - 关于成员函数指针的综合教程,以及一种仅生成两个 ASM 操作码的委托实现!
  • [Ryazanov]. 快得不可思议的 C++ 委托 - 一种委托库的实现,其运行速度比“最快的 C++ 委托”还要快,并且完全兼容 C++ 标准。
  • [Trunov]. C++ 中又一种广义仿函数的实现 - 一篇关于 C++ 中广义仿函数实现的文章。考虑了广义仿函数的需求、现有实现的问题和缺点。提供了几个新思路和问题解决方案,以及完整的实现。
  • [boost]. “……最受推崇、设计最专业的 C++ 库之一。” boost::functionboost::bindboost::mem_fn

历史

  • 1.1 - 2006年3月12日。克隆绑定对象、智能指针支持、自定义分配器支持以及错误修复。
  • 1.0 - 2006年3月1日。初始发布。
© . All rights reserved.