Noncopyable lambdas in C++

Noncopyable lambdas can sometimes be tricky to work with in C++, for a number of reasons. Take a look at snippet1.cpp:

snippet1.cpp:
#include <functional>
#include <iostream>
#include <memory>

std::function<void()> func() {
  auto ptr = std::make_unique<int>(1);
  return [_ptr = std::move(ptr)]() {
      std::cout << *_ptr << std::endl;
  };
}

int main() {
  func()();
}

It looks like we want func() to return a lambda that will print the value of a std::unique_ptr<int>. In this case it should print 1. But it doesn't compile! Instead, we get the following error:

Error 1:
functional:1571:10: error: call to implicitly-deleted copy constructor of '(lambda at snippet1.cpp:7:10)'
  new _Functor(*__source._M_access<_Functor*>());
      ^        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
snippet1.cpp:7:10: note: in instantiation of function template specialization 'std::function<void ()>::function<(lambda at snippet1.cpp:7:10), void, void>' requested here
  return [_ptr = std::move(ptr)]() {
         ^
snippet1.cpp:7:11: note: copy constructor of '​' is implicitly deleted because field '​' has a deleted copy constructor
  return [_ptr = std::move(ptr)]() {
          ^
unique_ptr.h:359:7: note: 'unique_ptr' has been explicitly marked deleted here
  unique_ptr(const unique_ptr&) = delete;
  ^

It's not a great set of error messages, but it's trying to tell us what's wrong; we're trying to call a method on unique_ptr that has been explicitly marked deleted. It's the copy constructor for unique_ptr, which is explicitly marked deleted because unique_ptr instances aren't copyable.

But where is the copy happening? Shouldn't C++14 move capture move ownership of ptr to the lambda object? First, it's important to take a look at how lambdas work in C++.

Lambdas and the ClosureType

A lambda object is really just an instance of an anonymous class (referred to as a ClosureType) that has a call operator (operator()) defined on it. This is a bit strange since C++ doesn't allow you to create anonymous classes explicitly, but it's analogous to anonymous classes in Java. The class definition doesn't have a name, so the C++ compiler refers to it as (lambda at snippet1.cpp:7:10) in error 1.

So what's really happening in snippet1.cpp:7 above is that a ClosureType is defined, an instance is created (the lambda), and we return it from func():

snippet2.cpp:
struct __anonymous {
  __anonymous(std::unique_ptr<int> ptr) : _ptr(std::move(ptr)) {}
  void operator()() {
    std::cout << *_ptr << std::endl;
  }
private:
  std::unique_ptr<int> _ptr;
};

std::function<void()> func() {
  auto ptr = std::make_unique<int>(1);
  return __anonymous(std::move(ptr));
}

The __anonymous struct works identically to the lambda in snippet1.cpp; It binds in the unique_ptr in its constructor (which is semantically identical to [_ptr = std::move(ptr)] in the lambda capture), and makes the object callable by overloading the operator() call operator.

However, when we try to compile snippet2.cpp, we get an almost identical error message but with some crucial information added:

Error 2:
snippet2.cpp:10:24: note: copy constructor of '__anonymous' is implicitly deleted because field '_ptr' has a deleted copy constructor
  std::unique_ptr<int> _ptr;
                       ^

Compare this to the note from error 1, which was generated from snippet1.cpp:

snippet1.cpp:7:11: note: copy constructor of '​' is implicitly deleted because field '​' has a deleted copy constructor
  return [_ptr = std::move(ptr)]() {
          ^

Now do you have a better idea of what's going on? When we introduce a noncopyable member to a class (that is, a member with a type that does not have a copy constructor defined for it, such as a unique_ptr), the compiler can't generate a default copy constructor for that class. So that means our __anonymous type from snippet2.cpp doesn't have a copy constructor defined for it, and it also means that the ClosureType that the C++ compiler generates for our lambda in snippet1.cpp won't have a copy constructor either.

But why is it even copying in the first place? Because in order to construct the std::function instance that's returned from func(), the ClosureType instance needs to be copied. It's not possible to avoid this copy since we need to convert the ClosureType to a std::function in order to satisfy the return type of the function, and there's no std::function constructor that can construct a std::function from a lambda (ClosureType) without copying it, so this will never work.

Instead, we should return the ClosureType directly. Since this type isn't given a name until the compiler generates it, we can't write it in syntax. Instead, we should use the auto return type!

snippet3.cpp:
auto func() {
  auto ptr = std::make_unique<int>(1);
  return [_ptr = std::move(ptr)]() {
      std::cout << *_ptr << std::endl;
  };
}

The return type auto will be deduced as a ClosureType, which is referred to as (lambda at snippet3.cpp:10:16) by the compiler. The return also doesn't require copying, because of copy elision.

Now with the func() definition from snippet3.cpp, the code compiles! Ownership of the unique_ptr that's returned from func() is moved into the lambda, and will be automatically destructed when the lambda returned by func() goes out of scope (or if ownership is moved elsewhere).

How does this work in Rust?

In Rust, values are moved by default. And there's no auto return type. So how can noncopyable lambdas be returned?

Rust has impl traits which allow you to specify some properties of unnamable types. In our case, we'd like to specify that the unnamable type we return from func() is some kind of lambda. We can do that with impl Fn():

snippet4.rs:
fn func() -> impl Fn() {
  let x = Box::new(0);
  move || {
    println!("x = {}", x);
  }
}

fn main() {
  let f = func();
  f();
}

This way, the type inferred for f is still an unnamable type, but it can be expressed more concretely than auto in C++ by expressing it as a type which implements the Fn trait.