Search code examples
c++std-function

Why would a class need move operations to bind to a std::function that has a signature in which the class is passed by value?


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?


Solution

  • 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 types ArgTypes... and return type R.

    [func.wrap.func]/2 A callable type (20.14.2) F is Lvalue-Callable for argument types ArgTypes and return type R if the expression INVOKE<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.