tree: d4c3f1c64b1d3a3172cb1283eb36227e608f064f [path history] [tgz]
  1. include/
  2. test/
  3. BUILD.gn
  4. fit.api
  5. OWNERS
  6. README.md
sdk/lib/fit/README.md

libfit

FIT is a lean library of portable C++ abstractions for control flow and memory management beyond what is offered by the C++ 17 standard library.

FIT only depends on the C++ language and standard library, including the stdcompat library to provide some C++ 17 library features. It offers essential enhancements to the C++ standard library rather than attempting to replace it or become a framework for writing applications. FIT can be thought of as an “annex” that expresses a few ideas we wish the C++ standard library might itself implement someday.

FIT is lean.

What Belongs in FIT

Several Fuchsia SDK libraries, such as libfidl, depend on FIT and on the C++ standard library. As these libraries are broadly used, we must take care in deciding what features to include in FIT to avoid burdening developers with unnecessary code or dependencies.

In general, the goal is to identify specific abstractions that make sense to generalize across the entire ecosystem of Fuchsia C++ applications. These will necessarily be somewhat low-level but high impact. We don‘t want to add code to FIT simply because we think it’s cool. We need evidence that it is a common idiom and that a broad audience of developers will significantly benefit from its promotion.

Here are a few criteria to consider:

  • Is the feature lightweight, general-purpose, and platform-independent?
  • Is the feature not well served by other means, particularly by the C++ standard library?
  • Is the feature needed by a Fuchsia SDK library?
  • Does the feature embody a beneficial idiom that clients of the Fuchsia SDK commonly use?
  • Has the feature been re-implemented many times already leading to code fragmentation that we would like to eliminate?

If in doubt, leave it out. See [Justifications] below.

What Doesn't Belong in FIT

FIT is not intended to become a catch-all class library.

Specifically prohibited features:

  • Features that introduce dependencies on libraries other than the C and C++ standard library.
  • Features that only work on certain operating systems.
  • Collection classes where the C++ 17 standard library already offers an adequate (if not perfect) alternative.
  • Classes that impose an implementation burden on clients such as event loops, dispatchers, frameworks, and other glue code.

Implementation Considerations

FIT is not exception safe (but could be made to be in the future).

Style Conventions

FIT's API style follows C++ standard library conventions.

In brief:

  • Class, method, field, and variable identifiers are snake_case.
  • Template parameters are CamelCase.
  • Preprocessor macros are UPPER_SNAKE_CASE.
  • Whenever a FIT API mimics a C++ standard library API, it should have a similar structure. For example, fit::function offers the same methods as std::function except where necessary to diverge due to its move-only semantics.

Rule of thumb: Using FIT should feel like using the C++ standard library.

Justifications

These sections explain why certain features are in FIT.

fit::function

  • libfidl‘s API needs a callable function wrapper with move semantics but C++ 14’s std::function only supports copyable function objects which forces FIDL to allocate callback state on the heap making programs less efficient and harder to write.
  • Lots of other C++ code uses callbacks extensively and would benefit from move semantics for similar reasons.
  • So we should create a move-only function wrapper to use everywhere.

fit::defer

  • When writing asynchronous event-driven programs, it can become challenging to ensure that resources remain in scope for the duration of an operation in progress and are subsequently released.
  • The C++ 14 standard library offers several classes with RAII semantics, such as std::unique_ptr, which are helpful in these situations. Unfortunately the C++ 14 standard library does not offer affordances for easily invoking a function when a block or object goes out of scope short of implementing a new class from scratch.
  • We have observed several re-implementations of the same idea throughout the system.
  • So we should create a simple way to invoke a function on scope exit.

fit::nullable

  • Case study: fit::defer has a need to store a closure that may be nullable. We were able to replace its hand-rolled lifetime management code with fit::nullable thereby vastly simplifying its implementation.
  • Case study: fpromise::future has a need to track its own validity along with a continuation that may or not be present.
  • Case study: We have previously observed bugs where developers were surprised when assigning a null closure to wrappers such as fit::function fit::defer, or fpromise::future left these objects in a supposedly “valid” but uninvocable state. These objects therefore take care to detect null closures and enter an “invalid” state. Using fit::is_null and fit::nullable makes it easier to eliminate this redundant state and simplifies the API for clients of these wrappers.
  • std::optional can be effective here but it doesn‘t directly handle nullity so it takes more care to coalesce the null and “not present” states. std::optional also increases the size of the object to carry an extra bool and passing, whereas fit::nullable eliminates this overhead by taking advantage of the underlying value’s null state (if there is one).
  • So we introduce fit::nullable to handle both cases systematically while still hewing close to the semantics of std::optional.

fit::result<E, Ts...>

fit::result is an efficient implementation of the general value-or-error result type pattern. The type supports returning either an error, or zero or one values from a function or method.

