Doc. no.: P0792R13
Date: 2022-12-14
Audience: LWG
Reply-to: Vittorio Romeo <vittorio.romeo@outlook.com>
Zhihao Yuan <zy@miator.net>
Jarrad Waterloo <descender76@gmail.com>

function_ref: a type-erased callable reference

Table of contents

Changelog

R13

R12

R11

R10

R9

R8

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 examples

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.

Additional information

P2472 “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 in the implementation, 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.

For good or bad, the expression &qualified-id that retrieves pointer-to-member shares grammar with the expression that gets a pointer to explicit object member functions. [expr.unary.op/3]

Design decisions

Wording

The wording is relative to N4917.

Add new templates to [utility.syn], header <utility> synopsis, after in_place_index_t and in_place_index:

namespace std {
  [...]

  // nontype argument tag
  template<auto V>
    struct nontype_t {
      explicit nontype_t() = default;
    };

  template<auto V> constexpr nontype_t<V> nontype{};
}

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;              // freestanding, not defined
  template<class R, class... ArgTypes>
    class function_ref<R(ArgTypes...) cv noexcept(noex)>;           // freestanding, see below

[…]

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

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:


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 operators
    template<class F> function_ref(F*) noexcept;
    template<class F> constexpr function_ref(F&&) noexcept;
    template<auto f> constexpr function_ref(nontype_t<f>) noexcept;
    template<auto f, class U>
      constexpr function_ref(nontype_t<f>, U&&) noexcept;
    template<auto f, class T>
      constexpr function_ref(nontype_t<f>, cv T*) noexcept;

    constexpr function_ref(const function_ref&) noexcept = default;
    constexpr function_ref& operator=(const function_ref&) noexcept = default;
    template<class T> function_ref& operator=(T) = delete;

    // [func.wrap.ref.inv], invocation
    R operator()(ArgTypes...) 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>;
  template<auto f>
    function_ref(nontype_t<f>) -> function_ref<see below>;
  template<auto f>
    function_ref(nontype_t<f>, auto) -> function_ref<see below>;
}

An object of class function_ref<R(Args...) cv noexcept(noex)> stores a pointer to function thunk-ptr and an object bound-entity. bound-entity has an unspecified trivially copyable type BoundEntityType, that models copyable and is capable of storing a pointer to object value or a pointer to function value. The type of thunk-ptr is R(*)(BoundEntityType, Args&&...) noexcept(noex).

Each specialization of function_ref is a trivially copyable type [basic.types] that models copyable.

Within this subclause, call-args is an argument pack with elements such that decltype((call-args))... denote Args&&... respectively.



Constructors and assignment operators

[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> function_ref(F* f);

Constraints:

Preconditions: f is not a null pointer.

Effects: Initializes bound-entity with f, and thunk-ptr to the address of a function thunk such that thunk(bound-entity, call-args...) is expression-equivalent to invoke_r<R>(f, call-args...).


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

Let T be remove_reference_t<F>.

Constraints:

Effects: Initializes bound-entity with addressof(f), and thunk-ptr to the address of a function thunk such that thunk(bound-entity, call-args...) is expression-equivalent to invoke_r<R>(static_cast<cv T&>(f), call-args...).


template<auto f> constexpr function_ref(nontype_t<f>) noexcept;

Let F be decltype(f).

Constraints: is-invocable-using<F> is true.

Mandates: If is_pointer_v<F> || is_member_pointer_v<F> is true, f is not a null pointer.

Effects: Initializes bound-entity with a pointer to unspecified object or null pointer value, and thunk-ptr to the address of a function thunk such that thunk(bound-entity, call-args...) is expression-equivalent to invoke_r<R>(f, call-args...).


template<auto f, class U>
  constexpr function_ref(nontype_t<f>, U&& obj) noexcept;

Let T be remove_reference_t<U> and F be decltype(f).

Constraints:

Mandates: If is_pointer_v<F> || is_member_pointer_v<F> is true, f is not a null pointer.

Effects: Initializes bound-entity with addressof(obj), and thunk-ptr to the address of a function thunk such that thunk(bound-entity, call-args...) is expression-equivalent to invoke_r<R>(f, static_cast<cv T&>(obj), call-args...).


template<auto f, class T>
  constexpr function_ref(nontype_t<f>, cv T* obj) noexcept;

Let F be decltype(f).

Constraints: is-invocable-using<F, cv T*> is true.

Mandates: If is_pointer_v<F> || is_member_pointer_v<F> is true, f is not a null pointer.

Preconditions: If is_member_pointer_v<F> is true, obj is not a null pointer.

Effects: Initializes bound-entity with obj, and thunk-ptr to the address of a function thunk such that thunk(bound-entity, call-args...) is expression-equivalent to invoke_r<R>(f, obj, call-args...).


template<class T> function_ref& operator=(T) = delete;

Constraints:



Invocation

[func.wrap.ref.inv]

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

Effects: Equivalent to: return thunk-ptr(bound-entity, 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.


template<auto f>
  function_ref(nontype_t<f>) -> function_ref<see below>;

Let F be remove_pointer_t<decltype(f)>.

Constraints: is_function_v<F> is true.

Remarks: The deduced type is function_ref<F>.


template<auto f, class T>
  function_ref(nontype_t<f>, T&&) -> function_ref<see below>;

Let F be decltype(f).

Constraints:

Remarks: The deduced type is function_ref<R(A...) noexcept(E)>.


Feature test macro

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

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

Implementation Experience

A complete implementation is available from  zhihaoy/nontype_functional@p0792r13.

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 and Tomasz Kamiński for the thorough wording review.

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 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2472r3.html ↩︎