Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C++
Article

Fast C++ Delegate

Rate me:
Please Sign up or sign in to vote.
4.80/5 (52 votes)
27 Mar 2006CPOL19 min read 211.1K   3.6K   110   31
An implementation of a fast C++ delegate which is portable and C++ Standard-compliant.

Introduction

Don Clugston explained about the member function pointer and its behavior in a very well organized way in his article: "Member Function Pointers and the Fastest Possible C++ Delegates". In short, member function pointers can not be assigned into a void * data pointer, so special care need to be taken to deal with a member function pointer as a data (functor). Don's FastestDelegate is the fastest possible delegate as he claims, but he leveraged reinterpret_cast<> to deceive the compiler, which he calls a 'horrible hack' by himself in his article.

Sometime later, Sergey Ryazanov came up with a fast C++ delegate which he found to be as fast as Don's FastestDelegate, but completely C++ Standard compliant. His technique is to use a templatized static member function ('stub') to store/restore the function call information (return type, argument type, cv-qualifier, and platform specific calling convention) at compile-time. Actually, it isn't new as a similar approach has already been introduced by Rich Hickey in his article "Callbacks in C++ Using Template Functors", 1994. Rich called such a static member function as a 'thunk' function. However, Sergey's technique is unique as he passes over member function call information through a non-type template parameter. Unfortunately, not many commercially available compilers can support this truly C++ Standard feature. Thus, his code is not really portable in that sense.

There is one more great article about how to implement C++ delegates, here in CodeProject. It is: "Yet Another Generalized Functors Implementation in C++", by Aleksei Trunov. He explains and analyzes so well in detail about generalized functor requirements and the problems of the existing delegates. His article inspired me to start implementing my own fast delegate, which will be explained here below.

By the way, these techniques are considered as 'fast' since they are able to avoid heap memory allocation to store member function pointers, unlike boost::function.

Another fast C++ delegate? So, what's new?

No, there is nothing new in my fast delegate. All the features that I will show in this article already exist in other's implementations.

  1. Fast delegate. (No heap memory allocation in 'most' cases.)
  2. Support three callable entities (free function, member function, and functor).
  3. C++ Standard-compliant, thus portable (tested in VC6, VC71, and DEV-C++ 4.9.9.2 (Mingw/gcc 3.4.2)).
  4. STL container compatible, copy-constructible, and assignable.
  5. Equality, less than, and greater than comparisons.
  6. Cv-qualifier (const) correctness.
  7. Supports platform specific calling conventions (__stdcall, __fastcall, __cdecl).
  8. Preferred syntax, portable syntax, or even mixed.
  9. Type-check relaxation.
  10. static_assert at compile-time.
  11. Either stores only the pointer (reference) to the bound object, or clones the bound object internally. (new)
  12. Stores the smart pointer to the bound object internally. (new)
  13. Custom memory allocator. (new)
  14. Customizes delegate's behavior/features by defining the relevant macros.

But, what if these features are all available in one? Doesn't that sound promising? Let's see!

Type restoring without heap memory allocation

Let's start off by revisiting Sergey's technique. It is only time the type information of the callee object is available when the member function pointer is getting assigned into the delegate; thus, some kind of mechanism is required to store the type info of the member function pointer into a non-typed universal form, and then to restore it whenever the delegate is being invoked later. Sergey uses a templatized static member function called 'method_stub' for this purpose, and also uses a member function pointer as a non type template parameter. These two techniques make it possible to avoid heap memory allocation. But, both of these truly C++ Standard-compliant features are accepted by the relatively new compilers only.

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

  }
};

From my own experience, I know that old compilers such as VC6 do not like to deal with templatized static member functions. At the same time, I also know that, in such a case, using a static member function of the nested template class will make those compilers happy. So, I changed it as shown below:

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

    }
  };
};

