Doc. no.: P0792R7
Date: 2022-02-14
Audience: LEWG, LWG
Reply-to: Vittorio Romeo <[email protected]>
Zhihao Yuan <[email protected]>
Jarrad Waterloo <[email protected]>

function_ref: a type-erased callable reference

Table of contents

Changelog

R7

R6

R5

R4

R3

R2

R1

Abstract

This paper proposes the addition of function_ref<R(Args...)>, a vocabulary type with reference semantics for passing entities to call, to the standard library.

Motivating example

Here’s one example use case that benefits from higher-order functions: a retry(n, f) function that attempts to call f up to n times synchronously until success. This example might model the real-world scenario of repeatedly querying a flaky web service.

using payload = std::optional< /* ... */ >;

// Repeatedly invokes `action` up to `times` repetitions.
// Immediately returns if `action` returns a valid `payload`.
// Returns `std::nullopt` otherwise.
payload retry(size_t times, /* ????? */ action);

The passed-in action should be a callable entity that takes no arguments and returns a payload. Let’s see how to implemented retry with various techniques.

Using function pointers

payload retry(size_t times, payload(*action)())
{
    /* ... */
}

Using a template

template<class F>
auto retry(size_t times, F&& action)
requires std::is_invocable_r_v<payload, F>
{
    /* ... */
}

Using std::function or std::move_only_function

payload retry(size_t times, std::move_only_function<payload()> action)
{
    /* ... */
}

Using the proposed function_ref

payload retry(size_t times, function_ref<payload()> action)
{
    /* ... */
}

Design considerations

This paper went through LEWG at R5, with a number of consensuses reached and applied to the wording:

  1. Do not provide target() or target_type;
  2. Do not provide operator bool, default constructor, or comparison with nullptr;
  3. Provide R(Args...) noexcept specializations;
  4. Provide R(Args...) const specializations;
  5. Require the target entity to be Lvalue-Callable;
  6. Make operator() unconditionally const;
  7. Choose function_ref as the right name.

One design question remains not fully understood by many: how should a function pointer initialize function_ref?

In a typical scenario, there is no lifetime issue no matter whether the download entity below is a function, a function pointer, or a closure:

auto result = retry(3, download); // always safe

However, even if the users use function_ref only as parameters initially, it’s not uncommon to evolve the API by grouping parameters into structures,

struct retry_options
{
    size_t times;
    function_ref<payload()> action;
    seconds step_back;
};

payload retry(retry_options);

/* ... */

auto result = retry({.times = 3,
                     .action = download,
                     .step_back = 1.5s});

and structures start to need constructors or factories to simplify initialization:

auto opt = default_strategy();
opt.action = download;
auto result = retry(opt);

According to the P0792R5[2] wording, the code has well-defined behavior if download is a function. However, one cannot write the code as

auto opt = default_strategy();
opt.action = &download;
auto result = retry(opt);

since this will create a function_ref with a dangling object pointer that points to a temporary object – the function pointer.

In other words, the following code also has undefined behavior:

auto opt = default_strategy();
opt.action = ssh.get_download_callback(); // a function pointer
auto result = retry(opt);

The users have to write the following to get well-defined behavior.

auto opt = default_strategy();
opt.action = *ssh.get_download_callback();
auto result = retry(opt);

Survey

We collected the following function_ref implementations available today:

llvm::function_ref – from LLVM[3]

tl::function_ref – by Sy Brand

folly::FunctionRef – from Meta

gdb::function_view – from GNU

type_safe::function_ref – by Jonathan Müller[4]

absl::function_ref – from Google

They have diverging behaviors when initialized from function pointers:

Behavior A.1: Stores a function pointer if initialized from a function, stores a pointer to function pointer if initialized from a function pointer
OutcomeLibrary

Undefined:

opt.action = ssh.get_download_callback();

Well-defined:

opt.action = download;

llvm::function_ref

tl::function_ref

Behavior A.2: Stores a function pointer if initialized from a function or a function pointer
OutcomeLibrary

Well-defined:

opt.action = ssh.get_download_callback();

Well-defined:

opt.action = download;

folly::FunctionRef

gdb::function_view

type_safe::function_ref

absl::function_ref

P0792R5 wording gives Behavior A.1.

A related question is what happens when initialized from pointer-to-members. In the following tables, assume &Ssh::connect is a pointer to member function:

Behavior B.1: Stores a pointer to pointer-to-member if initialized from a pointer-to-member
OutcomeLibrary

Well-defined:

lib.send_cmd(&Ssh::connect);

Undefined:

function_ref<void(Ssh&)> cmd = &Ssh::connect;

tl::function_ref

folly::FunctionRef

absl::function_ref

Behavior B.2: Only supports callable entities with function call expression
OutcomeLibrary

Ill-formed:

lib.send_cmd(&Ssh::connect);

Ill-formed:

function_ref<void(Ssh&)> cmd = &Ssh::connect;

Well-defined:

lib.send_cmd(std::mem_fn(&Ssh::connect));

llvm::function_ref

gdb::function_view

type_safe::function_ref

P0792R5-R7 wording gives Behavior B.1.

Proposal

We propose Behavior A.2 to eliminate the difference between initializing function_ref from a function and initializing function_ref from a function pointer.

Additional information

P2472R1 “make function_ref more functional” [5] suggests a way to initialize function_ref from pointer-to-members without dangling in all contexts:

