c++move-constructorstdbindc++23

Why does bind_front/bind_back/not_fn/bind require Args... to be move-constructible?


I noticed that the std::bind_front/std::bind_back/std::not_fn that yields the perfect forwarding call wrapper all require that the function argument and argument arguments passed in must be move-constructible.

Take the standard specification for std::bind_front as an example:

template<class F, class... Args>
  constexpr unspecified bind_front(F&& f, Args&&... args);

[...]

Mandates:

  is_constructible_v<FD, F> &&
  is_move_constructible_v<FD> &&
  (is_constructible_v<BoundArgs, Args> && ...) &&
  (is_move_constructible_v<BoundArgs> && ...)

is true.

Where FD is the type decay_t<F>, and BoundArgs is a pack that denotes decay_t<Args>....

Also, the legacy std::bind has the Preconditions that FD and BoundArgs must meet the Cpp17MoveConstructible requirements in [func.bind.bind].

I can understand the is_constructible_v part since we must be able to forward arguments to the decayed copy inside the wrapper. But what confuses me is why we need to require these arguments to be move-constructible?

My initial guess was that this was to make the perfect forwarding call wrapper also move-constructible, because the standard requires that the call wrapper must meet the Cpp17MoveConstructible and Cpp17Destructible requirements according to [func.require].

However, this seems wrong, because a wrapper wrapping a non-movable object can still be move-constructible if the object has a valid copy constructor:

struct OnlyCopyable {
  OnlyCopyable(const OnlyCopyable&) = default;
  OnlyCopyable(OnlyCopyable&&) = delete;
};

struct Wrapper {
  OnlyCopyable copy1;
  std::tuple<OnlyCopyable> copy2;
};

static_assert(!std::move_constructible<OnlyCopyable>); 
static_assert( std::move_constructible<Wrapper>); // ok

It turns out that such extra constraint makes the standard reject the following:

#include <functional>

struct OnlyCopyable {
  OnlyCopyable() = default;
  OnlyCopyable(const OnlyCopyable&) = default;
  OnlyCopyable(OnlyCopyable&&) = delete;
};

struct OnlyCopyableFun {
  OnlyCopyableFun() = default;
  OnlyCopyableFun(const OnlyCopyableFun&) = default;
  OnlyCopyableFun(OnlyCopyableFun&&) = delete;
  int operator()(auto) const;
};

int main() {
  OnlyCopyable arg;
  auto fun1 = std::bind_front([](auto) { }, arg);  // ill-formed
  OnlyCopyableFun fun;
  auto fun2 = std::bind_front(OnlyCopyableFun, 0); // ill-formed
}

which in my opinion, it shouldn't be as I don't see the benefits of rejecting the above.

So, why does the standard require that the argument types passed into the call wrapper factory must be move-constructible? What is the rationale behind this?


Solution

  • Types that are "only copyable" are not something the C++ standard recognizes as valid, reasonable code. You are allowed to do it, but such types are conceptually broken as far as the standard is concerned. As such, many features of standard library types may not work with such types.

    Conceptually, move is treated as a specialized version of a copy, one that replaces copying in certain circumstances. This makes move a subset of copy. And most of the standard treats it that way: a type which can be copied can also be moved.

    Again, you can technically make a type which doesn't work like this, but most of the standard will not be happy with you. You can't put them into std::vector<T> for example.