One problem solved, but still need some 'C++ Standard-compliant but portable' mechanism which can substitute the non-typed template parameter of the member function pointer. The TMethod should be stored as a non-typed form in the delegate class. But, as we have all learned from Don's great article, the size of the member function pointer varies according to the inheritance trait of the class that it belongs to as well as according to the compiler vendors. Therefore, dynamic memory allocation is 'inevitable' unless otherwise we decide to use a really huge sized buffer. (Don's analysis showed that the maximum size of the member function pointer is about 20 ~ 24 bytes, but I don't want to put a disclaimer saying that "this delegate class works only if the size of the member function pointer is less than or equal to 24 bytes".)

If you've read Rich's article "Callbacks in C++ Using Template Functors", you will realize that what I am doing here now is just a replication of what he did 10 years ago. But, after I read Aleksei's article "Yet Another Generalized Functors Implementation in C++ - An article on generalized functors implementation in C++", I realized that his meta meta-template technique to make the class behave differently according to the size of the member function pointer can be applied here and that I might be able to create something neat.

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

By using the If < > meta meta-template, it is possible to make the member function pointer be stored in the internal buffer, whose size can be determined at compile time, and is smaller than the size of the internal buffer; but, if the size of the member function pointer is found to be greater than the size of the internal buffer, we can still deal with it by allocating the heap memory to whatever size it is. And, the compiler will automatically decide it for you.

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

  };

};

Now, we have a 'C++ Standard-compliant and portable' substitution for Sergey's member function pointer as a non-typed template parameter. Two or three levels of indirection has been added, but the compiler with the descent inline optimization can opt it out and yield a code which is equivalent to Sergey's.

In addition to this, we now have a binary representation of the member function pointer as an internal data structure so that it becomes possible to compare it with others. In other words, we can use delegates in STL containers which require its element for comparisons capability. (See the demo project included.)

The size of the internal buffer can be customized by defining a proper macro before including the header file. I chose 8 bytes as the default size according to the size of the member function pointer table from Don's article. (I am mostly using MSVC, and has never used virtual inheritance, so 8 bytes is sufficient for myself, but again, you can customize the default buffer size.)

Object Cloning Manager Stub (New)

In the previous version, only the pointer (reference) to the bound object can be stored in the delegate for the member function pointer (argument binding) to be called on when the delegate is being invoked later. I decided to add the support for cloning the bound object in the delegate so that the member function pointer can be invoked on the internal copy of the bound object rather than on the pointer to the bound object.

Again, only when the type information of the member function pointer or its bound object is available is when the member function pointer is being bound to the delegate. Therefore, some sort of type retaining the 'stub' for object cloning and destruction is required. A similar static member function of the nested template class, as shown above, to be used for invoking the member function pointer can be applied here again.

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

An object can be cloned or destructed type safely using obj_clone_man_ptr_, as shown below:

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

The size of the bound object is unknown, and it can be as small as a few bytes to several hundred bytes, or even more. Thus, heap memory allocation/deallocation is inevitable. This conflicts against the design criteria of 'fast delegates' since the main purpose of using a fast delegate is to avoid the use of heap memory at all costs. (A custom memory allocator, which will be addressed later, might play a decent role to soothe this issue.)

Actually, I decided to introduce the cloning feature in my delegate for smart pointer support. Unlike in C#, we don't have a built-in garbage collector in C++, but we have smart pointers. In order to work with a smart pointer, it requires to be able to copy or destruct the smart pointer instance in a type safe manner (in other words, an appropriate assignment operator or destructor of the smart pointer must be called). We already have a 'stub' function to serve this purpose. But, there are still two more prerequisites / conditions to be fulfilled. (The idea is borrowed from boost::mem_fn.)

  1. A function, get_pointer(), which takes the reference or const reference to the smart pointer and retunes the pointer to the target object stored must be provided in the qualified namespace (including an argument-dependent lookup).
  2. The smart pointer class must expose a public interface (typedef) of element_type. (std::auto_ptr<T>, boost::shared_ptr, and its sibling, loki::smartPtr expose this public interface.)

