Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Generalized Callbacks: C++ and C#


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.

  • Automatic callback cleanup. What happens when a callback target is no longer valid, for example a callback that refers to a method in an object that has been deleted? For libsigc++, a class that makes its nonstatic methods available for connection to a Signal must inherit from SigC::Object, allowing the library to keep track of all the Signals to which a SigC::Object entity is connected. When that object is deleted, libsigc++ automatically disconnects all Slots that refer to its methods. Similarly, Signals disconnect from all Slots that they contain when it is deleted.

    In contrast, C# does not perform automatic delegate disconnection. When a nonstatic method for a class is registered with a delegate, the container creates a new reference to that instance, so the object will not be deleted by the garbage collector until it is removed from the delegate. To avoid potential unwanted dangling references, you may want to take extra care to always explicitly disconnect all callbacks that you add to a delegate.

  • Chaining callback containers. You can chain one libsigc++ Signal object to another by creating a Slot object that wraps around the target Signal, like the following code:

    Signal1<void, int> s1, s2;

    s1.connect(s2.slot());

    Here, emitting Signal s1 also invokes Signal s2 and all callbacks connected to it. The generic nature of Slots and the template facilities of C++ makes this fairly easy to implement.

    C# delegates can also be easily chained together. Since a delegate is both a container of other delegates and a wrapper around a callback target, you can simply add one delegate to another:

    SimpleDelegate d1 =

    new SimpleDelegate(Class1.method);

    SimpleDelegate d2 =

    new SimpleDelegate(Class2.method);

    d2 += new SimpleDelegate(Class3.method);

    d1 += d2;

    When d1 is invoked, it calls Class1.method, then invokes d2 that in turn calls Class2.method and Class3.method.

  • Mismatched argument types or counts. Consider the problem that may arise if you are using an external library that uses one of these callback packages to provide Signal or delegate containers, and you need to connect callback targets that are part of another external package. The targets may not exactly conform in the types of arguments, return values, or the number of arguments. One solution is to write adapters that map from the type of Signal/delegate to the type of the callback. Can you avoid this extra work?

    Libsigc++ provides a number of templated adapters that do just this. Adapter functions such as the following generate new Slot objects that wrap around an existing Slot to adapt it to a different use:

    Slot1<RT,T1> bind<T2>(Slot2<RT,T1, T2> &, T2 &extra) lets you connect a callback Slot that takes two arguments to a Signal that emits just one argument. The extra argument is specified in the bind call; the returned Slot1 stores the original Slot2 and the extra argument. Invoking the new Slot1 with the smaller number of arguments results in the wrapped Slot2 being called with the extra argument included at the end. Similar bind calls work with Slots of different argument counts.

    Slot2<RT,T1,T2> hide<T1>(Slot1<RT, T1> &) does the oppose of bind—it drops one of the arguments emitted from a Signal.

    Slot1<RT,NewT1> retype<NewT1> (Slot1<RT,T1> &) lets you convert the type of the argument to a Slot1 to another type, as long as there is a simple conversion between the types.

    Similar functions handle adapting the return type or value. In this area, C# has no functional equivalent. You must write your own adapters to convert from one type of delegate to another, or write your own subclass of System.MulticastDelegate.

  • Handling return values. What is the return value of a Signal or delegate, used as a functor, that in turn calls many different callbacks that each return different values? Libsigc++ refers to this issue as how do you marshal the return values. The answer is that you may want to do a number of different things—if the callbacks return Boolean values, for example, you may want the results ANDed together, or ORed together, or perhaps summed to determine how many callbacks returned True. Each Signal class takes a final template parameter that specifies a type of Marshal object (with a default Marshal type that simply uses the return value from the last invoked callback), and you can write your own marshalers to do specific operations. Thus, each Signal instance can customize how to combine the callback return values. Listing Seven is a custom marshaler that returns the count of the number of callback return values that equal zero.

    In C#, when you invoke a delegate for callbacks that return a value, the result is simply the value from the last callback that was called. To perform more complex marshaling, you must iterate over the target delegates stored within a delegate container and invoke each one by hand to see the results of each call. Libsigc++ also lets you iterate over the Slots in a Signal, but this capability is often not needed in an application.

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


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.