After having worked with languages like Python and Objective C, it is kind of instinctive to search for dynamic features in Qt, to the point that it feels impossible to do something with bare C++ (without Qt Core at least).
In the past, the signal/slot just sounded like yet another UI toolkit event handling scheme, with the extra handicap of being "impure C++" (based on an IDL compiler). Well, I took the time to read the Qt headers and the MOC generated code, and I must say I envy the guys that created it, in particular because they did it back then at 1990s. Some random details:
1) "signals", "slots", "emit" and other Qt words are #define'd to absolutely nothing, so they don't cause any special behavior in developer's C++ code, which can be compiled and debugged normally.
2) Slots are normal C++ methods, laid out by the developer. The only special thing is that MOC adds some introspection information about them in moc_classname.cpp: method name, which includes the signature (i.e. parameter count and typing), and a big switch/case statement which maps slot string names into slot method calls.
3) Signals are also normal C++ methods; that's why "emit method()" works even though emit is #define'd as nothing. But the developer does not write signal method bodies; MOC does that in moc_classname.cpp. Of course, the signal method's body just forwards the call to Qt's signal dispatcher.
4) Signal methods cast all parameters to void* and put them in an array — a very opaque way of marshaling the parameters. This array is passed to the signal dispatcher.
5) The switch/case that translates slot names into actual method calls does the "unpacking" of this array of void*s. It does that believing blindly that the array contains the expected parameter types and quantity.
6) Since signal and slot names include the parameter signature, it is easy to test whether a given signal and a given slot have compatible signatures; it is just a matter of comparing strings. Better yet, it just needs to be tested at connect(). That's why the void* array of parameters can be cast "blindly" back to the original types.
7) The switch/case includes signals, too, so they can be invoked dynamically as well as slots. The purpose of this seems to be signal cascading (connecting a signal to another signal).
8) SIGNAL() and SLOT() simply convert what is inside parenthesis into a common string, which is the method signature. It's a simple macro #argument trick.
Thinking over the fact that every signal method has a real implementation, signals/slots feel more like a delegation mechanism (and not just an UI event propagation thing), because calling a (signal) method on one object causes the call of another (slot) method on another object.
Given that enough introspection information about slot methods is stored in moc_sourcecode.cpp, the next question is: how can I invoke dynamically a method, without having to connect to a signal. It seems that it was not (easily) possible before Qt 4.
In Qt 4, there is a special method called
QMetaObject::invokeMethod(object, method_name, param1, param2...)
that does the desired trick.
Problem is, how this call will know parameter types, so the "call signature" can be verified against target slot's signature? One solution would be to trust blindly our parameter list — if it doesn't fit the target method signature, a segmentation fault would take place. Not exactly an elegant solution.
The actual Qt solution is to wrap every argument with Q_ARG(). The argument types must be known by QMetaObject system so it "knows" the type and can do all checks and conversions. You can register your own classes in QMetaObject, allowing almost anything to be passed as Q_ARG(). If the method invocation has a wrong signature, a friendly error message will be displayed by Qt.
Google Search returns very few entries for "QMetaObject invokeMethod". I guess this technique is rarely employed. But I see a lot of potential in it. (I'll never get used to using multiple inheritance in C++ just because we need to call *one* slimy observer method, or using downcasts pervasively.)
The meta-object system, as well as the ability to register new types, is important in another Qt cornerstone: properties. Every object which is descendant of QObject can have properties added, set and recovered dynamically, without any previous specification in header file:
obj->setProperty("bla", 1); int x = obj->property("bla").toInt();
As expected, object->property() can not return the desired property directly, because properties may be of any type. This method returns a QVariant object, which in turn can be converted to the native type.
Using Q_PROPERTY() in the class header is needed only if you want the property to have collateral effects (i.e. cause a method call) when read and/or written. The methods abovementioned will still work for such getter/setter properties..
A more radical approach to add collateral effects to properties, is to extend property() and setProperty() methods themselves, intercepting every property request and potentially acting upon all of them. Those QObject methods are virtual, so it is a perfectly legal trick.
Of course, all those tricks are commonplace in Python and some of them are easy to do even in Objective C, but allowing those things in C++ is another story.