In the following code:
struct X
{
X() = default;
X(const X&) { printf("copy construct\n"); }
X& operator=(const X&) { printf("copy assign\n"); return *this; }
X(X&&) { printf("move construct\n"); }
X& operator=(X&&) { printf("move assign\n"); return *this; }
// Replacing the above two lines with these lines below causes a compile error.
// X(X&&) = delete;
// X& operator=(X&&) = delete;
};
void f(X x) {}
int main()
{
X x;
std::function<void (X)> fx(f);
f(x);
return 0;
}
if I define struct X to have copy and move operations, then a std::function with signature void (X) is able to bind to it. But if I delete the move operations, the code doesn't compile any more, with this error:
prog.cc:26:29: error: no matching constructor for initialization of 'std::function<void (X)>'
std::function<void (X)> fx(f);
candidate template ignored: requirement '__callable<void (*&)(X), false>::value' was not satisfied [with _Fp = void (*)(X)]
function(_Fp);
I'm just trying to understand why are the move operations required if the signature describes a function where X is passed by value?
std::function<void (X)> fx(f)
constructor invocation is ill-formed.
First, the requirements on this constructor:
[func.wrap.func.con]
template<class F> function(F f);
7 Constraints:F
is Lvalue-Callable (20.14.16.2) for argument typesArgTypes...
and return typeR
.[func.wrap.func]/2 A callable type (20.14.2)
F
is Lvalue-Callable for argument typesArgTypes
and return typeR
if the expressionINVOKE<R>(declval<F&>(), declval<ArgTypes>()...)
, considered as an unevaluated operand (7.2), is well-formed (20.14.3).
I believe that f
is not in fact Lvalue-Callable for argument types X
, however strange this may sound. This hinges on the definition of declval
:
[declval]
template<class T> add_rvalue_reference_t<T> declval() noexcept;
So, the type of declval<X>()
is actually X&&
, not X
. A call f(declval<X>())
would need to move from this rvalue reference to a by-value parameter - but the move constructor is declared deleted. And indeed, sizeof(f(std::declval<X>()), 0);
fails to compile, also complaining about the deleted move constructor.
In other words, std::function<void (X)> fx(f)
is ill-formed for essentially the same reasons that X x; f(std::move(x));
is ill-formed.
Practically speaking, std::function::operator()
needs to be able to forward its arguments to the wrapped callable, and uses std::forward
for that - which would also turn an rvalue into an rvalue reference and expect to be able to move the argument.