1

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?

1
  • 2
    The real question is: why would you delete the move constructor, if copy constructor is supposed to exist? what does this pattern that you are using mean? Normally we either need objects whose contents are in no way transferable to peers(no move, no copy), or objects whose contents shall be seen through a single handle(move only), or objects whose contents can be duplicated by peers(copyable); the last category might declare move as an optimization on function boundaries. Your weird pattern does not match any of the rational use cases. It is wise to ask why this pattern is allowed? Commented Sep 24, 2023 at 10:15

1 Answer 1

4

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 is formalized in the old Cpp17CopyConstructible named requirement which explicitly requires the Cpp17MoveConstructible requirement. Similarly, the C++20 concept std::copy_constructible explicitly requires std::move_constructible. This makes move a subset of copy; 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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, this may be the answer I'm looking for. Such unreasonable types may also involve types with explicit T(const T&) or T(T&). I wonder if the standard says anything about such "unhappy" types anywhere, although I doubt it.
Indeed, this idea is formalized in std::copy_constructible, and std::copy_constructible<OnlyCopyable> being false is what the standard has to say about it. ;-] (I.e. that the 'copyable' concept of the OP is not what 'copyable' actually means as far as the standard is concerned)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.