JAN
11
2004

Combining the Advantages of Qt Signal/Slots and C# Delegates/Events

My favorite Qt feature is the Signal/Slots mechanism. Before working with Qt I only knew the horrors of Java's event handling by implementing interfaces, and libraries that worked only with simple functions but not with class methods. Qt was the first library that allowed to handle an event in a method of a specific object instance - which is what you actually want most of the time. Unfortunately Qt Signal/Slots are not really well integrated into the language. This is mainly due to the preprocessor mechanism that works only on the declarations. Thus the declaration syntax is fine, but the use of signals and slots in the definition has some problems and limitations:

  • It lacks compile-time checks. You can misspell signal or slot names, you can
    connect a signal to a slot even when their signature does not match and so on. This happened to me far too often and I only noticed the problem when the application was running. In most simple cases it is not so bad, when all signal/slots are connected at start-up you can see the errors on STDOUT (a bad solution IMHO, it should throw an exception or exit). But it gets worse as you write more complex code that connects signal/slots at a later point. I had this several times, for example my Desktop Sharing client rewires signal/slots on certain events.
    Then a wrong connect() becomes really ugly, because it may only trigger a bug in rare situations and the error is only visible when you watch STDOUT.
  • You can't use slots as target for callbacks or invoke a slot by name. This was certainly not a design goal of Qt signal/slots, but it makes the mechanism less powerful than C#'s delegates and creates the need for a second mechanism
  • The connect syntax is unnecessary complicated because of the SIGNAL()/SLOT() macros. Not that bad, but the syntax could be easier if it would be integrated into the language
  • Another minor syntax problem is the slot keyword. It should not be required to declare a slot, it would be easier to be able to connect any method. It happened to me more than once that I needed to write a slot that did nothing but call a function.

Before I continue to show C#'s delegate feature, here is a piece of code that uses signal/slots in Qt. I will use it as a reference to show other syntaxes:

class Counter : public QObject {
	Q_OBJECT
private:
	int mValue;

public:
	Counter() : mValue(0) {}
	int get() const { return mValue; }
	void set(int v) { mValue = v; emit changed(v); }
	void inc() { emit changed(++mValue); }
	void dec() { emit changed(--mValue); }

public signals:
	void changed(int newValue);
};

class CounterTest : public QObject {
	Q_OBJECT
private:
	Counter mCounter;
public:
	CounterUser() {
		connect(&mCounter, SIGNAL(changed(int)),
			this, SLOT(counterChanged(int)));
	}

	void start() {
		mCounter.set(5);
		mCounter.dec();
		mCounter.inc();
	}

public slots:
	void counterChanged(int newValue) {
		qDebug("The Counter changed, new value is %d.", newValue);
	}
};

void main() {
	CounterUser cu;
	cu.start();
}

As you can see it creates a Counter class that uses a Qt signal to notify slots

when the counter's value has changed. CounterTest creates a Counter, connects a slot
that notifies the user of changes and then modifies it a few times.





Delegates/Events in C#



Delegates are a C# mechanism for safe callbacks to object instance methods. Many people use the word delegate for two things, which confused me a lot at the beginning. It becomes easier to understand when you differentiate between these two: a delegate type describes the signature of a class method. The delegate type is not restricted to a class, only the signature of the method matters. A delegate instance referers to a specific method of a specific object instance. Every delegate instance has a delegate type. The declaration of a delegate type looks like this:

delegate int IntModifierCallback(int newValue);

This line declares a delegate type called 'IntModifierCallback' that gets an integer as argument and returns an int. The declaration looks as if delegates would be another native C# type, comparable to C++ function pointers, but actually delegate type declarations are just syntactic sugar for a regular class declaration. This is a simplified version of the generated code:

sealed class IntModifierCallback : System.Delegate {
	public IntModifierCallback(object o, unsigned int functionPtr) { /* some code here */ }

	public virtual int Invoke(int newValue) { /* some code here */ }
}

The constructor takes the object instance as the first argument, and a function pointer as a second.
'Invoke' always matches the delegate type's signature and must be called to invoke the delegate instance.



Creating a delegate instance works like creating any other object instance, using the new keyword. It can then be called like a regular function:

delegate int IntModifierCallback(int newValue);

class SomeClass {
	int square(int v) { return v*v; }
}

class Test {
	void Main() {
		SomeClass sc = new SomeClass();
		IntModifierCallback cb = new IntModifierCallback(sc, SomeClass.square);

		Console.WriteLine("4*4 is " + cb(4));
	}
}



The C# equivalent of a Qt signal is a 'event'. To use events you need to declare a multicast delegate type. It has exactly the same syntax as a regular delegate, but the return type of the function signature must be void. The generated class will then derive from System.MulticastDelegate instead of System.Delegate. Multicast delegates have an additional feature, you can add and remove delegates instances to/from a multicast delegate. This allows you to call several delegates with a single invocation, just like Qt allows you to connect several slots to a signal and then invoke them all by emitting the signal once:

// Multicast delegate: the return type is void
delegate void PrintANumber(int);

class SomeClass {
	void printDecimal(int v) { Console.WriteLine("Decimal: {0}", v); }
	void printHex(int v) { Console.WriteLine("Hexadecimal: {0:x}", v); }
}