The following two versions of get_pointer()s are implemented in my delegate, by default:

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_ptr defines get_pointer() for itself in the boost namespace. Thus, those compilers which implement the Koenig lookup (argument-dependent lookup), such as VC71 or higher, GCC3.x.x.x, will be able to see the definition so that it can be recognized and supported by my delegate without adding any extra line of code at all. But for those which don't implement the argument-lookup properly, nor doesn't have it at all, we can help them by providing the appropriate get_pointer() in the fd namespace.

#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

Using the code

Preferred syntax and Portable syntax

#include "delegate.h"
// Preferred Syntax

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

fd::delegate2 < void, int, char * > dg;

The Preferred syntax can be only accepted by the relatively newer C++ Standard-compliant compilers such as VC7.1 or higher, or GNU C++ 3.XX, while the Portable syntax is supposed to be accepted by most compilers (I assume that my fast delegate can be easily ported to some other compilers without any significant problems, as it is proven to work even in the notorious VC6). (Remark: I tested my delegate only in VC6, VC7.1, and DEV-C++ 4.9.9.2 (Mingw/gcc 3.4.2).)

When both Preferred syntax and Portable syntax are supported, it is just fine to use both syntaxes mixed (copy, comparison, copy-construction, assignment, and so on). Therefore, all the example code snippets hereafter will be demonstrated in Portable syntax.

Wrapping three callable entities

A callable entity has a function call operator (), and three callable entities are:

  • Free functions (including static member functions),
  • Member functions, and
  • Functors (Function objects).

These three callable entities can be assigned to fd::delegate in a very similar manner as boost::function, except functor.

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

// 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*/ }
};

Free functions

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

Member functions (Member function adapter)

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

Probably, this might not be what you wanted to achieve. You might want to declare a delegate more like: fd::delegate1 < void, int >, rather than: fd::delegate2 < void, CBase1 *, int >. If so, it is called 'argument binding' for a member function, and will be addressed later.

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

It is very interesting that the member function can be adapted and invoked as shown above. While I was using boost::function in this way, I almost had an illusion that a raw member function pointer can be called also in the same way, and actually, I even tried (and the compiler complained to me :P). It is a special provision, and it involves lots of internal coding to cast such an illusion. I will call it a 'member function adapter'.

Functors (Function objects) (Updated)

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!

I didn't consider including functor support in the beginning. When I changed the plan (even after I completed a tedious and boring code duplication for a calling convention), I tried to implement the normal assignment operator (operator =) for the functor, but it caused me so many overloaded function ambiguity issues. So, I almost gave up this support and planned to enforce the user to implement it, something like:

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

Silly me! I hope you will be happy with operator <<= instead of the above.

A delegate can't be a delegate without something for which it represents. That is, the wrapped target callable entity must be in a valid state when the delegate is being invoked. This behavior is somewhat different from how boost::function is assigned from a functor. By default, boost::function clones the target functor internally (heap memory allocation) unless otherwise boost::ref or boost::cref is explicitly used. In the previous version, my delegate only stores the reference (pointer) to the target functor assigned. So, if it is a stateful functor, the caller is responsible to keep the target functor intact to be called (the exact same idea is applied to the callee object bound for the member function later).

But in the new version, I added a cloning bound object feature, therefore the syntax of operator <<= has been changed to distinguish the reference (pointer) storing version and the cloning version. Also, a special copy-constructor which accepts the dummy bool as the second argument for the functor is shown above.

Member function argument binding (Updated)

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

A member function pointer is required to be called on the callee object of the same type. The callee object is bound as a reference (pointer) so that it should be in a valid state when the delegate is being invoked. It is the caller's responsibility to make the callee object intact to be called.

In the new version, the bound object can be cloned internally, or even a smart pointer can be bound for automatic memory management.

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

The fd::bind() helper function is copied from Jody Hagins' idea/contribution for Don's FastestDelegate. It allows easy code migration from boost::function and 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 );

The above can be easily converted to:

#include "delegate.h"

using fd::delegate1;
using fd::bind;

CBase1 b1;

delegate1 < void, int > fn = bind( &CBase1::foo, &b1, _1 );

But, be aware that the placeholder _1 here does not perform like boost::_1. It is nothing but a placeholder.

