Используем std::shared_ptr - это же модно!

1 мин на чтение

Недавно на проекте встретили интересный баг связанный с применением умных указателей совместно с лямбдами.

Рассмотрим простой пример, иллюстрирующий проблему:

// main.cpp

#include <QCoreApplication>

#include <memory>

#include <QDebug>
#include <QTimer>

int main(int argc, char * argv[]) {
    QCoreApplication a(argc, argv);

    QObject obj;
    {
        auto timer_ptr = std::make_shared<QTimer>();
        obj.connect(timer_ptr.get(), &QTimer::timeout, [&]() {
            qDebug() << "timeout!";
            timer_ptr.reset();
        });
        timer_ptr->start(1000);
    }

    return a.exec();
}

Как вы думаете, что делает этот код? Ждет секунду и печатает на экран “timeout!” - это же очевидно.

А вот и нет, на экран не будет напечатано НИЧЕГО. Давайте разберемся почему так происходит и в чем здесь ошибка.

Итак, у нас есть некий QObject obj, который “владеет” связью сигнала и лямбды. Эта связь существует до тех пор, пока существует obj. Очевидно, что он существует до конца функции main. Далее открывается блок, в котором создается std::shared_ptr<QTimer>. Время жизни этого объекта штука сложная и зависит от многих факторов. В данном случае, мы хотим, чтобы объект существовал столь же долго, сколько живет obj. Для этого мы выполняем в лямбде захват всех видимых в блоке объектов по ссылке. Это значит, что мы захватили ссылку на локальную переменную timer_ptr, которая умрет после выхода из блока. Проверим нашу гипотезу:

// main.cpp

#include <QCoreApplication>

#include <memory>

#include <QDebug>
#include <QTimer>

int main(int argc, char * argv[]) {
    QCoreApplication a(argc, argv);

    QObject obj;
    {
        auto timer_ptr = std::make_shared<QTimer>();
        obj.connect(timer_ptr.get(), &QTimer::timeout, [&timer_ptr]() {
            qDebug() << "timeout!";
            timer_ptr.reset();
        });
        obj.connect(timer_ptr.get(), &QTimer::destroyed,
                    []() { qDebug() << "destroyed!"; });
        timer_ptr->start(1000);
    }

    return a.exec();
}

В консоли видим радостное сообщение: destroyed!, а это значит, что при захвате умного указателя по ссылке, в лямбда выражении сохраняется именно ССЫЛКА на этот объект. Поэтому счетчик ссылок std::shared_ptr не увеличивается и объект разрушается сразу после выхода из блока.

Как это исправить? Очевидным решением является смена режима захвата с & на =. Но более правильно, будет захватить лишь то, что потребуется в дальнейшей работе:

// main.cpp

#include <QCoreApplication>

#include <memory>

#include <QDebug>
#include <QTimer>

int main(int argc, char * argv[]) {
    QCoreApplication a(argc, argv);

    QObject obj;
    {
        auto timer_ptr = std::make_shared<QTimer>();
        obj.connect(timer_ptr.get(), &QTimer::timeout, [timer_ptr]() mutable {
            qDebug() << "timeout!";
            timer_ptr.reset();
        });
        obj.connect(timer_ptr.get(), &QTimer::destroyed,
                    []() { qDebug() << "destroyed!"; });
        timer_ptr->start(1000);
    }

    return a.exec();
}

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

Исходники для своих экспериментов можно получить по ссылке: github

Дата изменения: