9

In OO, one usually implements callbacks with interfaces: (rough example)

class Message {}

class IMsgProcessor {
public:
     virtual void handle_msg(const Message& msg) = 0;
}

class RequestMsgProcessor : public IMsgProcessor {
     virtual void handle_msg(const Message& msg)  {
     // process request message
    }
}

class CustomSocket {
public:
   Socket(IMsgProcessor* p) : processor_(p) {}

   void receive_message_from_network(const Message& msg) {
       // processor_ does implement handle_msg. Otherwise a compile time error. 
       // So we've got a safe design.
       processor_->handle_msg(msg);
   }
private:
   IMsgProcessor* processor_;
}

So far so good. With C++11, another way to do this is to have CustomSocket just receive an instance of std::function object. It does not care where it is implemented or even if the object is a non-null value :

class CustomSocket {
public:
   Socket(std::function<void(const Message&)>&& f) : func_(std:forward(f)) {}

   void receive_message_from_network(const Message& msg) {
       // unfortunately we have to do this check for every msg.
       // or maybe not ...
       if(func_)
            func_(msg);
   }
private:
   std::function<void(const Message&)> func_;
}

Now here are the questions:
1. What about the performance impacts? I'm guessing a virtual function call is faster than calling a function object but how much faster? I'm implementing a fast messaging system and I'd rather avoid any unnecessary performance penalty.
2. In terms of software engineering practices, I have to say I like the second approach better. Less code, fewer files, less clutter: no interface class. More flexibility: you can only implement a subset of the interface by setting some of the function objects and leaving the others null. Or you can have different parts of the interface implemented in separate classes or by free functions or combination of both (instead of in a single subclass). Furthermore, CustomSocket can be used by any class not just subclasses of IMsgProcessor. This is a great advantage, in my opinion.
What do you say? Do you see any fundamental flaw in these argument ?

PoP
  • 2,055
  • 3
  • 16
  • 20
  • `// unfortunately we have to do this check for every msg.` -- the same thing should be true for the interface code. You never check if you don't get passed a null pointer... – Xeo Nov 29 '12 at 14:54
  • About perfs: virtual func basically adds a table lookup, std::function forward the calls to a specialized adapter. If your function/method is not virtual it may be optimized to be as fast. Have to benchmark. If you replace `std::function` with a template argument you can make it faster than virtual call. – Antoine Nov 29 '12 at 14:59
  • @Xeo Not necessarily: in the interface code I'd just check in constructor that the passed object is not NULL--accepting a NULL does not make sense. But in the second case because you can have partial implementation of the interface (i.e., only some function objects maybe non-null) you need the check. – PoP Nov 29 '12 at 15:06
  • 1
    The constructor accepting a `function&&` is not doing what you think it does. It will only accept `rvalues` and you probably don't want that. It often does not make sense to explicitly specify `function`. Take a generic Functor as member. Your users then can decide if they need the polymorphic behavior of `std::function`. – pmr Nov 29 '12 at 15:10
  • You could make the ctor throw if the function is invalid. – Pete Nov 29 '12 at 15:21
  • Interface mechanism is likely to be slightly faster unless the compiler / library does some clever optimizations. Another option is to use the fastdelegate library. – Pete Nov 29 '12 at 15:22
  • This question is primarily opinion-based and should not have been reopened. – TylerH Feb 14 '17 at 16:01
  • @TylerH: The question was closed based on being "too broad". It is asking "between A and B which one works better in terms of software engineering and performance?" I have a hard time imagining how it could it be phrased to be more specific. Of course, the answer to _any_ software engineering question can be prone to opinion biases. Getting people's different perspectives was the very intent of this question. So please do not close this as too broad or opinion-based without solid argument. – PoP Feb 15 '17 at 01:00
  • @PoP The solid argument is that it *is* opinion-based. Such questions are defined by the system as off-topic, because Stack Overflow isn't the place for such discussions. Where it's better and under what conditions is largely dependent upon factors that are always specific to the situation and the users' preferences. That makes it too broad and opinion-based. It is a worthwhile question, perhaps, but on SoftwareEngineering.StackExchange (formerly Programmers), not on Stack Overflow. – TylerH Feb 15 '17 at 14:27
  • @TylerH Alright; I agree that this question would be a better fit for SoftwareEngineering.StackExchange rather than Stack Overflow. If this counts as a basis for closing this question then I don't have objections to closing it. – PoP Feb 15 '17 at 18:10

3 Answers3

3

You can have the best of both worlds

template<class F>
class MsgProcessorT:public IMsgProcessor{
  F f_;
  public:
  MsgProcessorT(F f):f_(f){}
  virtual void handle_msg(const Message& msg)  {
      f_(msg);
 }

};
template<class F>
IMsgProcessor* CreateMessageProcessor(F f){
    return new MsgProcessor<T>(f);

};

Then you can either use like this

Socket s(CreateMessageProcessor([](const Message& msg){...}));

Or to make it even easier add another constructor to Socket

class Socket{
...
template<class F>
Socket(F f):processor_(CreateMessageProcessor(f){}


};

Then you could do

Socket s([](const Message& msg){...});

And still have the same efficiency as a virtual function call

John Bandela
  • 2,416
  • 12
  • 19
  • 4
    Ew, naked `new`! Cover your shame! ... On a more serious note, `std::function` internally uses either builtin virtual dispatch or a manual implementation with function pointers, `void*` and function templates. – Xeo Nov 29 '12 at 15:09
  • @Xeo You are correct. I would make Socket non-copyable and use unique_ptr for processor_ and return a unique_ptr from CreateMessageProcessor. – John Bandela Nov 29 '12 at 16:33
  • @Xeo : Haha, that cracked me up. :-] – ildjarn Nov 29 '12 at 19:33
2

The interface approach is more traditional but also more verbose: it is clear what a MessageProcessor does and you don't have to check it. Moreover, you can re-use one and the same object with many Sockets.

The std::function approach is more general: anything that accepts the operator()(Message const&) can be used. However, the lack of verbosity has the danger of making the code less readable.

I don't know about performance penalties, but would be surprised if there are significant differences.

I would stick to the interface approach if this is an essential part of your code (as it seems to be).

Walter
  • 44,150
  • 20
  • 113
  • 196
1

In both of your examples you're actually using interfaces. What's different is the way you define them. In first case the interface is a traditional class with pure virtual functions and in second case the interface is a function reference - it's note much different from a C function pointer from design perspective. In my opinion you can mix both variations according to the specific requirements and consider the pros and cons (which are like you stated) for each new case. Regarding the performance impact, I think the best answer would be to perform testing, compare results and match to your performance requirements.

SomeWittyUsername
  • 18,025
  • 3
  • 42
  • 85