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.