This type is designed to address the following goals:

  • Improve error handling and propagation ergonomics.
  • Avoid the safety hazards and inefficiency of out parameters.
  • Support effective software composition patterns.

Basic Usage

fit::result<E, T?> may be used as the return type of a function or method. The first template parameter E is the type to represent the error value. The optional template parameter T is zero or one types to represent the value to return on success. The value type may be empty, however, an error type is required.

#include <lib/fit/result.h>

// Define an error type and set of distinct error values. A success value is not
// necessary as the error and value spaces of fit::result are separate.
enum class Error {
  InvalidArgs,
  BufferNotAvailable,
  RequestTooLarge,
};

// Returns a pointer the buffer and its size. Returns Error::BufferNotAvailable
// when the buffer is not available.
struct BufferResult {
  uint8_t* const buffer;
  const size_t buffer_size;
};
fit::result<Error, BufferResult> GetBuffer();

fit::result<Error> FillBuffer(uint8_t* data, size_t size) {
  if (data == nullptr || size == 0) {
    return fit::as_error(Error::InvalidArgs);
  }

  auto result = GetBuffer()
  if (result.is_ok()) {
    auto [buffer, buffer_size] = result.value();
    if (size > buffer_size) {
      return fit::as_error(Error::RequestTooLarge);
    }
    std::memcpy(buffer, data, size);
    return fit::ok();
  } else {
    return result.take_error();
  }
}

Returning Values

fit::result emphasizes ease of use when returning values or otherwise signaling success. The result type provides a number of constructors, most of which are implicit, to make returning values simple and terse.

The following constructors are supported. Error and value constructors are listed for completeness.

Copy/Move Construction and Assignment

The result type generally has the same trivial/non-trivial and copy/move constructibility as the least common denominator of the error type E and the value type T, if any. That is, it is only trivially constructible when all of the supplied types are trivially constructible and it is only copy constructible when all of the supplied types are copy constructible.

fit::success

fit::result<E, T?> is implicitly constructible from fit::success<U?>, when T is constructible from U or both are empty.

fit::ok(U?) is a utility function that deduces U from the given argument, if any.

fit::success is not permitted as the error type of fit::result.

fit::result<Error> CheckBounds(size_t size) {
  if (size > kSizeLimit) {
    return fit::as_error(Error::TooBig);
  }
  return fit::ok();
}
fit::failed

The special sentinel type fit::failed may be used as the error type when an elaborated (enumerated) error is not necessary. When fit::failed is used as the error type, the result type is implicitly constructible from fit::failed.

fit::failed is not permitted as a value type of fit::result.

fit::result<fit::failed> CheckBounds(size_t size) {
  if (size > kSizeLimit) {
    return fit::failed();
  }
  return fit::ok();
}
fit::error

The result type is implicitly constructible from any instance of error space of the result, regardless of which types are used.

fit::result<std::string, size_t> StringLength(const char* string) {
  if (string == nullptr) {
    // Uses the deduction guide to deduce fit::error<const char*>. The
    // fit::result error constructor is permitted because std::string is
    // constructible from const char*.
    return fit::error("String may not be nullptr!");
  }
  return fit::ok(strlen(string));
}
fit::result<F, U?> with Compatible Error and Value

The result fit::result<E, T?> type is implicitly constructible from any other fit::result<F, U?>, where the error type E is constructible from the error type F and T is constructible from U, if any.

fit::result<const char*, const char*> GetMessageString();

fit::result<std::string, std::string> GetMessage() {
  return GetMessageString();
}

Discriminating Errors from Values

fit::result has two predicate methods, is_ok() and is_error(), that determine whether a result represents success and contains zero or one values, or represents an error and contains an error value, respectively.

fit::result<const char*, size_t> GetSize();

void Example() {
  auto result = GetSize();
  if (result.is_ok()) {
    printf("size=%zu\n", result.value());
  }
  if (result.is_error()) {
    printf("error=%s\n", result.error_value());
  }
}

Accessing Values

fit::result supports several methods to access the value from a successful result.

fit:result::value() Accessor Methods

The value of a successful result may be accessed using the value() methods of fit::result.

fit::result<Error, A> GetValues();

void Example() {
  auto result = GetValues();
  if (result.is_ok()) {
    A a = result.value();
  }
}
fit::result::operator*() Accessor Methods

*my_result is a syntax sugar for my_result.value(), when my_result is a fit::result.

fit::result::take_value() Accessor Method

The value of a successful result may be propagated to another result using the take_value() method of fit::result.

fit::result<Error, A> GetValues();

fit::result<Error, A> Example() {
  auto result = GetValues();
  if (result.is_ok()) {
    return result.take_value();
  } else {
    ConsumeError(result.take_error());
    return fit::ok();
  }
}
fit::result::operator->() Accessor Method

The members of the underlying value of a successful result may be accessed using the operator->() overloads of fit::result.

struct FooBarResult {
  Foo foo;
  Bar bar;
};
fit::result<Error, FooBarResult> GetFooBar();

void Example() {
  auto result = GetFooBar();
  if (result.is_ok()) {
    ConsumeFoo(std::move(result->foo));
    ConsumeBar(std::move(result->bar));
  }
}

fit::result forwards to the underlying value's operator->() overload when one is defined.

struct FooBarResult {
  Foo foo;
  Bar bar;
};
fit::result<Error, std::unique_ptr<FooBarResult>> GetFooBar();

void Example() {
  auto result = GetFooBar();
  if (result.is_ok()) {
    ConsumeFoo(std::move(result->foo));
    ConsumeBar(std::move(result->bar));
  }
}

Returning Errors

Returning errors with fit::result<E, T?> always involves wrapping the error value in an instance of fit::error<F>, where E is constructible from F. This ensures that error values are never ambiguous, even when E and T are compatible types.

There are a variety ways to return errors:

Direct fit::error

The most direct way to return an error is to use fit::error directly.

fit::error has a single argument deduction guide fit::error(T) -> fit::error<T> when compiling for C++17 and above to simplify basic error return values.

fit::result<std::string, size_t> StringLength(const char* string) {
  if (string == nullptr) {
    return fit::error<std::string>("String is nullptr!");
  }
  return fit::ok(strlen(string));
}

fit::result<std::string, size_t> StringLength(const char* string) {
  if (string == nullptr) {
    return fit::error("String is nullptr!");
  }
  return fit::ok(strlen(string));
}

// Error with multiple values.
using Error = std::pair<std::string, Backtrace>;

fit::result<Error, size_t> StringLength(const char* string) {
  if (string == nullptr) {
    return fit::error<Error>("String is nullptr!", Backtrace::Get());
  }
  return fit::ok(strlen(nullptr));
}
fit::as_error Utility Function

The single-argument utility function fit::as_error may be used to simplify returning a fit::error<F> by deducting F from the argument type.

This function is a C++14 compatible alternative to the deduction guide.

fit::result<std::string, size_t> StringLength(const char* string) {
  if (string == nullptr) {
    return fit::as_error("String is nullptr!"); // Deduces fit::error<const char*>.
  }
  return fit::ok(strlen(string));
}

Handling Errors

fit::result supports ergonomic error handling and propagation.

error_value() and take_error() Access Methods

The error value of a result may be accessed by reference using the error_value() methods.

The error may be propagated using the take_error() method, which returns the error value as an instance of fit::error<E>, as required to pass the error to fit::result.

fit::result<const char*> Example() {
  if (auto result = GetValues()) {
    // Use values ...
    return fit::ok();
  } else {
    LOG_ERROR("Failed to get values: %s\n", result.error_value());
    return result.take_error();
  }
}
Augmenting Errors

fit::result supports augmented error types, where details about the error are accumulated as the error propagates back through the call chain. The result type conditionally overloads operator+= to append details to contained error.

The error type E must be a class: pointers, primitives, and enums are not enabled. E must also overload operator+= to receive the value to append to the error.

// Define an error type with augmentable details, similar to absl::Status.
class ErrorMsg {
 public:
  explicit ErrorMsg(Error error) : error_(error) {}
  ErrorMsg(Error error, std::string detail) : error_{error}, details_{{std::move(detail)}} {}

  Error error() const { return error_; }
  const auto& details() const { return details_; }

  std::string ToString() const;

  // fit::result detects this operator and enables augmentation of the error.
  ErrorMsg& operator+=(std::string detail) {
    details_.push_back(std::move(detail));
    return *this;
  }

 private:
  Error error_;
  std::vector<std::string> details_;
};

fit::result<Error> FillBuffer(uint8_t* data, size_t size);

fit::result<ErrorMsg> FillBufferFromVector(const std::vector<uint8_t>& vector) {
  // ErrorMsg is constructible from Error.
  fit::result<ErrorMsg> result = FillBuffer(vector.data(), vector.size());
  if (result.is_error()) {
    result += fit::error("Error while filling from vector.");
  }
  return result;
}

Relational Operators

fit::result supports a variety of relational operator variants.

fit::success and fit::failure

fit::result is always equal/not equal comparable to instances of fit::success<> and fit::failed.

Comparing to fit::success<> is equivalent to comparing to is_ok(). Comparing to fit::failed is equivalent to comparing to is_error().

fit::result<Error, A> GetValues();

fit::result<Error> Example() {
  auto result = GetValues();
  if (result == fit::ok()) {
    return fit::ok();
  }
  return result.take_error();
}
Any fit::result with Compatible Value Types

