Generalized Callbacks: C++ and C#

Callbacks implement dynamic selection and invocation of functions at run time. Bill examines two technologies that implement callbacks—the libsigc++ class library in C++ and the delegate feature in C#.


March 01, 2003
URL:http://www.drdobbs.com/cpp/generalized-callbacks-c-and-c/184405295

Mar03: Generalized Callbacks: C++ and C#

Bill holds a Ph.D. in Physics from the University of Illinois and is currently a developer for Proximation LLC. He can be contacted at bill @kiboko.com.


Callbacks are techniques for implementing dynamic selection and invocation of functions at run time. In C, the address of a callback function can be stored in a variable for invocation at some later time; see Listing One. A number of packages build on this concept to implement generalized callback mechanisms. These systems let multiple callback targets be bound to abstracted callback containers (acting as events or signals), and generalize the type of callback targets that can be bound to include functions, static methods, nonstatic methods, and more. The techniques used to implement these features vary from package to package and language to language, giving you a choice between systems with different feature sets, performance characteristics, and interfaces.

In this article, I examine two technologies that implement generalized, extensible callback mechanisms in C++ and C#. In doing so, I compare the libsigc++ class library in C++ to the delegate feature in C#, focusing on the different features and interfaces they provide. The libsigc++ library is available under the LGPL license from http://libsigc.sourceforge.net/, while implementations of C# are available from Microsoft (http://msdn.microsoft.com/) or the Mono project (http://www.go-mono.com/). I've tested the sample libsigc++ Version 1.2 code presented here with the Gnu C++ compiler 2.95.2, and the C# delegate code with Microsoft Visual Studio .NET using the Microsoft C# 1.0 compiler.

While I focus on libsigc++ and C# delegates, there are several C++ packages that support generalized callbacks using techniques or features similar to those described here, including the Boost Signals library (http://www.boost.org/).

Basic Libsigc++ and C# Callbacks