fd::make_delegate() helper function

It can be useful when passing a delegate as a function parameter.

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

But the member function adapter version of fd::make_delegate() needs to be treated as different from the other version of fd::make_delegate(), and there is reason for it.

The CBase1::virtual_not_overridden member function in this example is a public member function, and the derived class didn't override it. Since it is a public member function, it is just fine to refer the member function pointer as a notation of 'CDerived1::virtual_not_overridden'. But, when this notation of the member function pointer is passed over to some automatic template deduction function such as fd::make_delegate() as an argument, the template type automatically deduced is surprisingly 'CBase1::virtual_not_overridden', not 'CDerived1::virtual_not_overridden'. Therefore, the delegate created from fd::make_delegate() will become the fd::delegate2 < void, CBase1 *, int > type, while what we wanted was the fd::delegate2 < void, CDerived1 *, int > type. This is why the typed null pointer is required to be passed over explicitly as the first argument of the make_delegate() helper function, in this case. A similar concept is used in type-check relaxation later.

Comparisons and Miscellaneous

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

Comparing two delegates means comparing the memory address of the function pointer stored internally, and it does not really mean anything special. But, making it possible allowed my delegate to be used in an STL container seamlessly. As it is a 'fast' delegate in 'most' cases, we don't need to worry too much about performance degrade while the delegate is being copied inside an STL container by the value semantic.

const correctness

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

Platform specific calling convention

Calling convention is not a C++ standard feature, but it can't be just ignored as the Win32 API and the COM API use it. From an implementation point of view, it is just a matter of boring and tedious replication of the same code. By default, none of the platform specific calling conventions are enabled. To enable it, the relevant macro needs to be defined before "delegate.h" is included.

  • FD_MEM_FN_ENABLE_STDCALL - to enable __stdcall support for member function
  • FD_MEM_FN_ENABLE_FASTCALL - to enable __fastcall support for member function
  • FD_MEM_FN_ENABLE_CDECL - to enable __cdecl support for member function
  • FD_FN_ENABLE_STDCALL - to enable __stdcall support for free function
  • FD_FN_ENABLE_FASTCALL - to enable __fastcall support for free function
  • FD_FN_ENABLE_PASCAL - to enable Pascal support for free function

(Remark) Calling convention support only works in MSVC at the time, due to the lack of my understanding of gcc.

Type-check relaxation

Template parameter types passed over to a delegate are very strictly checked, but this could be too much in real life. There might be a situation where we want to treat a bunch of int (*)(int) functions and int (*)(long) functions together. When these functions are assigned into fd::delegate1 < int, int >, the compiler will emit errors for the int (*)(long) functions saying that it cannot be assigned since 'int' and 'long' are different types.

By defining the FD_TYPE_RELAXATION macro before including "delegate.h", type-check relaxation can be enabled. In a nutshell, a function (free function, member function, or functor) can be assigned or bound to fd::delegate whenever the following three conditions are met:

  1. the number of arguments matches,
  2. each matching argument can be trivially converted (from the delegate's argument to the target function's argument) by the compiler,
  3. the return type can be trivially converted (from the delegate's return type to the target function's return type "and vice versa") by the compiler.

If any of the above conditions can not be met, the compiler will complain about it (compile-time warning and/or error messages).

CBase1 b1;
//
// int CBase1::foo(int) const;
// int CBase1::bar(int);
//

fd::delegate1 < int, long > dg1(&CBase1::foo, &b1);

dg1(123);

The above delegate definition is theoretically equivalent to the following function definition:

CBase1 b1;
int fd_delegate1_dg1(long l)
{
  return b1.foo(l);
}

fd_delegate1_dg1(123);

This shows why the three conditions are required to be fulfilled for fd::delegate to work in the type-check relaxation mode.

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

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

is equivalent to:

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

  return b1.bar(l);
}

An 'int' return type to 'float' return type can be converted seamlessly, but a 'float' return type to 'int' return type will issue 'a possible loss of data' warning.

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

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

