views:

426

answers:

7

When implementing polymorphic behavior in C++ one can either use a pure virtual method or one can use function pointers (or functors). For example an asynchronous callback can be implemented by:

Approach 1

class Callback
{
public:
    Callback();
    ~Callback();
    void go();
protected:
    virtual void doGo() = 0;  
};

//Constructor and Destructor

void Callback::go()
{
   doGo();
}

So to use the callback here, you would need to override the doGo() method to call whatever function you want

Approach 2

typedef void (CallbackFunction*)(void*)

class Callback
{
public:
    Callback(CallbackFunction* func, void* param);
    ~Callback();
    void go();
private:
   CallbackFunction* iFunc;
   void* iParam;
};

Callback::Callback(CallbackFunction* func, void* param) :
    iFunc(func),
    iParam(param)
{}

//Destructor

void go()
{
    (*iFunc)(iParam);
}

To use the callback method here you will need to create a function pointer to be called by the Callback object.

Approach 3

[This was added to the question by me (Andreas); it wasn't written by the original poster]

template <typename T>
class Callback
{
public:
    Callback() {}
    ~Callback() {}
    void go() {
     T t; t();
    }
};

class CallbackTest
{
public:
    void operator()() { cout << "Test"; }
};

int main()
{
    Callback<CallbackTest> test;

    test.go();
}

What are the advantages and disadvantages of each implementation?

A: 

Approach 1 (Virtual Function)

  • "+" The "correct way to do it in C++
  • "-" A new class must be created per callback
  • "-" Performance-wise an additional dereference through VF-Table compared to Function Pointer. Two indirect references compared to Functor solution.

Approach 2 (Class with Function Pointer)

  • "+" Can wrap a C-style function for C++ Callback Class
  • "+" Callback function can be changed after callback object is created
  • "-" Requires an indirect call. May be slower than functor method for callbacks that can be statically computed at compile-time.

Approach 3 (Class calling T functor)

  • "+" Possibly the fastest way to do it. No indirect call overhead and may be inlined completely.
  • "-" Requires an additional Functor class to be defined.
  • "-" Requires that callback is statically declared at compile-time.


FWIW, Function Pointers are not the same as Functors. Functors (in C++) are classes that are used to provide a function call which is typically operator().

Here is an example functor as well as a template function which utilizes a functor argument:

class TFunctor
{
public:
    void operator()(const char *charstring)
    {
     printf(charstring);
    }
};

template<class T> void CallFunctor(T& functor_arg,const char *charstring)
{
    functor_arg(charstring);
};

int main()
{
    TFunctor foo;
    CallFunctor(foo,"hello world\n");
}

From a performance perspective, Virtual functions and Function Pointers both result in an indirect function call (i.e. through a register) although virtual functions require an additional load of the VFTABLE pointer prior to loading the function pointer. Using Functors (with a non-virtual call) as a callback are the highest performing method to use a parameter to template functions because they can be inlined and even if not inlined, do not generate an indirect call.

Adisak
But in my approach 2, can they be used interchangeably?
doron
Technically, all three classes could loosely be considered functors if you take go() to be the class provided function instead of operator(). The function pointer itself in #2 has nothing to do with functors. However, only Approach 3 follows the "spirit" of a real C++ functor and can benefit from the template optimizations available to what is generally considered a functor for use in C++.
Adisak
+3  A: 

Approach 1

  • Easier to read and understand
  • Less possibility of errors (iFunc cannot be NULL, you're not using a void *iParam, etc
  • C++ programmers will tell you that this is the "right" way to do it in C++

Approach 2

  • Slightly less typing to do
  • VERY slightly faster (calling a virtual method has some overhead, usually the same of two simple arithmetic operations.. So it most likely won't matter)
  • That's how you would do it in C

Approach 3

Probably the best way to do it when possible. It will have the best performance, it will be type safe, and it's easy to understand (it's the method used by the STL).

Andreas Bonini
Virtual functions can be over 40x slower when they prevent inlining of a small function, removal of duplicate or impossible branches and/or the use of register-only variable storage.
Zan Lynx
@Zan: #1 prevents inline optimizations as well and that's what I was comparing it too. Now I added option #3 as well anyway
Andreas Bonini
There are two reasons why approach 2 is a "right" way to do this in C++1. C++ introduces functors which in a sense extend the idea of function pointers.2. The Boost thread library take approach 2 when implementing creating a new thread.
doron
Non of which applies to the call through a function pointer either.
struppi
I doubt aproach 2 is faster. AS the compiler is doing exactly the same with virtual methods. Virtual methods have an overhead in looking up the function to call. Approach 2 has the same overhead (it looks up the variabel to call in the pointer).
Martin York
Approach 3 is not the best way to do it. As you need to break encapsulation of your class to allow arbitory objects accesses to the data.
Martin York
@deus-ex-machina399, Qt thread library, however, prefers approach 1.
Pavel Shved
@Zan: Method 2 also prevents inlining. As it is basically user implemented version of virtual functions.
Martin York
@Martin York: #2 is faster because #1 requires everything #2 requires PLUS an arithmetic addition to find the pointer address in the vtable.
Andreas Bonini
@Andreas, it's not always one arithmetic addition; I even wrote an "article" about virtual function calls: http://coldattic.info/shvedsky/pro/blogs/a-foo-walks-into-a-bar/posts/3
Pavel Shved
@Andreas: Requires everything: This is meaningless. Finding the address of a method in the vtable is just as expensive as finding the address of a method in the object.
Martin York
A: 

Function pointers are more C-style I would say. Mainly because in order to use them you usually must define a flat function with the same exact signature as your pointer definition.

When I write C++ the only flat function I write is int main(). Everything else is a class object. Out of the two choices I would choose to define an class and override your virtual, but if all you want is to notify some code that some action happened in your class, neither of these choices would be the best solution.

I am unaware of your exact situation but you might want to peruse design patterns

I would suggest the observer pattern. It is what I use when I need to monitor a class or wait for some sort of notification.

Charles
Also what Adisak said about functors is a good idea, though I have not used them much
Charles
A: 

One major advantage of the first method is it has more type safety. The second method uses a void * for iParam so the compiler will not be able to diagnose type problems.

A minor advantage of the second method is that it would be less work to integrate with C. But if you're code base is only C++, this advantage is moot.

R Samuel Klatchko
+3  A: 

It's not clear from your example if you're creating a utility class or not. Is you Callback class intended to implement a closure or a more substantial object that you just didn't flesh out?

The first form:

  • Is easier to read and understand,
  • Is far easier to extend: try adding methods pause, resume and stop.
  • Is better at handling encapsulation (presuming doGo is defined in the class).
  • Is probably a better abstraction, so easier to maintain.

The second form:

  • Can be used with different methods for doGo, so it's more than just polymorphic.
  • Could allow (with additional methods) changing the doGo method at run-time, allowing the instances of the object to mutate their functionality after creation.

Ultimately, IMO, the first form is better for all normal cases. The second has some interesting capabilities, though -- but not ones you'll need often.

NVRAM
A: 

For example, let us look at an interface for adding read functionality to a class:

struct Read_Via_Inheritance
{
   virtual void  read_members(void) = 0;
};

Any time I want to add another source of reading, I have to inherit from the class and add a specific method:

struct Read_Inherited_From_Cin
  : public Read_Via_Inheritance
{
  void read_members(void)
  {
    cin >> member;
  }
};

If I want to read from a file, database, or USB, this requires 3 more separate classes. The combinations start to be come very ugly with multiple objects and multiple sources.

If I use a functor, which happens to resemble the Visitor design pattern:

struct Reader_Visitor_Interface
{
  virtual void read(unsigned int& member) = 0;
  virtual void read(std::string& member) = 0;
};

struct Read_Client
{
   void read_members(Reader_Interface & reader)
   {
     reader.read(x);
     reader.read(text);
     return;
   }
   unsigned int x;
   std::string& text;
};

With the above foundation, objects can read from different sources just by supplying different readers to the read_members method:

struct Read_From_Cin
  : Reader_Visitor_Interface
{
  void read(unsigned int& value)
  {
     cin>>value;
  }
  void read(std::string& value)
  {
     getline(cin, value);
  }
};

I don't have to change any of the object's code (a good thing because it is already working). I can also apply the reader to other objects.

Generally, I use inheritance when I am performing generic programming. For example, if I have a Field class, then I can create Field_Boolean, Field_Text and Field_Integer. In can put pointers to their instances into a vector<Field *> and call it a record. The record can perform generic operations on the fields, and doesn't care or know what kind of a field is processed.

Thomas Matthews
A: 
  1. Change to pure virtual, first off. Then inline it. That should negate any method overhead call at all, so long as inlining doesn't fail (and it won't if you force it).
  2. May as well use C, because this is the only real useful major feature of C++ compared to C. You will always call method and it can't be inlined, so it will be less efficient.
Charles Eli Cheese