P0288R9: move_only_function

P0288R9 is a proposal for C++23 that introduces std::move_only_function, a conservative, move-only equivalent of std::function. It doesn't have the const-correctness bug of std::function, provides support for const/&/&&/noexcept qualified function types, doesn't have the target_type() and target() accessors, and imposes a strong precondition on invocation.

Motivation

std::function is a function wrapper introduced in C++11. std::function can store, copy, and invoke copy-constructible callable objects, which excludes move-only callable objects. std::move_only_function addresses this issue as it doesn't provide a copy-constructor and a copy-assignment operator.

auto invocation_count = std::make_unique<std::uint64_t>(0);
// error: std::function target must be copy-constructible
std::function fn = [invocation_count = std::move(invocation_count)]() {
  std::println("{}", *invocation_count);
  *invocation_count += 1;
};

while (true) {
  fn();
}

std::function has a const-correctness bug. Its operator() is const-qualified even though the function object's operator() might be not, so it's possible to invoke a function object's non-const operator through a const instance. Because std::function owns the callable object, this bug will allow code to mutate its content through a const-reference. std::move_only_function addresses this issue.

// the `operator()` of the lambda expression's function object is not `const`-qualified
auto lambda = [&] mutable {};

{
  std::function<void(void)> fn = lambda;
  fn();

  const auto &fn_ref = fn;
  // the `operator()` of `lambda` is not `const`-qualified, but the invocation compiles
  fn_ref();
}

{
  std::move_only_function<void(void)> fn = lambda;
  fn();

  const auto &fn_ref = fn;
  // the `operator()` of `fn_ref` is not `const`-qualified, so the invocation is invalid
  fn_ref();
}

{
  // the `operator()` of `lambda` is not `const`-qualified, so the constraints can't be satisfied
  std::move_only_function<void(void) const> fn = lambda;
}

Design

template <typename R, typename... ArgTypes> class move_only_function;

template <typename R, typename... ArgTypes>
class move_only_function<
    R(ArgTypes...) MOVE_ONLY_FUNCTION_CV
        MOVE_ONLY_FUNCTION_REF noexcept(MOVE_ONLY_FUNCTION_NOEXCEPT)>;

The <functional> header provides a partial specialization of move_only_function for each combination of the qualifiers on the function type:

  • MOVE_ONLY_FUNCTION_CV is either empty or const.
  • MOVE_ONLY_FUNCTION_REF is either empty, &, or &&.
  • MOVE_ONLY_FUNCTION_NOEXCEPT is either true or false.

std::move_only_function class template provides polymorphic wrappers that generalize the notion of a callable object. These wrappers can store, move, and call callable objects, given a call signature, allowing functions to be first-class objects. The constructor that accepts a callable object is constrained with std::is_invocable_r or std::is_nothrow_invocable_r. The qualifiers present in the class template parameter will be forwarded to its operator(). The operator() won't check if the callable object is present, so it won't throw the std::bad_function_call exception.