Thursday, 7 May 2009

Using C++0x lambda to replace Boost Bind in C++03 code

[Note: This post looks ugly under Internet Explorer. If you want to tell me how to fix my CSS, that would be much appreciated]
[Note: This post was written using the Intel C++ compiler with -std=c++0x.]

As some of you know, I work on Worklog Assistant which is written using C++/Qt.

One thing that I've made very good use of is a slightly modified version of the code in an earlier post titled "Using Boost Bind and Boost Function with Qt".

As an example, consider the following code from the above app which creates a popup menu to toggle the visibility of table columns. This is done by creating a "toggle" action for each column header that... toggles the visibility. It might help to think how this action would be created in plain Qt (hint: it would be painful.) It would certainly not have the locality it does now.

void
showColumnHeaderContextMenu(ssci::CustomTableView * self,const QPoint & pos)
{
QMenu columns(ssci::CustomTableView::tr("Visible Columns"));
columns.setIcon(QIcon(QString::fromLatin1(":/ui/icons/grid.png")));

QHeaderView * headers(self->horizontalHeader());

std::vector<std::pair<QString,int> > sorted_columns;
getSortedColumns(self->model(),headers,sorted_columns);

for(int ii = 0; ii < headers->count(); ++ii)
{
QAction * action =
new QAction(sorted_columns[ii].first,
&columns);

bool is_hidden = headers->isSectionHidden(sorted_columns[ii].second);

action->setCheckable(true);
action->setChecked(!is_hidden);

using boost::bind;
using boost::function;

function<void()> toggle =
bind(&QHeaderView::setSectionHidden,headers,
sorted_columns[ii].second,!is_hidden);

ssci::connect(action,SIGNAL(triggered()),
toggle);

columns.addAction(action);
}

QMenu menu;
menu.addMenu(&columns);

menu.exec(self->mapToGlobal(pos));
}


Of particular interest is the code in the for loop which sets up the actions:
    function<void()> toggle =
bind(&QHeaderView::setSectionHidden,headers,
sorted_columns[ii].second,!is_hidden);

ssci::connect(action,SIGNAL(triggered()),
toggle);



This creates a function object on the fly which hides the selected column using code from the above linked post. To do this in plain Qt is a gigantic pain. LibQxt has a solution as well but mine is much better!

Anyway, the point is that you might find a lot of code using bind and function in an app like this. Binding member function pointers, member data pointers and function pointers is fairly normal and is really the only way to maintain sanity and reduce boiler plate. Nested binds are also used where appropriate.

Boost Bind is a way to create closures, or at least as close as you can get in C++ to true closures. Therefore it is natural to try and replace some uses of Boost Bind with C++0x lambda. There is good coverage of how C++0x lambda works here so I won't repeat the same information. However, I will cover the cases of bind I could convert and (horrifically) the cases I couldn't!

Using automatic variables



Consider the following code:

const int LICENSING_TAB_INDEX = 1;

ssci::connect(lblImportNewLicense,SIGNAL(linkActivated(QString const &)),
boost::function<void()>(
boost::bind(&QTabWidget::setCurrentIndex,
tabWidget,
LICENSING_TAB_INDEX)
)
);



The above function call sets things up so that when a link is clicked in the interface, the LICENSING_TAB_INDEX tab is selected in the configuration.

One thing Boost Bind does is that it stores all bound arguments by value. That means &QTabWidget::setCurrentIndex, tabWidget and LICENSING_TAB_INDEX are all stored by value. C++0x lambda calls this capturing. This can be done using implicit capture (by parsing the lambda body) or by you. By default, C++0x lambda captures variables by reference. It is potentially buggy to capture function-local variables by reference automatically. Therefore, the above code translated to C++0x lambda is:
[=]()
{ tabWidget->setCurrentIndex(LICENSING_TAB_INDEX); }

The first element of a lambda-introducer (the []) can be a capture default which can be one of = or &. The capture default tells the compiler how to capture those variables that are implicitly captured. The assignment is supposed to make you think of copy assignment and the & is supposed to make you think of reference to. So in the above case, to get the exact same behaviour as the bind, we need to intentionally copy all variables by value.

Great! Not so bad :-)

A simpler example... Or is it?



Consider the following code:

ssci::connect(clearSelectedRole,SIGNAL(clicked()),
boost::function<void()>(
boost::bind(
&QComboBox::setCurrentIndex,
projectRoles,
-1)
)
);


This code sets things up so that when the clearSelectedRole button is clicked, the corresponding QComboBox has an invalid index.

The C++0x version of this is really straightforward:

ssci::connect(clearSelectedRole,SIGNAL(clicked()),
boost::function<void()>(
[&](){projectRoles->setCurrentIndex(-1);}
)
);

In this case, the pointer object (not the value) projectRoles is captured by reference. This is not an issue because the pointer object is guaranteed to outlive the closure. Now you know why closures and garbage collection go together :-)

Verbosity or Why I wish C++0x used polymorphic lambdas



Consider the following code:

static
void
updateThreadSetup(QObject * parent,ssci::MainWindow * self);

m_checkForUpdatesThread(boost::bind(updateThreadSetup,_1,&parent))


The bound function in this case is an object that calls the function updateThreadSetup with a to-be-determined value for the first parameter and a fixed value for the second parameter.

