External code emitting my Qt signal: is QPrivateSignal the right guard for a public signals section?
0 reputation · 29 Apr 2024, 11:11 UTC
0 reputation · 29 Apr 2024, 11:11 UTC
I have a QObject-derived class whose signals should only be emitted by the class itself, but since the signals macro expands to public in Qt 5 and later, any code that can see the header can call emit mySignal(). I want to prevent accidental external emission at compile time without breaking existing connect() calls from other objects.
The documented idiom is to declare a nested struct QPrivateSignal {}; in the private section and add a defaulted QPrivateSignal* parameter to each signal, so only the class itself can construct the argument and therefore emit it. External connects should still work because connection does not require constructing that type.
What is unclear to me is the actual boundary this creates:
SIGNAL() connects and QMetaObject::invokeMethod still trigger the signal at runtime regardless of the extra parameter?private slots: member truly unreachable via the meta-object system, or is C++ access control simply not enforced there?Target is Qt 5.15 / Qt 6 with the new-style connect syntax.
32275 reputation · 29 Apr 2024, 14:24 UTC
Yes, QPrivateSignal is the correct idiom for preventing compile-time emission of signals by external code. It effectively blocks direct C++ calls to emit mySignal() from outside the class because the caller cannot instantiate the required private token. However, it does not provide a runtime security boundary; it cannot block the Qt Meta-Object System from triggering the signal.
The idiom relies on C++ access control rather than Qt-specific logic. By adding a parameter of a type defined in the private section of your class, you create a dependency that external code cannot satisfy.
emit mySignal() from an external class will fail at compile time because the compiler cannot find a valid constructor for the QPrivateSignal argument.connect(&sender, &Sender::mySignal, ... ) continues to work perfectly because the connection only requires a pointer to the member function, not an instance of the arguments.QMetaObject::invokeMethod: Since this operates via the meta-object system at runtime, it bypasses C++ access modifiers and type checks. It can still trigger the signal.SIGNAL(): Legacy connections using the SIGNAL() macro resolve names at runtime and will still trigger the signal.To implement this in Qt 5.15 or 6, use the following pattern:
class MyObject : public QObject {
Q_OBJECT
private:
struct QPrivateSignal {}; // Token type
public:
// Signal with a defaulted private token
signals:
void dataChanged(int newValue, QPrivateSignal = QPrivateSignal());
};
| Area | Impact | Result |
|---|---|---|
| MOC Signature | Changed | The meta-object system now sees the QPrivateSignal as part of the signature. |
| Legacy Connects | Broken | Existing connect(..., SIGNAL(dataChanged(int)), ... ) calls will fail because the signature no longer matches. |
| Private Slots | Accessible | private slots are only private to the C++ compiler; they remain reachable via invokeMethod. |
object->mySignal(10); from a different class. It should trigger a compiler error regarding the private QPrivateSignal struct.QMetaObject::invokeMethod(object, "mySignal", ... ). If the signal fires, you have confirmed that the meta-object system bypasses the guard.Diagnostic Detail Required: Are you currently using any string-based SIGNAL() connections or QMetaObject::invokeMethod calls for these signals? If so, adding the private token will break those connections.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.