is equivalent to:

int fd_delegate1_dg3(char * ch)
{
  // compile error! : cannot convert parameter 'ch' from 'char *' to 'int'

  return b1.foo(ch);
}

and the compiler will issue an error since 'char *' can not be trivially converted into 'int'.

CDerived1 d1("d1");
//

// class CDerived1 : public CBase1 { };

//

fd::delegate2 < int, CDerived1 *, long > dg5(&CBase1::bar);

is equivalent to:

int fd_delegate2_dg5(CDerived1 * pd1, long l)
{
  return pd1->bar(l);
}

and upcasting from 'CDerived1 *' to 'CBase1 *' is always safe, thus can be converted trivially.

fd::make_delegate() for type-check relaxation mode (Removed)

[Obsolete] When FD_TYPE_RELAXATION is defined, sets of fd::make_delegate() are enabled to support this mode. Since fd::make_delegate() can not guess at all what type of delegate is required to be created, the caller must specify the delegate type null pointer as the first argument of fd::make_delegate(). This is exactly the same concept used in the member function adapter version of fd::make_delegate() explained earlier. [Obsolete]

The purpose of using make_delegate() is automatic template parameter deduction, so there is no reason to use make_delegate if the additional type information is compulsorily provided as the first argument. Since this feature even causes a big confusion to poor compilers such as VC6, it is removed in the new version.

static_assert (Debugging support)

When a delegate is assigned or bound to a function pointer, the compiler generates the appropriate function call operator () at compile-time. If there is a type mismatch warning or error, it is really difficult to track down where those originate from. A smart compiler like VC7.1 has the nice capability of tracing those warnings or errors with detailed template type information up to the user source code, but VC6 doesn't. (VC6 usually gives two levels of traces.) So, I tried to place a static_assert (FD_STATIC_ASSERT, FD_PARAM_TYPE_CHK) in as many places as possible so that it can be easier to track down the origin of warnings/errors in the user source code.

Custom memory allocator support (New)

In the new version, my delegate can use services from any custom memory allocator when it requires to allocate or deallocate memory either for storing the member function pointer whose size is greater than the internal buffer size or for storing the cloned bound object. std::allocator< void > uses heap memory, which is known to be very expensive and very slow. Using fixed size block (chunk) memory allocator for small objects can increase the performance in a quite big time than when just using the default std::allocator< void >. Of course, the degree of benefit from using a custom allocator will vary according to the implementation detail of the custom allocator employed.

I included a fd::util::fixed_allocator which allocates a big chunk of memory at once for small objects' later usage. It is implemented based on several articles that can be found in CodeProject. You can provide any custom memory allocator of your favor.

Final words

If the speed is your only concern, define FD_DISABLE_CLONE_BOUND_OBJECT (extra four bytes space per delegate will be saved as bonus), and store the pointer to the bound object version of member functions only; otherwise, you can use a smart pointer to the bound object and the custom memory allocator to tweak the performance by balancing between speed and safety. The delegate's behaviror and features are fully customizable by defining the proper macros (look at the "config.hpp" file).

I also included a guide on how to extract a simplified version from the full version for those who want to see the implementation details after the macro expansion.

Definition

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