function_ref<void(Ssh&)> cmd = nontype<&Ssh::connect>;

Not convertible from pointer-to-members means that function_ref does not need to use invoke_r, improving debug codegen in specific toolchains with little effort.

Making function_ref large enough to fit a thunk pointer plus any pointer-to-member-function may render std::function_ref irrelevant in the real world. Some platform ABIs can pass a trivially copyable type of a 2-word size in registers and cannot do the same to a bigger type. Here is some LLVM IR to show the difference: https://godbolt.org/z/Ke3475vz8.

Wording

The wording is relative to N4901.

Add the template to [functional.syn], header <functional> synopsis:

[…]

  // [func.wrap.move], move only wrapper
  template<class... S> class move_only_function;        // not defined
  template<class R, class... ArgTypes>
    class move_only_function<R(ArgTypes...) cv ref noexcept(noex)>; // see below

  // [func.wrap.ref], non-owning wrapper
  template<class S> class function_ref;                 // not defined
  template<class R, class... ArgTypes>
    class function_ref<R(ArgTypes...) cv noexcept(noex)>;           // see below

[…]

Create a new section “Non-owning wrapper”, [func.wrap.ref] with the following:

General

[func.wrap.ref.general]

The header provides partial specializations of function_ref for each combination of the possible replacements of the placeholders cv and noex where:

An object of class function_ref<S> stores a pointer to thunk and a bound argument entity. A thunk is a function where a pointer to that function is a perfect forwarding call wrapper [func.def]. The bound argument is of an implementation-defined type to represent a pointer to object value, a pointer to function value, or a null pointer value.

function_ref<S> is a trivially copyable type [basic.types].

Within this subclause, call_args is an argument pack used in a function call expression [expr.call] of *this, and val is the value that the bound argument stores.


Class template function_ref

[func.wrap.ref.class]

namespace std
{
  template<class S> class function_ref;       // not defined

  template<class R, class... ArgTypes>
  class function_ref<R(ArgTypes...) cv noexcept(noex)>
  {
  public:
    // [func.wrap.ref.ctor], constructors and assignment operator
    constexpr template<class F> function_ref(F*) noexcept;
    constexpr template<class F> function_ref(F&&) noexcept;

    constexpr function_ref(const function_ref&) noexcept = default;
    constexpr function_ref& operator=(const function_ref&) noexcept = default;

    // [func.wrap.ref.inv], invocation
    R operator()(ArgsTypes...) const noexcept(noex);
  private:
    template<class... T>
      static constexpr bool is-invocable-using = see below;   // exposition only
  };

  // [func.wrap.ref.deduct], deduction guides
  template<class F>
    function_ref(F*) -> function_ref<F>;
}


Constructors and assignment operator

[func.wrap.ref.ctor]

template<class... T>
  static constexpr bool is-invocable-using = see below;

If noex is true, is-invocable-using<T...> is equal to:

  is_nothrow_invocable_r_v<R, T..., ArgTypes...>

Otherwise, is-invocable-using<T...> is equal to:

  is_invocable_r_v<R, T..., ArgTypes...>


template<class F> constexpr function_ref(F* f);

Constraints:

Effects: Constructs a function_ref object with the following properties:


template<class F> constexpr function_ref(F&& f);

Let T be remove_reference_t<F>.

Constraints:

Effects: Constructs a function_ref object with the following properties:


constexpr function_ref(const function_ref& f) noexcept = default;

Effects: Constructs a function_ref object with a copy of f’s state entities.

Remarks: This constructor is trivial.


constexpr function_ref& operator=(const function_ref& f) noexcept = default;

Effects: Replaces the state entities of *this with the state entities of f.

Returns: *this.

Remarks: This assignment operator is trivial.



Invocation

[func.wrap.ref.inv]

R operator()(ArgsTypes... args) const noexcept(noex);

Let g be the target object and p be the bound argument entity of *this.

Preconditions: p stores a non-null pointer value.

Effects: Equivalent to return g(p, std::forward<ArgTypes>(args)...);



Deduction guides

[func.wrap.ref.deduct]

template<class F>
  function_ref(F*) -> function_ref<F>;

Constraints: is_function_v<F> is true.

Feature test macro

Insert the following to [version.syn], header <version> synopsis, after __cpp_lib_move_only_function:

#define __cpp_lib_function_ref 20XXXXL // also in <functional>

Implementation Experience

A complete implementation is available from  zhihaoy/nontype_functional@p0792r7. You can play with it in Godbolt.

An older

Many facilities similar to function_ref exist and are widely used in large codebases. See Survey for details.

Acknowledgments

Thanks to Agustín Bergé, Dietmar Kühl, Eric Niebler, Tim van Deurzen, and Alisdair Meredith for providing very valuable feedback on earlier drafts of this proposal.

Thanks to Jens Maurer for encouraging participation.

References


  1. move_only_function http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0288r9.html ↩︎

  2. function_ref: a non-owning reference to a Callable http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0792r5.html ↩︎

  3. The function_ref class template. LLVM Programmer’s Manual https://llvm.org/docs/ProgrammersManual.html#the-function-ref-class-template ↩︎

  4. Implementing function_view is harder than you might think http://foonathan.net/blog/2017/01/20/function-ref-implementation.html ↩︎

  5. make function_ref more functional http://wg21.link/p2472r1 ↩︎