// ...
class Test {
	void Main() {
		SomeClass sc = new SomeClass();

		PrintANumber cb = null;
		cb += new PrintANumber(sc, SomeClass.printDecimal);
		cb += new PrintANumber(sc, SomeClass.printHex);
		cb(10);
	}
}



A delegate instance is added using the '+=' operator and removed using '-='. Note that 'cb' is set to null and the first '+=' will create an instance for 'cb'.



The last element to get the Signal/Slot-like mechanism is the 'event' keyword. You could also expose a multicast delegate instance as a property and add your the event listeners using '+=', but then you could overwrite the delegate instance with '=' and delete the delegate instance list. The 'event' keyword works almost like a
property, but has two important differences: other classes can only use '+=' and '-=' operators,
but can not invoke or replace the delegate. And you do not need to write accessor
methods for the property, they are created automatically (you can write them yourself if you want though).



So here is the Qt example rewritten to C#:

delegate void CounterEvent(int newValue);

class Counter {
	private int mValue;

	public event CounterEvent changed;

	public Counter() : mValue(0) {}
	public int get() { return mValue; }
	public void set(int v) { mValue = v; changed(v); }
	public void inc() { changed(++mValue); }
	public void dec() { changed(--mValue); }
};

class CounterTest {
	private Counter mCounter;

	public CounterUser() {
		mCounter.changed += new CounterEvent(this, CounterTest.counterChanged);
	}

	public void start() {
		mCounter.set(5);
		mCounter.dec();
		mCounter.inc();
	}

	public void counterChanged(int newValue) {
		Console.WriteLine("The Counter changed, new value is {0}.", newValue);
	}

	static void Main() {
		start();
	}
};





Improving the C# syntax



C# delegates are more powerful than Qt Signal/Slots, but I think that they can be improved:

  • All delegate types should be anonymous, and there should be only one type for each signature. Thus all delegates with identical signature always have the same type. This is a prerequisite for the following points.
  • Creating a delegate instance in C# is relatively complicated and requires the existance of function pointers. An easier solution is to get a delegate instance by accessing the method like a field/property. Thus if you have a reference to a "CounterUser" object called 'cu', you can get a delegate to its counterChanged() method using "cu.counterChanged".
  • Instead of declaring delegate types you can define a delegate 'prototype' that defines the function signature. The prototype is just a moniker for the anynymous type, not a real type declaration, but more like a typedef.
    Thus several prototypes with the same function signature but different names are still compatible. The name of the prototype should not be exported or displayed as part of the API.
    It's valid for the whole source file when defined at the top or valid for the class if defined in the class body. The syntax is like the delegate type definition, but with a 'prototype' keyword instead of 'delegate'.
  • Events can, alternatively, be defined with the 'delegate' syntax and thus without declaring a prototype. This is the most common use of delegates and with this feature you rarely need to define a prototype.
  • The 'delegate' keyword is not needed anymore.



With these changes the Qt example looks like this:

class Counter {
	private int mValue;

	public event void changed(int newValue);

	public Counter() : mValue(0) {}
	public int get() { return mValue; }
	public void set(int v) { mValue = v; changed(v); }
	public void inc() { changed(++mValue); }
	public void dec() { changed(--mValue); }
};

class CounterTest {
	private Counter mCounter;

	public CounterUser() {
		mCounter.changed += counterChanged;
	}

	public void start() {
		mCounter.set(5);
		mCounter.dec();
		mCounter.inc();
	}

	public void counterChanged(int newValue) {
		Console.WriteLine("The Counter changed, new value is {0}.", newValue);
	}

	static void Main() {
		start();
	}
};

Note that there is no delegate or prototype declaration needed anymore. The delegate type does not need a name.



Here is the first C# example modified to use the new syntax:

prototype int IntModifierCallback(int newValue);

class SomeClass {
	int square(int v) { return v*v; }
}

class Test {
	void Main() {
		SomeClass sc = new SomeClass();
		IntModifierCallback cb = sc.square;
		
		Console.WriteLine("4*4 is " + cb(4));
	}
}



I think that the new syntax has several advantages:

  • It frees the developer from naming delegate types. Finding good delegate type names turned out to be quite difficult, and people started to use schemes that just describe the prototype (like IntIntToBool), which is just plain stupid. Protoype names do not matter because they won't be exposed as API and are not needed for events.
  • The keyword 'delegate' was confusing, because it suggested that you create a delegate (=instance),
    but you created a delegate type. It is like using an 'object' keyword instead of 'class'. 'prototype' is clearer.
  • It works fine in languages like Java that do not have function pointers (which are needed
    in the C# syntax for the second argument of the delegate constructor).
  • The syntax for the delegate instance creation is friendlier.
  • It cannot happen that the same function signature has two incompatible delegate types.

Comments

who love Qt. Don't try to implement the "new syntax" section, its a concept, not actually possible. Or at least it won't compile on my system :).


By matelich at Wed, 03/05/2008 - 17:11

Nice. A lot cleaner syntax. Like it a lot.


By Nick at Mon, 01/02/2017 - 12:40