References

  • [Hickey]. Callbacks in C++ Using Template Functors - summarizes existing callback methods and their weaknesses, then describes a flexible, powerful, and easy-to-use callback technique based on template functors. ('1994)
  • [Peers]. Callbacks in C++ - An article based on Rich Hickey's article to illustrate the concept and techniques used to implement callbacks.
  • [Clugston]. Member Function Pointers and the Fastest Possible C++ Delegates - A comprehensive tutorial on member function pointers, and an implementation of delegates that generates only two ASM opcodes!
  • [Ryazanov]. The Impossibly Fast C++ Delegates - An implementation of a delegate library which can work faster than "The Fastest Possible C++ Delegates", and is completely compatible with the C++ Standard.
  • [Trunov]. Yet Another Generalized Functors Implementation in C++ - An article on a generalized functors implementation in C++. Generalized functor requirements, existing implementation problems, and disadvantages are considered. Several new ideas and problem solutions, together with complete implementation are provided.
  • [boost]. "...One of the most highly regarded and expertly designed C++ libraries." boost::function, boost::bind, boost::mem_fn.

History

  • 1.1 - March 12, 2006. Cloning bound object, smart pointer support, custom allocator support, and bug fixes.
  • 1.0 - March 01, 2006. Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Other
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Paltr28-May-13 1:40
Paltr28-May-13 1:40 
GeneralNew version link Pin
JaeWook Choi13-Apr-07 4:32
JaeWook Choi13-Apr-07 4:32 
GeneralRe: New version link Pin
Vince Ricci5-Jan-15 21:59
Vince Ricci5-Jan-15 21:59 
GeneralRe: New version link Pin
cas4ey30-Jun-15 6:25
cas4ey30-Jun-15 6:25 
GeneralBinding arguments to the delegate. Pin
raindog15-Nov-06 12:56
raindog15-Nov-06 12:56 
GeneralRe: Binding arguments to the delegate. Pin
JaeWook Choi16-Nov-06 20:08
JaeWook Choi16-Nov-06 20:08 
QuestionBind - Why not? Pin
Ted Rossam14-Oct-06 13:22
Ted Rossam14-Oct-06 13:22 
AnswerRe: Bind - Why not? Pin
JaeWook Choi15-Oct-06 6:39
JaeWook Choi15-Oct-06 6:39 
GeneralVC8 Errors - Not present compiled as VC7.1 Pin
Stone Free13-Oct-06 5:29
Stone Free13-Oct-06 5:29 
GeneralRe: VC8 Errors - Not present compiled as VC7.1 Pin
JaeWook Choi13-Oct-06 9:52
JaeWook Choi13-Oct-06 9:52 
GeneralRe: VC8 Errors - Not present compiled as VC7.1 Pin
Stone Free15-Oct-06 23:14
Stone Free15-Oct-06 23:14 
GeneralRe: VC8 Errors - Not present compiled as VC7.1 Pin
JaeWook Choi16-Oct-06 6:46
JaeWook Choi16-Oct-06 6:46 
Generali prefer don's solution Pin
masterminder23-Jun-06 8:32
masterminder23-Jun-06 8:32 
GeneralRe: i prefer don's solution Pin
JaeWook Choi13-Oct-06 10:06
JaeWook Choi13-Oct-06 10:06 
GeneralRe: i prefer don's solution Pin
raindog10-Nov-06 7:48
raindog10-Nov-06 7:48 
GeneralSome corrections Pin
stephen_adrian_hill15-Mar-06 11:04
stephen_adrian_hill15-Mar-06 11:04 
GeneralRe: Some corrections Pin
Stephen Adrian Hill15-Mar-06 23:41
Stephen Adrian Hill15-Mar-06 23:41 
GeneralRe: Some corrections Pin
JaeWook Choi16-Mar-06 4:30
JaeWook Choi16-Mar-06 4:30 
GeneralRe: Some corrections Pin
Stephen Adrian Hill16-Mar-06 5:03
Stephen Adrian Hill16-Mar-06 5:03 
GeneralRe: Some corrections Pin
JaeWook Choi16-Mar-06 7:17
JaeWook Choi16-Mar-06 7:17 
GeneralRe: Some corrections Pin
Stephen Adrian Hill16-Mar-06 21:28
Stephen Adrian Hill16-Mar-06 21:28 
GeneralThumbs up! Pin
Ivo Ivanov7-Mar-06 10:16
Ivo Ivanov7-Mar-06 10:16 
GeneralRe: Thumbs up! Pin
JaeWook Choi8-Mar-06 11:08
JaeWook Choi8-Mar-06 11:08 
Generalformatting Pin
Emilio Garavaglia2-Mar-06 23:26
Emilio Garavaglia2-Mar-06 23:26 
AnswerRe: formatting Pin
JaeWook Choi3-Mar-06 2:58
JaeWook Choi3-Mar-06 2:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.