Generic lambda-capture initializers, supporting capture-by-move

ISO/IEC JTC1 SC22 WG21 N3610 - 2013-03-15

Ville Voutilainen, ville.voutilainen@gmail.com

Introduction

C++11 lambdas do not support capture-by-move. There has been at least two rejected NB comments on this for C++11, the first one was JP9 on CD1, the second was FI8 on FCD. FI8 refers to a Core reflector message by Roshan Naik, where Roshan explains that it would be very useful to be able to move-capture containers and other objects that are expensive to copy. Furthermore, move-only types like iostreams (especially stringstreams) and unique_ptrs can't be captured without wrapping them. This lack of cooperation between move semantics and lambdas was briefly discussed in the Evolution Working Group in Portland 2012 as one of potential extensions that would "complete C++11". Unfortunately, scheduling conflicts and lack of time has prevented producing a wording proposal for such an extension; this paper tries to explain the intended design.

The problem, and its ugly work-around

The following code doesn't work:


#include <memory>
#include <iostream>
#include <utility>

template <class T> void run(T&& runnable)
{
  runnable();
};

int main()
{
  std::unique_ptr<int> result(new int{42});
  run([result](){std::cout << *result << std::endl;});
}

The assumption here is that by design, the function object invoked by run() is not supposed to capture 'result' by reference. Chances are it may run in a separate thread and result is no longer available, or for other reasons it really wants a move-constructed object. This doesn't work because a unique_ptr isn't copyable.

The problem has a work-around that should make people's stomach crawl: write a wrapper that performs move-on-copy, much like the deprecated auto_ptr:


#include <memory>
#include <iostream>
#include <utility>

template <class T> void run(T&& runnable)
{
  runnable();
};

template <class T> struct evil_wrap
{
  T payload;
  evil_wrap(T&& payload) : payload(std::forward<T>(payload)) {}
  evil_wrap(evil_wrap& other) : payload(std::move(other.payload)) {}
};

int main()
{
  evil_wrap<std::unique_ptr<int>> result{std::unique_ptr<int>{new int{42}}};
  run([result](){std::cout << *result.payload << std::endl;});
}

This wrapper arguably makes capture-by-move work for move-only types, but it's tedious to get right, and probably not suitable for non-novices, and the technique is a polar opposite of what we tried to achieve by deprecating auto_ptr and introducing unique_ptr.

Proposed solution

Rather than merely add support for capture-by-move, it has been suggested that we should add support for generic capture initialization. An example was proposed in the Portland 2012 meeting:


[ x { move(x) }, y = transform(y, z), foo, bar, baz ] { ... } 

In this case x is direct initialized by moving x, y is copy initialized with the result of calling transform, and the remainder are captured by value.

Why not capture with &&?

It's often asked why we wouldn't just support something like


[&&x] { ... } 

The issue here is that we're not capturing by an rvalue reference, we are attempting to move. If we would capture by rvalue reference, the move would occur too late, since it's intended to happen at capture time, not at call time. And as long as we're capturing something that has a name, we shouldn't do a hidden move from it.

Effect and dependencies to other features

There are potential similar issues to polymorphic lambdas and concepts. We didn't want to omit the type specifier from parameters of polymorphic lambdas, to avoid ambiguities and to allow constraints to be used. With generic capture initializers, the type of the capture may be deducible from its brace-or-equal-initializer, but we should perhaps consider whether we allow specifying the type of the capture, or to allow constraining the type. Example:


[int x = get_x()] { ... } 
[Container y{get_container()}] { ... }
[int z = 5] { ... }

As is probably clear from the example above, such capture initializers practically allow specifying named members for a lambda regardless of how they are initialized.

Wording

At this time, there unfortunately is no wording proposed.