Если вы это пропустили, поддержка лямбда-выражений была добавлена ​​в C ++ в C ++ 11. В этой статье мы увидим, как использовать преимущества лямбда-выражений в вашем коде Qt, чтобы упростить его и сделать более надежным, а также какие подводные камни следует избегать.

Но сначала, что такое лямбда-выражение?

Лямбда-выражения - это анонимные функции, определенные непосредственно внутри тела другой функции. Их можно использовать везде, где сейчас передается указатель на функцию.

Синтаксис лямбда-выражения следующий:

[captured variables](arguments) {
    lambda code
}

Не обращая внимания на часть «захваченных переменных», вот простая лямбда, которая увеличивает число:

[](int value) {
    return value + 1;
}

Мы можем использовать эту лямбду в функции типа std::transform() для увеличения элементов вектора:

#include <algorithm>
#include <iostream>
#include <vector>
int main() {
    std::vector<int> vect = { 1, 2, 3 };
    std::transform(vect.begin(), vect.end(), vect.begin(),
                   [](int value) { return value + 1; });
    for(int value : vect) {
	std::cout << value << std::endl;
    }
    return 0;
}

Это печатает:

2
3
4

Захват переменных

Лямбда-выражение может использовать переменные в текущей области, «захватив» их. Вот пример, в котором мы используем лямбда для вычисления суммы вектора.

std::vector<int> vect = { 1, 2, 3 };
int sum = 0;
std::for_each(vect.begin(), vect.end(), [&sum](int value) {
    sum += value;
});

Как видите, мы захватили локальную переменную sum, чтобы использовать ее в лямбде. sum имеет префикс &, чтобы указать, что мы захватываем по ссылке: внутри лямбды sum является ссылкой, поэтому любое изменение, которое мы вносим в нее, влияет на переменную вне лямбды.

Если вместо этого вам нужна копия переменной, вы должны записать ее без префикса &.

Если вы хотите захватить несколько переменных, просто разделите их запятыми, как аргументы функции.

Невозможно напрямую захватить переменную-член, но вы можете захватить this и получить доступ ко всем членам текущего объекта.

Под капотом захваченные переменные лямбды хранятся в скрытом объекте, если компилятор не может быть уверен, что лямбда не будет использоваться за пределами текущей локальной области, и в этом случае он может оптимизировать это, напрямую ссылаясь на локальные переменные.

Можно немного полениться и захватить все локальные переменные. Чтобы захватить их все по ссылке, используйте [&]. Чтобы записать их все по копии, используйте [=]. Однако мы не рекомендуем это делать, потому что это упрощает обращение к переменным, жизненный цикл которых будет короче, чем жизненный цикл вашей лямбда-выражения, что приведет к нечетным сбоям, даже захват копированием может вызвать такие сбои, если вы скопируете указатель. Явный список переменных, от которых вы зависите, помогает избежать подобных ловушек. Если вы хотите узнать больше об этой ловушке, ознакомьтесь с пунктом 31 Эффективный современный C ++.

Лямбды в соединениях Qt

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

Вот пример телефонного дозвона, где пользователь может ввести номер и начать звонок:

Dialer::Dialer() {
    mPhoneNumberLineEdit = new QLineEdit();
    QPushButton* button = new QPushButton("Call");
    /* ... */
    connect(button, &QPushButton::clicked,
            this, &Dialer::startCall);
}
void Dialer::startCall() {
    mPhoneService->call(mPhoneNumberLineEdit->text());
}

Мы можем избавиться от метода startCall() с помощью лямбда:

Dialer::Dialer() {
    mPhoneNumberLineEdit = new QLineEdit();
    QPushButton* button = new QPushButton("Call");
    /* ... */
    connect(button, &QPushButton::clicked, [this]() {
        mPhoneService->call(mPhoneNumberLineEdit->text());
    });
}

Использование лямбда-выражений вместо QObject :: sender ()

Лямбды также являются отличной альтернативой использованию QObject::sender(). Представим, что наш номеронабиратель теперь представляет собой массив цифровых кнопок.

Без лямбда-выражений код для составления числа мог бы выглядеть так:

Dialer::Dialer() {
    for (int digit = 0; digit <= 9; ++digit) {
        QString text = QString::number(digit);
        QPushButton* button = new QPushButton(text);
        button->setProperty("digit", digit);
        connect(button, &QPushButton::clicked,
                this, &Dialer::onClicked);
    }
    /* ... */
}
void Dialer::onClicked() {
    QPushButton* button = static_cast<QPushButton*>(sender());
    int digit = button->property("digit").toInt();
    mPhoneService->dial(digit);
}