With callback techniques, you register references to a callable target entity with some callback container at run time, so that the target can be invoked at later times and with varying arguments. In languages with object-oriented support (C++, C#, and Java, for instance), this pattern can be implemented using polymorphic entities, like that for C++ in Listing Two. Polymorphic designs use abstract interfaces that define virtual functions implemented by derived classes. The virtual functions and polymorphic class instances represent the callback targets while the abstract interfaces represent the items to which callback containers store references and use to invoke the callback. This strategy limits the types of targets that can be registered as callbacks to virtual functions within classes that implement the proper interface. To register other functions or methods that would be useful as callback targets, you must write adapters that implement the abstract interface and call the alternative target, or modify the desired target to expose the required interface.

In most cases, requiring a specific interface to be implemented for all callback targets is exactly what is desired. For some systems, however, such as generalized frameworks or libraries designed to work with a wide variety of external software packages, it is useful to be able to register many different types of executable callback targets with a callback container, such as global functions, nonstatic and static methods of a class. This is the purpose of the libsigc++ library in C++, and the delegate mechanism in C#.

The libsigc++ library lets you define C++ Signal objects templated on a return type and on the types of arguments for a callback. A Signal is a callback container that can store any number of references to different callback targets. The name "Signal" points to one of the intended purposes for this class—to act as a generator of signals that interact with the connected callbacks at certain times. You can include Signals in a class, and connect callbacks to the Signal objects within instances of that class to obtain notification when the instance emits those Signals. Listing Three is an example of adding a Signal to a class simply as public member data. Signal is templated on the return type of the callback and the types of each callback argument, and there is actually a family of templated Signal classes—Signal0, Signal1, and so on—that work with different numbers of arguments.

Libsigc++ Signals can be connected to any number of Slot objects that act as generic wrappers around callback targets, using the Signal::connect method. Slots provide an abstraction for many different types of callback targets—global functions, static methods, nonstatic methods, even other Slots.

Listing Four provides examples of connecting callbacks to the Signal object created in Listing Three. Just like Signals, Slots are templated on the return type and argument types of the callback, and there is a family of Slot classes Slot0, Slot1, and so on. The main restrictions on the use of Slots are that the Slot template parameters must match those of the Signal to which it connects, and a class must inherit from SigC::Object for its nonstatic methods to be used in a Slot connected to a Signal.

Callbacks in a Signal can be invoked at any time by emitting the signal

sample.signalobj.emit(1, 2.0);

or by simply treating the Signal as a functor

sample.signalobj(1, 2.0);

Slot objects also implement operator() and can be used as functors that invoke the underlying callback target that they wrap. Emitting a Signal simply invokes all connected Slots in the order they were added.

When a connection is made between a Slot and a Signal, a Connection object that manages this connection is returned. You can sever the association by calling the disconnect() method on the relevant Connection object (Listing Four).

By comparison, C# provides a feature known as "delegates" to accomplish a similar task. Delegates can store a list of callback targets and invoke them when requested. They are declared using the delegate keyword; see Listing Five. You create and use delegate objects in C# classes in much the same way that libsigc++ Signal objects are used in C++; in fact, C# defines an event keyword that reads suspiciously like a signal and is used to create delegates with certain restrictions on their use outside the owning class.

The declaration of SampleDelegate in Listing Five defines a new delegate type, much like the typedef declaration of SampleSignal_t in Listing Three. SampleDelegate can now be used to define delegate instances that work with callbacks that have the SampleDelegate type signature. The delegate keyword is simply shorthand for declaring a new subclass of the System.MulticastDelegate class. C# delegate objects and libsigc++ Signal objects are both callback containers, storing references to callbacks of the specified type and capable of invoking them at later times.

To store references to callback targets, C# reuses the same delegate object instead of using a separate class, as is done in libsigc++ with the Slot objects. To connect a new callback to an empty delegate, you simply assign it a new instance of that delegate type initialized with the callback function, or use the += operator to connect an additional callback to the same delegate. You can connect static or nonstatic methods, as in Listing Six. A callback can be disconnected by using the -= operator, which requires a reference to the originally added callback delegate or a new instance that refers to the same callback target. This differs from the libsigc++ strategy, which returns Connection objects when a Slot is connected to a Signal, and uses Connection::disconnect to break the connection. The libsigc++ solution is more flexible in some situations—it lets users connect the same callback target to a Signal multiple times, while C# delegates can only store one reference to a target. C# delegates, just like libsigc++ Signals, act like functors, so that you can invoke all the registered callbacks by treating the delegate like a function:

sample.delegateobj(1, 2.0);.

Additional Callback Techniques

The libsigc++ library includes an API for defining Signal and Slot objects of different numbers of arguments, and for easily generating adapter Slots that can do things such as bind additional arguments to a callback expecting more arguments than the signal emits, or connect one Signal to another. Similarly, C# delegates have an API that may be used to perform more advanced callback operations.

Conclusion

Conceptually, both callback mechanisms I describe here are similar. Both define type-safe callback containers that can be used as signal or event objects, and let you dynamically connect or disconnect a variety of different callback targets for later invocation. The key implementation distinction between libsigc++ and C# delegates, beyond the clear difference in language, is that libsigc++ achieves a high degree of generality by using C++ templates, while C# delegates are provided as a core language feature understood by the compiler.

Syntactically, both packages require similar amounts of code to perform the basic tasks of a generalized callback system: declare Signals or delegates, connect or disconnect callback targets, and invoke callbacks through their containers (compare Listing Four and Listing Six). A type-safe callback package could quite possibly be written in C# with the help of the C# reflection and attribute APIs, but it would have some difficulty in matching the ease of use and minimal syntax afforded by the delegate keyword. Libsigc++, in this comparison, demonstrates the utility of the template features of C++ and uses them well to provide a more extensive and extensible set of callback management features such as callback adapters and return value marshalers.

DDJ

Listing One

/* Define the callback function */
float callback(int, double) { /* function body */ }
int main(int argc, char *argv[]) {
  /* Create a variable to store a reference to the callback */
typedef float (*CallbackRef)(int, double);
  CallbackRef cb;
  /* Register the callback function for later invocation */
  cb = &callback;
  /* Invoke the callback function with arguments */
  cb(1, 2.0);
}

Back to Article

Listing Two

// The abstract callback interface
class CallbackInterface {
public:
  // The target callback function
  virtual void callback(int, double) = 0;
};
// An implementation of the interface
class CallbackImpl : public CallbackInterface {
public:
  virtual void callback(int, double) {
    std::cout << "Callback invoked" << std::endl;
  }
};
// A class that stores a target callback reference and invokes it 
// later using operator()
class CallbackContainer {
public:
  CallbackContainer(CallbackInterface *c) : cb_m(c) { }
  void operator()(int a, double b) { cb_m->callback(a, b); }
private:
  CallbackInterface *cb_m;
};
int main(int argc, char *argv[]) {
  // Declare a callback container and add a reference
  CallbackContainer cb(new CallbackImpl());
  // Invoke the callback after doing some work
  cb(1, 2.0);
}

Back to Article

Listing Three

typedef SigC::Signal2<float, int, double> SampleSignal_t;
class SampleClass {
public:
  SampleSignal_t signalobj;
};

Back to Article

Listing Four

// A global function callback
float callback(int, double) { /* function body */ }
// A class with nonstatic method callbacks
class SampleCB : public SigC::Object {
public:
  float method(int, double) { /* body */ }
};
// Connect callbacks to the signal from Listing Three
int main(int argc, char *argv[]) {
  SampleClass sample;
  SampleCB target;
  // Connect a global function
  sample.signalobj.connect(SigC::Slot2<float,int,double>(callback));
  // Connect a method using 'slot' utility function, noting connection
  SigC::Connection connection =
    sample.signalobj.connect(slot(target, &SampleCB::method));
  // Emit the signal, calling 'callback' and 'method'
  sample.signalobj.emit(1, 2.0);
  // Disconnect the second callback then emit the same signal;
  // this will only invoke 'callback'
  connection.disconnect();
  sample.signalobj.emit(2, 2.0);
}

Back to Article

Listing Five

public delegate float SampleDelegate(int a, double b);
public class SampleClass {
  public SampleDelegate delegateobj = null;
}

Back to Article

Listing Six

// A class with static and nonstatic method callbacks
public class SampleCB {
  public static float callback(int a, double b) { return 1.0F; }
  public float method(int a, double b) { return 2.0F; }
};
// Connect callbacks to the delegate from Listing Five
public class MainClass {
  public static void Main(string[] args) {
    SampleClass sample = new SampleClass();
    // Connect a static method to the delegate in sample
    sample.delegateobj += new SampleDelegate(SampleCB.callback);
    // Connect an instance method to the delegate
    SampleCB target = new SampleCB();
    sample.delegateobj += new SampleDelegate(target.method);
    // Invoke the delegate, calling 'callback' and 'method'
    sample.delegateobj(1, 2.0);
    // Disconnect the second callback then invoke the delegate again;
    // this will only invoke 'SampleCB.callback'
    sample.delegateobj -= new SampleDelegate(target.method);
    sample.delegateobj(2, 2.0);
  }
}

Back to Article

Listing Seven

// A new Marshal class
template<class T>
class EqualZeroMarshal {
public:
  typedef T InType;
  typedef int OutType;
  OutType value()
  {
    return result_m;
  }
  static OutType default_value()
  {
    return 0;
  }
  // If this returns true, no more callbacks will be called.
  bool marshal(const InType &in)
  {
    if (in == 0)
      result_m++;
    return false;
  }
  EqualZeroMarshal() : result_m(0) { }
private:
  OutType result_m;
};
// Create a Signal using this marshaller.
SigC::Signal1<bool,float,EqualZeroMarshal<bool> > signalobj;


Back to Article

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.