pw_function

The function module provides a standard, general-purpose API for wrapping callable objects.

Note

This module is under construction and its API is not complete.

Overview

Basic usage

pw_function defines the pw::Function class. A Function is a move-only callable wrapper constructable from any callable object. Functions are templated on the signature of the callable they store.

Functions implement the call operator — invoking the object will forward to the stored callable.

int Add(int a, int b) { return a + b; }

// Construct a Function object from a function pointer.
pw::Function<int(int, int)> add_function(Add);

// Invoke the function object.
int result = add_function(3, 5);
EXPECT_EQ(result, 8);

// Construct a function from a lambda.
pw::Function<int(int)> negate([](int value) { return -value; });
EXPECT_EQ(negate(27), -27);

Functions are nullable. Invoking a null function triggers a runtime assert.

// A function intialized without a callable is implicitly null.
pw::Function<void()> null_function;

// Null functions may also be explicitly created or set.
pw::Function<void()> explicit_null_function(nullptr);

pw::Function<void()> function([]() {});  // Valid (non-null) function.
function = nullptr;  // Set to null, clearing the stored callable.

// Functions are comparable to nullptr.
if (function != nullptr) {
  function();
}

pw::Function’s default constructor is constexpr, so default-constructed functions may be used in classes with constexpr constructors and in constinit expressions.

class MyClass {
 public:
  // Default construction of a pw::Function is constexpr.
  constexpr MyClass() { ... }

  pw::Function<void(int)> my_function;
};

// pw::Function and classes that use it may be constant initialized.
constinit MyClass instance;

Storage

By default, a Function stores its callable inline within the object. The inline storage size defaults to the size of two pointers, but is configurable through the build system. The size of a Function object is equivalent to its inline storage size.

Attempting to construct a function from a callable larger than its inline size is a compile-time error.

Inline storage size

The default inline size of two pointers is sufficient to store most common callable objects, including function pointers, simple non-capturing and capturing lambdas, and lightweight custom classes.

// The lambda is moved into the function's internal storage.
pw::Function<int(int, int)> subtract([](int a, int b) { return a - b; });

// Functions can be also be constructed from custom classes that implement
// operator(). This particular object is large (8 ints of space).
class MyCallable {
 public:
  int operator()(int value);

 private:
  int data_[8];
};

// Compiler error: sizeof(MyCallable) exceeds function's inline storage size.
pw::Function<int(int)> function((MyCallable()));

In the future, pw::Function may support dynamic allocation of callable storage using the system allocator. This operation will always be explicit.

API usage

Implementation-side

When implementing an API which takes a callback, a Function can be used in place of a function pointer or equivalent callable.

// Before:
void DoTheThing(int arg, void (*callback)(int result));

// After. Note that it is possible to have parameter names within the function
// signature template for clarity.
void DoTheThing(int arg, pw::Function<void(int result)> callback);

An API can accept a function either by value or by reference. If taken by value, the implementation is responsible for managing the function by moving it into an appropriate location.

Value or reference?

It is preferable for APIs to take functions by value rather than by reference. This provides callers of the API with a more convenient interface, as well as making their lives easier by not requiring management of resources or lifetimes.

Caller-side

When calling an API which takes a function by reference, the standard pattern is to implicitly construct the function in place from a callable object. Simply pass the desired callable directly to the API.

// Implicitly initialize a Function from a capturing lambda.
DoTheThing(42, [this](int result) { result_ = result; });

Size reports

Function class

The following size report compares an API using a pw::Function to a traditional function pointer.

Label

Segment

Before

Delta

After

Simple pw::Function vs. function pointer

(all)
(same)
0
(same)

Callable sizes

The table below demonstrates typical sizes of various callable types, which can be used as a reference when sizing external buffers for Function objects.

Label

Segment

Before

Delta

After

Function pointer

(all)
(same)
0
(same)

Static lambda (operator+)

(all)
(same)
0
(same)

Non-capturing lambda

(all)
(same)
0
(same)

Simple capturing lambda

FLASH
RAM
20,088
656
+64
+8
20,152
664

Multi-argument capturing lambda

FLASH
RAM
20,088
656
+64
+8
20,152
664

Custom class

(all)
(same)
0
(same)

Design

pw::Function is based largely on fbl::Function from Fuchsia with some changes to make it more suitable for embedded development.

Functions are movable, but not copyable. This allows them to store and manage callables without having to perform bookkeeping such as reference counting, and avoids any reliance on dynamic memory management. The result is a simpler implementation which is easy to conceptualize and use in an embedded context.