7

I lately tried to do something like this:

auto x = std::make_unique<int>(1);
auto l = [y = std::move(x)]() { return *y; };
std::function<void()> f(std::move(l)); //error, requires copy construction

And to my huge disappointment and confusion it threw a bunch of error messages in my face. As you already know, std::function disallows construction from types that are not copy-constructible. Is there a specific reason why is it so? Or is it an overlook in the standard? What problems would construction from move-only types impose?

bartop
  • 9,971
  • 1
  • 23
  • 54
  • 1
    What would you expect to happen when you copy the `std::function`? – tkausl Jan 29 '20 at 13:08
  • 3
    `std::function` has to be copyable, otherwise you'd lose a huge benefit of that type. And conditionally deleting the copy constructor (and thus enabling move-only objects) doesn't work because `std::function` relies on type erasure. Only the constructor of `std::function` will know if a type is copyable or not. But that information is already lost on the class scope. – Timo Jan 29 '20 at 13:27
  • 1
    Does this answer your question? [Moving a lambda: once you've move-captured a move-only type, how can the lambda be used?](https://stackoverflow.com/questions/32486623/moving-a-lambda-once-youve-move-captured-a-move-only-type-how-can-the-lambda) – Zheng Qu Jan 29 '20 at 13:29

1 Answers1

7

std::function could take a move-only callable, but only by restricting itself to being move-only always. It uses type-erasure to store the object, so its static behavior must represent the lowest-common-denominator functionality that it requires of the objects it stores.

Here's the thing though: there are a lot of places in C++ and its standard library that expect that a callable shall be copyable. And by "lot", I mean the entirety of the standard algorithms library. Even the C++20 concept indirect_unary_predicate requires copy_constructible. Essentially, all algorithms are given the freedom to copy the given function at will; they even take those functions by value.

A move-only std::function type could never be used with any such algorithm. And being move-only is the only way for std::function to take move-only types.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • But why is the copy-constructability required in the OP's code? I can understand that it would not compile if the copy constructor of `std::function` was invoked. However, constructor of `std::function` should [initialize it's "copy" of constructor argument with `std::move`](http://eel.is/c++draft/func.wrap.func#con-10.sentence-1). – Daniel Langr Jan 29 '20 at 14:53
  • @DanielLangr: It's my second sentence: "It uses type-erasure to store the object, so its static behavior must represent the lowest-common-denominator functionality that it requires of the objects it stores." If the lowest-common-denominator that it requires of a type is "move-only", then `std::function` itself must be move-only. That's how type erasure works. – Nicol Bolas Jan 29 '20 at 15:04
  • Thanks for the clarification. I created a simplified `function` where [the problem is simulated](https://godbolt.org/z/-XQmZa). I first thought that `Derived::clone` did not need to be compiled, when copy constructor of `function` was not involved anywhere. But since `clone` is `virtual`, it seemingly must be compiled even if not used. Is this explanation right? (It's kind-of similar as in [this demo](https://godbolt.org/z/NRt7o6), where `f` is not compiled but `g` is even if not used, since it is `virtual`). – Daniel Langr Jan 29 '20 at 15:34
  • 1
    @DanielLangr that's the whole point of type erasure. We lose the information about whether a type is copy or move constructible when we exit the constructor of the erased container (`function` in your case). At that point we are bound to the contract that our erased container specifies, since there is no way to gain that lost information back (at least of today). The usage of inheritance is just an implementation detail and not really relevant. And yes, virtual functions have to be instantiated since you can access them via a base pointer. – Timo Jan 29 '20 at 16:45