Q_PRIVATE_SLOT with new connect syntax

When using PIMPL, we sometimes want to move implementation of slots into the private class as well. In order for Qt to be able to invoke those slots that formally exist only in the private class (which usually is not a QObject), we use the Q_PRIVATE_SLOT macro in the main class. It allows Qt to invoke the slot method, even though it exists in the private class.

Let’s have a short example:

/// mybutton.h

class MyButtonPrivate;
class MyButton : public QPushButton {
    Q_OBJECT
public:
    explicit MyButton(QWidget *parent);
    ~MyButton() noexcept override;

private:
    std::unique_ptr<MyButtonPrivate> const d_ptr;
    Q_DECLARE_PRIVATE(MyButton);

    Q_PRIVATE_SLOT(d_func(), void onClicked(bool));
};

/// mybutton.cpp

class MyButtonPrivate
{
public:
    void onClicked(bool clicked) {
        qDebug() << "Clicked!";
    }
};

MyButton::MyButton(QWidget *parent)
    : QPushButton(parent)
    , d_ptr(std::make_unique<MyButtonPrivate>())
{
    // Connecting to slot on "this" (MyButton*), although "onClicked" is defined in MyButtonPrivate
    connect(this, SIGNAL(clicked(bool)),
            this, SLOT(onClicked(bool)));
}

MyButton::~MyButton() noexcept = default;

Q_PRIVATE_SLOT does not create any new method in the MyButton class. The way Q_PRIVATE_SLOT works is that it just instructs moc to generate a metacall that looks like obj->d_func()->onClicked(val) instead of obj->onClicked(val), like it does for normal slots or invokables.

This approach has one big disadvantage: it means that Q_PRIVATE_SLOTs can only be invoked through the old QObject::connect() syntax.

So far I’ve been using a simple workaround to get all the compile-time checks that I would get with the new connect syntax normally:

connect(this, &MyButton::clicked,
        this, [this](bool clicked) { d_func()->onClicked(clicked); });

Here we use a lambda to forward the call to the actual PIMPL’ed slot. This is somewhat better than the old connect syntax but IMO it has two major drawbacks:

It’s hard to read - it’s difficult to immediatelly decipher what method the call is actually being forwarded to.

It’s tedious to write - it’s a lot of boilerplate code to be written and if there are too many arguments it becomes quite ugly. C++14 generic lambdas improve the situation a bit since we can use auto instead of spelling out the argument types, but I don’t think it makes the code necessarily better to read:

connect(this, &MyObject::mySignal,
        this, [this](const auto &foo, auto bar, auto *baz) {
            d_func()->mySlot(foo, bar, baz);
        });

It got me thinking if there might be some way to auto-generate the forwarding lambda and be able to just use the pointer-to-function here somehow.

In the end I came up with this tiny template function, which takes the d-pointer and the pointer to the PIMPL’ed slot and returns a generic lambda which gets passed into QObject::connect and which forwards arguments to the real slot method.

template<typename DPtr, typename Slot>
auto priv_slot(DPtr &&dptr, Slot &&slot)
{
    return [&dptr, &slot](auto && ... args)
    {
        (dptr->*slot)(std::forward<decltype(args)>(args) ...);
    }
}

The result has all the benefits of the new QObject::connect() syntax without the ugliness of the “forwarding lambda”:

connect(this, &MyObject::mySignal,
        this, priv_slot(d_func(), &MyObjectPrivate::onSignal));

Comments