mercoledì 9 maggio 2018

Can you spot the error? (C++ lambda function)

Yesterday evening a spent several minutes debugging this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    FtpInterface *backend = createBackend(nextBackend);
                             
    qDebug() << "trying login with backend" << backend;
    QMetaObject::Connection connection =   
        QObject::connect(backend, &FtpInterface::stateChanged,
                         [=](FtpInterface::State state) {
            if (state == FtpInterface::Connected) {
                qDebug() << "Connected to backend" << backend;
                QObject::disconnect(connection);   
                useBackend(backend);           
                Q_EMIT q->stateChanged(state);                
            } else if (state == FtpInterface::Unconnected) {
                backend->deleteLater();                             
                tryNextBackend(host, port, username, password);
            }                                                                 
        });                                                                   
                                                                   
    backend->login(host, port, username, password);

It was crashing when the lambda function was invoked (though, sometimes, much later than that), and valgrind was reporting errors in the constructor of the connection variable, claiming that it was trying to use some memory which had been already deallocated by... roll of drums ...by the qDebug() on line 3!

I quickly assumed that the error had to be some memory corruption happening well before the call to qDebug, and that valgrind was for some reason not detecting the real error. And indeed, even by commenting out the qDebug() statement, the crash would still occur, just with a different cause.

Well, can you spot the error? I'll add a few blank lines here not to immediately give out the answer, so scroll down if you want to reveal it:

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

Well, the problem is that in the lambda function we are using the QMetaObject::Connection which was returned by the QObject::connect() call, except that by the time that the lambda captures the scope variables the connection variable hasn't been assigned yet! That's because, of course, first the compiler captures the local variables for the lambda, then the lambda is generated, then QObject::connect() is called, and only once it returns we have a properly initialized QMetaObject::Connection. It's obvious when you think about it, but it might not be as obvious when you read the code line by line.

And is this silly mistake of mine really worth writing a blog post about? Well, if you consider that I had encountered (and solved) the same exact issue just a few months before, in the very same project, then yes, putting it on words might definitely help my poor memory! :-)

For the sake of completeness, here's how the issue can be solved (if you know of more elegant solutions, please comment!):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    FtpInterface *backend = createBackend(nextBackend);                                                
    
    qDebug() << "trying login with backend" << backend;
    QSharedPointer<QMetaObject::Connection> connection(new QMetaObject::Connection);                   
    *connection =
        QObject::connect(backend, &FtpInterface::stateChanged,                                         
                         [=](FtpInterface::State state) {                                              
            if (state == FtpInterface::Connected) {
                qDebug() << "Connected to backend" << backend;                                         
                QObject::disconnect(*connection);
                useBackend(backend);
                Q_EMIT q->stateChanged(state);
            } else if (state == FtpInterface::Unconnected) {                                           
                backend->deleteLater();
                tryNextBackend(host, port, username, password);                                        
            }                                                                                          
        });                                                                                            
    
    backend->login(host, port, username, password);                                                    

In this way, while it's true that the QMetaObject::Connection object is still unitialized by the time we invoke QObject::connect(), the connection variable now refers to a fully initialized QSharedPointer that wraps our connection and that can be safely captured by the lambda.

I hope that you have a better memory than mine, so that this piece of advice might come useful the next time you are playing with QMetaObject::Connections and lambdas. :-)

Etichette: , ,

0 Commenti:

Posta un commento

Iscriviti a Commenti sul post [Atom]

<< Home page