Сигналы и слоты в Qt5

http://woboq.com/blog/new-signals-slots-syntax-in-qt5.html
  • Перевод
Qt5 alpha увидел свет. В этой статье я опишу одну из фич, над которыми работал — это новый синтаксис сигналов и слотов.

Предыдущий синтаксис


Вот как мы обычно соединяем сигнал и слот:

connect(sender, SIGNAL(valueChanged(QString,QString)),
        receiver, SLOT(updateValue(QString)) );

На самом деле макросы SIGNAL and SLOT преобразуют свои аргументы в строки. Затем QObject::connect() сравнит эти строки с данными интроспекции собранными утилитой moc.

В чем проблема этого синтаксиса?

Не смотря на то, что в целом все работает хорошо, некоторые неудобства все же есть:

  • Невозможность проверки во время компиляции: Все проверки осуществляются во время исполнения, после парсинга строк. А это значит, что если в название сигнала или слота вкрадется опечатка, то программа успешно скомпилируется, но соединение не будет создано. Все что мы увидим — это предупреждение во время исполнения.
  • Так как все операции проходят со строками, имена типов в слотах обязаны буквально совпадать с именами типов в сигналах. Кроме того они должны совпадать в заголовочных файлах и в коде, описывающем соединение. А это означает проблемы при попытке использовать typedef-ы или пространства имен.

Новый синтаксис: использование указателей на функции


Готовящийся Qt5 поддерживает альтернативный синтаксис. Вдобавок к вышеописанному подходу вы сможете использовать вот такой новый способ соединения сигналов и слотов:

connect(sender, &Sender::valueChanged,
        receiver, &Receiver::updateValue );

Который из них более красивый — дело вкуса. Но привыкнуть к новому варианту очень просто.

Теперь рассмотрим преимущества, которые он дает:

Проверка во время компиляции

Вы получите ошибку компиляции, если ошибетесь в имени сигнала или слота, или если аргументы слота не будут соответствовать аргументам сигнала. Это сохранит вам время после рефакторинга.

Кроме того использован static_assert чтобы показывать понятные ошибки в случаях, если аргументы не совпадают или пропущен Q_OBJECT.

Автоматическое приведение типов аргументов

Теперь можно не только не опасаясь использовать typedef или пространства имен, но и соединять сигналы со слотами, которые принимают аргументы других типов, если неявное приведение возможно.

В следующем примере мы соединим сигнал, принимающий QString как параметр, со слотом, который принимает QVariant. Это сработает без проблем, так как QVariant имеет неявный конструктор, принимающий QString.


class Test : public QObject
{ Q_OBJECT
public:
    Test() {
        connect(this, &Test::someSignal, this, &Test::someSlot);
    }
signals:
    void someSignal(const QString &);
public:
    void someSlot(const QVariant &);
};

Соединяем сигнал с любой функцией

Как вы заметили в прошлом примере, someSlot был объявлен просто как публичный метод, без slot. Qt может напрямую вызвать слот и больше не нуждается в интроспекции для этого. (Хотя она все еще нужна для сигналов)

Но теперь мы также можем соединять синал с любой функцией или функтором:


static void someFunction() {
    qDebug() << "pressed";
}
// ... somewhere else
    QObject::connect(button, &QPushButton::clicked, someFunction);

Это может стать очень мощной фичей в сочетании с boost или tr1::bind.

Анонимные функции из C++11

Все описанное прежде работает и со старым C++98. Но если вы используете компилятор поддерживающий C++11, то я настоятельно рекомендую использовать новые языковые возможности. Lambda expressions поддерживаются по крайней мере MSVC 2010, GCC 4.5, clang 3.1. Для последних двух нужно указать -std=c++0x как флаг.

Теперь можно писать вот такой код:


void MyWindow::saveDocumentAs() {
    QFileDialog *dlg = new QFileDialog();
    dlg->open();
    QObject::connect(dlg, &QDialog::finished, [=](int result) {
        if (result) {
            QFile file(dlg->selectedFiles().first());
            // ... save document here ...
        }
        dlg->deleteLater();
    });
}

Это позволяет писать асинхронный код очень просто.
Поделиться публикацией

Похожие публикации

Комментарии 54

    +1
    inspired by boost::signals2?
      +12
      Даже и не знаю, что ответить :) Вообще изначально концепцию слотов в boost как раз позаимствовали из Qt.
        0
        Но в таком виде (compile-time соединения), по-моему, boost был первым, или я тут не прав? :)
          +1
          Соединение все-таки по-моему происходит в runtime. Просто на этапе компиляции теперь можно проверить, осуществимо ли оно.

          Насколько я помню, в Qt изначально была идея реализовать механизм сигналов/слотов через шаблоны, как это сделано сейчас в boost. Но от нее отказались, потому что не все компиляторы в то время хорошо поддерживали шаблоны (а для кроссплатформенной библиотеки это критично), и потому что не все, что можно сделать с помощью moc, можно выразить шаблонами.

          Подход Qt — более мощный, ценой немного меньшей производительности. А вообще в официальной документации Qt как всегда самое лучшее и полное объяснение: http://qt-project.org/doc/qt-4.8/templates.html

          P.S. Я тоже не знаю за что вас минусуют. Вопрос как вопрос.
            +1
            Вы думаете в бусте этот способ от хорошей жизни? boost::signals даже сравнивать c Qt signals нельзя. boost::signals менее функциональны и очень медленны, судя по тестам доступным в интернетах.
              0
              А вы можете дать пруф по поводу быстродействия, потому как в ссылке комментарием выше, в официально документации написано: «Qt's signals and slots implementation is not as fast as a template-based solution.»
                0
                тут

                А я и не писал, что Qt быстрее чем буст. Хотя это может быть так. Потомучто boost::signals плохо написаны, а не потомучто их подход медленнее.
          0
          Интересно, за что хоть минусуют? Мне правда интересно, кто первоистичник.
          0
          С замыканиями, увы, всё же действует правило в виде недопустимости использования переменных со стека функции, этакая ложка дёгтя так сказать.
            0
            В смысле, полноценного использования, как в языках типа шарпа.
              +1
              Упорно смотрю на код и понимаю, что ничего не понимаю в том, как работают плюсовые замыкания.
                +1
                Можно понимать их как невидимый класс с перегруженным operator() и конструктором, который делает то, что написано в []. В данном случае
                struct xxx
                {
                QDialog* dlg;
                xxx(QDialog* _dlg) : dlg(_dlg) {}
                operator()(int result)
                {
                ...
                }
                };

                Поэтому тягать с помощью [=] со стека можно что угодно; если нужна мутабельность — копировать указатель и как-то решить вопрос с мусором, как и сделано в примере с помощью new и deleteLater.
                  0
                  Я просто почему-то считал, что плюсовое замыкание всегда хранится как указатель на функцию, из-за чего при просмотре кода выше начал стремительно делить на ноль, пытаясь понять где хранятся затянутые в него переменные после выхода из saveDocumentAs.
              +2
              Простите, если я чего-то не понимаю, но как заменить такой код в новом синтаксисе?
              void SomeClass::Foo(const char *slot) {
                ...
                QAction *action = new QAction(...);
                connect(action, SIGNAL(triggered()), this, slot);
                ...
              }
              
                +2
                Старый способ же никуда не денется…
                  +2
                  void SomeClass::Foo(void (SomeClass::* slot)()) {
                    ...
                    QAction *action = new QAction(...);
                    connect(action, &QAction::triggered, this, slot);
                    ...
                  }
                  


                  Так как в Qt я не разбираюсь, с параметры слота взяты наугад.
                  0
                  А что насчёт скорости? Есть ли выигрыш/проигрыш, или же старый и новый способы соединения примерно равны по скорости исполнения?
                    +1
                    Новый метод будет явно быстрее, так как со строками ему работать не надо. Только вот откуда беспокойство, Вы сигналы сотнями в цикле подключаете?
                      0
                      Никто не может обещать, что кто-то возьмет ваш класс и создаст сотни экземпляров в цикле.
                      0
                      Сам connect в новом стиле будет немного быстрее (не нужен просмотр таблицы во время выполнения), а в вызове слотов вряд ли что-то изменится.
                      0
                      умоляю дайте пару ссылок на книжки Qt для желающего-но-тупого… из нечестно скачанного… всё сложно =(.
                      ей богу куплю книгу… очень хочу научится программировать с использованием Qt.
                        +8
                        По моему скромному личному мнению, самая лучшая книжка — документация Qt :)
                          0
                          Позвольте провести аналогию.
                          Лучше Handbook для FreeBSD нет ничего, НО в книге дают не только готовые команды, но и «зачем… почему так… что было до ...». Никогда не пожалел, что в своё время купил книгу.
                          Образно говоря, документация — это больше справочник.
                          А хорошая книга — это «ммм, ах вот зачем это делать так» и хорошая книга учит правильным вещам сразу и даёт направление.
                            +1
                            В офоках очень много разжёванных примеров и туториалов. Единственный минус (для многих) — неоконченный и неофициальный перевод на русский. Книги, как мне кажется, нужны уже после прохождения туториалов — для более глубокого понимания.
                              0
                              *офдоках
                              +1
                              Аналогия тут неуместна :) Доки и примеры Qt намного лучше чем Handbook.
                                0
                                >… в книге дают не только готовые команды, но и «зачем… почему так… что было до ...»
                                Образно говоря, документация — это больше справочник.


                                Понятно: документацию Qt вы в глаза не видели :)
                                0
                                А лично я начинал с книги Шлее, про Qt 4.5. Всё доступно и понятно. Лучше с неё начинать имхо.
                                +2
                                Документация и официальные примеры. В книгах вы ничего лучше все равно не найдете.
                                  0
                                  Когда-то давно, когда я почти не знал английского, но тоже хотел изучить Qt мне помог сайт doc.crossplatform.ru/qt/4.3.2/how-to-learn-qt.html
                                  И в частности doc.crossplatform.ru/qt/4.3.2/tutorial.html
                                  Да, информация слегка устаревшая, но общее представление дает.
                                  После этого можно запускать-читать примеры из Qt Examples и изучать по готовому коду.
                                    0
                                    Документацию никто не отменяет, но вот, чтобы начать самое оно:
                                    qt4.ru/blog/books/3.html
                                      0
                                      Я, когда-то тоже искал книгу по Qt. В результате что-то нашел, прочитал первые главы и плюнул. Документация тут решает, в отличии от многих других библиотек.
                                        0
                                        Классика: Бланшет/Саммерфилд. Предпочтительнее читать на английском (если нет серьёзных проблем с языком), там всё очень просто и подробно разжёвано.
                                        +3
                                        Наконец-то. Сколько проблем с connect возникало хотя бы из-за неймспейсов — везде приходилось писать идентификатор одинаково…
                                          0
                                          Можно ли будет писать так:
                                          class Test : public QObject
                                          { Q_OBJECT
                                          public:
                                              Test() {
                                                  connect(this, &Test::someSignal, this, &Test::someSlot);
                                              }
                                          signals:
                                              void someSignal(const QString &);
                                          public:
                                              void someSlot();
                                          };
                                          

                                          Всегда не хватало возможности соединять сигналы-слоты с разными списками аргументов. В примере выше хочется просто вызова без передачи чего-либо.
                                            0
                                            Думаю, что нет. Не вижу способа посчитать количество аргументов переданной функции.
                                              0
                                              Вообще-то это всегда было возможно :) Смотрите документацию.

                                              Нельзя только чтобы в слоте было меньше аргументов чем в сигнале. Поэтому иногда приходилось использовать QSignalMapper или свои велосипеды. Теперь с C++11 замыкания существенно упростят нам жизнь!
                                                0
                                                0 >= 1? Или нулевое количество — исключение?
                                                  0
                                                  Ой, это опечатка у меня, конечно. Больше, а не меньше.

                                                  В вашем примере у слота 0 аргументов, а у сигнала 1. Этот аргумент будет проигнорирован, тут ничего страшного.

                                                  Вот вам рабочий пример из документации:

                                                  connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
                                                0
                                                Так как показано в вашем примере, всегда было можно соединять. Наоборот нельзя, потому что если слот что-то принимает, то ему это, очевидно, надо дать. Ну и соответственно нельзя соединять сигналы и слоты с разными типами параметров, что на мой взгляд очевидно.
                                                  0
                                                  Ну, в новом синтаксисе можно будет соединять сигналы и слоты с разными типами параметров, если параметры сигнала имеют возможность неявного приведения к типам параметров слота.
                                                    +1
                                                    ну неявное преобразование это само собой разумеется, это возможности языка а не фреймворка, я говорю о не совместимых типах.
                                                    0
                                                    Окей, а как насчет аргументов с дефолтными значениями? Можно ли давать меньше аргументов слоту, в таком случае?
                                                      0
                                                      Да
                                                    0
                                                    >Всегда не хватало возможности соединять сигналы-слоты с разными списками аргументов.

                                                    Уж не знаю, почему вам её «всегда не хватало», но она в Qt всегда была :) Главное, чтобы количество аргументов слота было меньше либо равно количеству аргументов сигнала. Т.е. чтобы не оставалось «висячих» аргументов в слоте.
                                                    Избыточная информация — можно, недостаточная — нельзя.
                                                  • НЛО прилетело и опубликовало эту надпись здесь
                                                      0
                                                      Спасибо за статью.

                                                      Просто на всякий случай напомню: у метода connect есть еще и пятый аргумент (спасибо Нокии Троллтеку за это — он такой клевый!) — Qt::ConnectionType. С его использованием механизм сигналов/слотов играет еще более яркими красками!
                                                        0
                                                        На самом деле, не всё с новым синтаксисом сигналов и слотов так хорошо. В статье на Qt-project есть такой вот адский пример:

                                                        QTcpSocket *socket = new QTcpSocket;
                                                        ... 
                                                        QObject::connect(socket, static_cast<void (QTcpSocket::*)(QAbstractSocket::SocketError)>(&QAbstractSocket::error), [socket] (QAbstractSocket::SocketError) {
                                                            qDebug()<< "ERROR " << socket->errorString();
                                                            socket->deleteLater();
                                                        });
                                                        


                                                        Комментарий к примеру оттуда же:
                                                        As you might see in the example, connecting to QAbstractSocket::error is not really beautiful since error has an overload, and taking the address of an overloaded function requires explicit casting.

                                                        Some macro could help (with c++0x or typeof extensions)

                                                        The best thing is probably to recommend not to overload signals or slots …

                                                        … but we have been adding overloads in past minor releases of Qt because taking the address of a function was not a use case we support. But now this would be impossible without breaking the source compatibility.

                                                        Не знаю, насколько проблема актуальна на данный момент, но случай вполне себе из реальной жизни и использование нового синтаксиса превращает код в тотальный треш, угар и содомию. Хорошо, что это хоть чуть-чуть компенсируется возможностью использовать лямбды в качестве слотов.
                                                          0
                                                          Some macro could help

                                                          Я только что попробовал, и таки да, при помощи несложных манипуляций с препроцессорными макросами и новыми штуками C++ (я использовал decltype, но должно сработать и с нестандартным typeof) можно делать красиво. Правда, я трогал не Qt5, а просто написал сферический тест в вакууме, но в С++ принципиально возможно вместо
                                                          connect(testObject1, &TestClass1::someSignal1, testObject2, &TestClass2::someSlot1);
                                                          
                                                          писать
                                                          connect(testObject1, someSignal1, testObject2, someSlot1);
                                                          
                                                          с сохранением проверки на этапе компиляции и прочих плюшек.
                                                            0
                                                            Ну так значимый фрагмент сферического теста в студию! :)
                                                              +1
                                                              Это заклинание чинит decltype в текущей версии gcc.
                                                              template<typename T>
                                                              struct decltype_t
                                                              {
                                                                  typedef T type;
                                                              };
                                                              
                                                              #define DECLTYPE(expr) decltype_t<decltype(expr)>::type
                                                              

                                                              А это — кошки, на которых я тренировался:
                                                              template <class T>
                                                              void _invoke(T & obj, void (T::*method)())
                                                              {
                                                                  (obj.*method)();
                                                              }
                                                              
                                                              #define invoke(obj, method) (_invoke((obj), &DECLTYPE(obj)::method))
                                                              

                                                              class TestClass
                                                              {
                                                              public:
                                                                  void someSlot() { cout << "TestClass::someSlot invoked" << endl; }
                                                              };
                                                              
                                                              int main()
                                                              {
                                                                  TestClass test;
                                                                  _invoke(test, &TestClass::someSlot);
                                                                  invoke(test, someSlot);
                                                                  return 0;
                                                              }
                                                              
                                                          0
                                                          Последний пример приятно напомнил jQuery. Частенько подобного не хватало.

                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                          Самое читаемое