Virtual Methods or Function Pointers
Asked Answered
B

8

33

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?

Bowman answered 23/12, 2009 at 20:17 Comment(4)
Or go() could be a template function that can take either a function pointer or a class with overridden operator()(void*).Bohaty
Approach 2 is what the compiler is doing behind the senes to implement Approach 1. You should not be doing it yourself because it is dangerious and error prone.Dupion
Cool, thanks Andreas for approach 3Bowman
I dont get approach 3..Its just a function. Whats the usefullness of a "callback" that will call always the same method? you cant pass it to a system providing different functionality..Arsenide
T
14

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.

Topliffe answered 23/12, 2009 at 20:26 Comment(2)
But in my approach 2, can they be used interchangeably?Bowman
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++.Topliffe
K
7

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

Kimsey answered 23/12, 2009 at 20:26 Comment(11)
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.Roborant
@Zan: #1 prevents inline optimizations as well and that's what I was comparing it too. Now I added option #3 as well anywayKimsey
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.Bowman
Non of which applies to the call through a function pointer either.Cranwell
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).Dupion
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.Dupion
@deus-ex-machina399, Qt thread library, however, prefers approach 1.Pod
@Zan: Method 2 also prevents inlining. As it is basically user implemented version of virtual functions.Dupion
@Martin York: #2 is faster because #1 requires everything #2 requires PLUS an arithmetic addition to find the pointer address in the vtable.Kimsey
@Andreas, it's not always one arithmetic addition; I even wrote an "article" about virtual function calls: coldattic.info/shvedsky/pro/blogs/a-foo-walks-into-a-bar/posts/…Pod
@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.Dupion
T
5

The primary problem with Approach 2 is that it simply doesn't scale. Consider the equivalent for 100 functions:

class MahClass {
    // 100 pointers of various types
public:
    MahClass() { // set all 100 pointers }
    MahClass(const MahClass& other) {
        // copy all 100 function pointers
    }
};

The size of MahClass has ballooned, and the time to construct it has also significantly increased. Virtual functions, however, are O(1) increase in the size of the class and the time to construct it- not to mention that you, the user, must write all the callbacks for all the derived classes manually which adjust the pointer to become a pointer to derived, and must specify function pointer types and what a mess. Not to mention the idea that you might forget one, or set it to NULL or something equally stupid but totally going to happen because you're writing 30 classes this way and violating DRY like a parasitic wasp violates a caterpillar.

Approach 3 is only usable when the desired callback is statically knowable.

This leaves Approach 1 as the only usable approach when dynamic method invocation is required.

Theorize answered 22/6, 2012 at 18:0 Comment(1)
If copying function pointer becomes an issue, the function pointer could be abstracted into a separate static table which is how v-tables typically operate.Bowman
M
3

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.

Mouflon answered 23/12, 2009 at 20:39 Comment(0)
M
1

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.

Metalinguistic answered 23/12, 2009 at 20:36 Comment(0)
T
0

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.

Terenceterencio answered 23/12, 2009 at 20:28 Comment(1)
Also what Adisak said about functors is a good idea, though I have not used them muchTerenceterencio
K
0

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.

Kegler answered 23/12, 2009 at 20:41 Comment(0)
I
0
  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.
Izawa answered 23/12, 2009 at 22:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.