fit::result<E, T?> and fit::result<F, U?> are comparable when T is comparable to U, if any. The error types are not compared, only the is_ok() predicate and values are compared.

Comparing two result types has the same empty and lexicographic ordering as comparing std::optional<T>.

fit::result<Error, int> GetMin();
fit::result<Error, int> GetMax();

bool TestEqual() {
  // Returns true when both results have values and the values are the same.
  return GetMin() == GetMax();
}
Any Type U Comparable to T

When fit::result<E, T> has a single value type T, the result is comparable to any type U that is comparable to T.

fit::result<Error, std::string> GetMessage();

bool TestMessage() {
  // Returns true if there is a message and it matches the string literal.
  return GetMessage() == "Expected message";
}

Guidelines for using fit::result

Use the following guidelines to make the most of the result type's ergonomic and safety features.

Return Multiple Values Using Aggregates

Define an aggregate structure to return multiple values. Use meaningful names for each aggregate member to improve readability.

struct CreateFooBarResult {
  Foo foo;
  Bar bar;
};
fit::<Error, CreateFooBarResult> CreateFooBar(Baz baz);

Use Named Constructors for Complex Initialization

For types that require complex initialization that could fail, use a static method (i.e. a named constructor) to perform the initialization. Make the constructor private and only perform member initialization using values passed in from the named constructor.

class Foo {
 public:
  fit::result<Error, Foo> Create(size_t size) {
    auto buffer_result = AllocateBuffer(size);
    if (buffer_result.is_error()) {
      return buffer_result.take_error();
    }
    auto bar_result = Bar::Create(size);
    if (bar_result.is_error()) {
      return bar_result.take_error());
    }
    return fit::ok(Foo{std::move(buffer_result.value()), size, std::move(bar_result.value())});
  }

 private:
  Foo(std::unique_ptr<uint8_t[]> buffer, size_t size, Bar bar)
    : buffer_{std::move(buffer)}, size_{size}, bar_{std::move(bar)} {}

  std::unique_ptr<uint8_t[]> buffer_;
  size_t size_;
  Bar bar_;
};

Prefer Result Types to Output Parameters

Output parameters are often used when an operation might fail. Typically, the return value is used to indicate success or (possibly enumerated) failure, while other values are returned using the output parameters.

Output parameters introduce ambiguities that should be avoided:

  • Are parameters pure outputs or mutable inputs?
  • Is nullptr permitted or will it cause a CHECK-fail?
  • What are the pre-conditions of the output states?
  • What happens to the pre-existing states of the outputs on success?
  • What states are the outputs left in on failure?
  • What are the lifetime requirements of the output variables?

These ambiguities are often the source of subtle bugs. The result pattern avoids ambiguity by construction: returning a value or an error is mutally exclusive.

Consider the following example using output parameters. It is difficult to infer the answers to the questions above without referring to documentation. Even with documentation, there is non-trivial cognitive load to check that the implementation is correct, that callers follow the rules, and that the documentation is consistent.

enum class Status {
  Ok,
  InvalidArgs,
  NoMemory,
  Clamped,
};

Status AllocateBuffer(size_t size, std::unique_ptr<uint8_t[]>* buffer_out, size_t* size_out) {
  if (size == 0) {
    buffer_out->reset(nullptr);
    size_out = 0; // Forgot to dereference size_out!
  }
  if (buffer_out == nullptr) {
    return Status::InvalidArgs;
  }

  // What about size_out == nullptr?

  if (size > kMaxSize) {
    size = kMaxSize;
  }

  fbl::AllocChecker checker;
  std::unique_ptr<uint8_t[]> buffer{new (&checker) uint8_t[size]};
  if (!checker.check()) {
    *size_out = 0; // What state should buffer_out be left in?
    return Status::NoMemory;
  }

  *buffer_out = std::move(buffer);
  *size_out = size;

  // Was size really clamped or just concidently the max?
  return size == kMaxSize ? Status::Clamped : Status::Ok;
}

Compare the previous example with the following example using the result type. The Status type only enumerates the two possible reasons for failure, there is no need to enumerate success states. The output states are well-defined and there is very little cognitive load to validate the implementation and callers.

enum class Status {
  InvalidArgs,
  NoMemory,
};

struct AllocateResult {
  std::unique_ptr<uint8_t[]> buffer;
  size_t size;
};

fit::result<Status, AllocateResult> AllocateBuffer(size_t size) {
  if (size == 0) {
    return fit::error(Status::InvalidArgs);
  }
  if (size > kMaxSize) {
    size = kMaxSize;
  }

  fbl::AllocChecker checker;
  std::unique_ptr<uint8_t[]> buffer{new (&checker) uint8_t[size]};
  if (!checker.check()) {
    return fit::error(Status::NoMemory);
  }

  return fit::ok(AllocateResult{std::move(buffer), size});
}