Мы могли бы использовать QSignalMapper, чтобы избавиться от метода диспетчера Dialer::onClicked(), но использование лямбда более гибкое и приводит к еще более простому коду. Нам просто нужно захватить цифру, связанную с кнопкой, и вызвать mPhoneService->dial() прямо из лямбды.

Dialer::Dialer() {
    for (int digit = 0; digit <= 9; ++digit) {
        QString text = QString::number(digit);
        QPushButton* button = new QPushButton(text);
        connect(button, &QPushButton::clicked,
            [this, digit]() {
                mPhoneService->dial(digit);
            }
        );
    }
    /* ... */
}

Не забывайте жизненные циклы объектов!

Взгляните на этот код:

void Worker::setMonitor(Monitor* monitor) {
    connect(this, &Worker::progress,
            monitor, &Monitor::setProgress);
}

В этом слегка надуманном примере у нас есть экземпляр Worker, который сообщает о ходе выполнения экземпляру Monitor. Все идет нормально.

Теперь предположим, что сигнал Worker::progress() имеет аргумент типа int, и мы хотим вызвать другой метод для monitor в зависимости от значения этого аргумента. Мы можем попробовать что-то вроде этого:

void Worker::setMonitor(Monitor* monitor) {
    // Don't do this!
    connect(this, &Worker::progress, [monitor](int value) {
        if (value < 100) {
            monitor->setProgress(value);
	} else {
	    monitor->markFinished();
	}
    });
}

Выглядит неплохо, но ... этот код может дать сбой!

Система соединений Qt достаточно умна, чтобы удалять соединения, если удаляется отправитель или получатель, поэтому в нашей первой версии setMonitor(), если удаляется monitor, удаляется и соединение. Но теперь мы используем лямбда для получателя: Qt не имеет возможности теперь, когда эта лямбда использует monitor. Даже если monitor будет удален, лямбда все равно будет вызываться, и приложение выйдет из строя при попытке разыменования monitor.

Чтобы избежать этого, вы можете передать аргумент «context» в вызов connect(), например:

void Worker::setMonitor(Monitor* monitor) {
    // Do this instead!
    connect(this, &Worker::progress, monitor,
            [monitor](int value) {
	if (value < 100) {
	    monitor->setProgress(value);
	} else {
	    monitor->markFinished();
	}
    });
}

В этой версии мы передаем monitor в качестве контекста в connect(). Это не повлияет на выполнение нашей лямбды, но когда monitor будет удален, Qt заметит и отключит Worker::progress() от нашей лямбды.

Этот контекст также используется для определения того, следует ли ставить соединение в очередь или нет. Подобно классическому соединению сигнал-слот, если поток объекта контекста не совпадает с кодом, излучающим сигнал, Qt будет использовать соединение с очередью.

Альтернатива QMetaObject :: invokeMethod

Возможно, вы знакомы со способом асинхронного вызова слота с помощью QMetaObject::invokeMethod. Учитывая такой класс:

class Foo : public QObject {
public slots:
    void doSomething(int x);
};

Вы можете указать Qt вызвать Foo::doSomething(), когда он вернется в цикл событий, используя QMetaObject::invokeMethod:

QMetaObject::invokeMethod(this, "doSomething",
                          Qt::QueuedConnection, Q_ARG(int, 1));

Это работает, но:

  • Синтаксис уродливый
  • Это не типобезопасный
  • Это заставляет вас объявлять методы для вызова как слоты

Вы можете заменить этот код на QTimer::singleShot(), вызывающий лямбду:

QTimer::singleShot(0, [this]() {
    doSomething(1);
});

Это немного менее эффективно, потому что QTimer::singleShot() создает объект под капотом, но если вы не вызываете это несколько раз в секунду, затраты на производительность незначительны, поэтому преимущества перевешивают недостатки.

Точно так же вы можете указать контекст перед лямбда, это в основном полезно для перехода между потоками. Однако будьте осторожны: если вы застряли с версией Qt старше 5.6.0, в QTimer::singleShot() есть ошибка, которая вызывает сбой при использовании между потоками. Мы узнали об этом на собственном горьком опыте ...

Ключевые выводы

  • Воспользуйтесь лямбда-выражениями для удаления методов диспетчера при соединении объектов Qt вместе
  • При использовании лямбда в вызове connect() всегда определяйте контекст
  • Не фиксируйте больше переменных, чем необходимо

Вот и все, мы надеемся, что вам понравилась эта статья, а теперь поищите места, где можно заменить стандартные методы на причудливые лямбды!