The C++0x version of this is horrible:
m_checkForUpdatesThread([&parent](QObject * obj){updateThreadSetup(obj,&parent);});


First, since parent is a local variable (was passed into the function), you can't implicitly capture it. So you have to explicitly capture it. In this case, we want to capture it by reference, hence [&parent] in the lambda-introducer. Then, since this thread setup function is passed in a QObject* we have to tell the compiler to accept this argument. Apparently it isn't smart enough. Ask someone why it is this way, you'll hear some hand waving about the callable concept. Whatever.

That is some ugly code.

Anyway...

What you cannot convert without making your code super ugly



Consider the following simple code:
vector<int> d = {1,2,3,4,5};
vector<function<int()>> funcs;

for(vector<int>::iterator it = d.begin(), end = d.end();
it != end;
++it)
{
funcs.push_back(boost::bind(add5,*it));
}


This creates a vector of function objects that presumably add 5 to their bound argument and return the result.

Here is the equivalent in C++0x lambda:

vector<int> d = {1,2,3,4,5};
vector<function<int()>> funcs;

for(vector<int>::iterator it = d.begin(), end = d.end();
it != end;
++it)
{
int &i = *it; // WTF?
funcs.push_back([i](){add5(i);});//THIS IS REDUNDANT. REDUNDANT.
}


Naively, one might have done:

funcs.push_back([it](){add5(*it);});

in the loop. But this is a bug waiting to happen. Add the following line right after the for loop:
d.push_back(0)

Now all those iterators that are captured by value can be invalidated! YAY! The only way to avoid this issue is to manually extract the value referenced by the iterator and pass that into the closure which is what I did in my translation.

Conclusion



This exercise showed me that C++0x lambda is of some interest to me. I'd really like to get rid of the verbosity (I know, too late!) A couple of things would make this the perfect lambda for me:

  • Polymorphic lambdas (or at least let me specify auto for the arguments)

  • An easy way to capture computed values (the *it bug above)



I don't know how these problems would be solved, or whether they could be but until they are, I think there is still a future for function binding ala Boost Bind which is fine by me!

7 comments:

Éric said...

[Warning:
I didn't (yet) play with a compiler implementing C++0x lambdas, so I may very well be completely mistaken in the following.
--end warning]
You write that "lambdas capture all their referenced variables by reference unless told otherwise". This is not my understanding, which is that, by default, there is no capture at all. So referring to a name from the enclosing scope in a no-capture lambda (i.e., with "[]") is an error, and one has to either explictely name the referred variable ("[foo]" or "[&foo]") or set a default capture mode explicitely ("[&]" or "[=]").

Sohail Somani said...

Look at n2550. In this, the effective capture set includes:

for each name v that appears in the lambda expression and denotes a local variable or reference with
automatic storage duration in the context where the lambda expression appears and that does not appear in
the capture-list or as a parameter name in the lambda-parameter-declaration-list, &v if the capture-default
is & and v otherwise;

Éric said...

The section you quote is under:
"For a lambda-introducer of the form [ capture-default ] or [ capture-default , capture-list ],"

Just above that, I read:
"For a lambda-introducer of the form [ ], the effective capture set is empty."

So, my understanding is that there is no "default default capture mode":
- With [], there is no capture at all
- For a name appearing in the lambda expression to be bound without being explicitely mentioned in the capture list, a default capture mode must have been specified in the lambda introducer.

Note that I'm not trying to by nitpicky or to play the language lawyer. I'm only seeking for confirmation (or repudiation) of my understanding.

Sohail Somani said...

I think you are right. I don't know if this is a bug in Intel's compiler or if there has been some further discussion that is not reflected in the drafts...

To be sure, I think if [] implies [&], this is ok...

Sohail Somani said...

Btw I don't think you are being nitpicky at all. I read the standard before this little adventure to understand what it should do but it isn't very easy to understand!

jfreeman said...

First, it's always great to see lambdas catching on!

Eric is correct about default capture. Compilers are always free to implement extensions or variations, but to implicitly capture variables without a capture-default in the introducer is not standards-conforming.

Speaking of extensions, one of the features I wanted for lambdas but did not make it out of committee was the ability to specify expressions for the captures. To illustrate, your last example could be written

[&i=*it](){add5(i);}

or if add5 does what I think it does,

[&i=*it](){i+=5;}

This extension is supported in my GCC implementation.

Also, it may be worth pointing out that an empty parameter list can be omitted:

[&i=*it]{i+=5;}

In your third example, judging from your description, parent /should/ be captureable. The code shown looks incomplete---I cannot tell the scope of the lambda or what "parent" refers to---so I don't know for sure.

I do agree that polymorphic lambdas would be nice. Blame constrained generics (concepts) for them not being in the next standard. However, we do want to get to it someday.

Sohail Somani said...

J (don't know your real first name!), I think your syntax is ok. What was it that kept your extension from becoming standard? I think it would be useful to address it. If it was just lack of time on the part of committee members, I guess there isn't much one can do about it.

I think the reason parent wasn't "capturable" was because Intel seems to have [] imply [&] and it was not happy to capture a local variable (parent was a local variable) implicitly.

So from what you are saying, capturing parent implicitly via [] is a bug anyway so it is probably without any real consequence.

Thanks for your message!