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.
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:
If in doubt, leave it out. See [Justifications] below.
FIT is not intended to become a catch-all class library.
Specifically prohibited features:
FIT is not exception safe (but could be made to be in the future).
FIT's API style follows C++ standard library conventions.
In brief:
snake_case
.CamelCase
.UPPER_SNAKE_CASE
.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.
These sections explain why certain features are in FIT.
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.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.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:
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(); } }
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.
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::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(); }
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(); }
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)); }
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(); }
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()); } }
fit::result
supports several methods to access the value from a successful result.
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(); } }
*my_result
is a syntax sugar for my_result.value()
, when my_result
is a fit::result
.
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(); } }
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 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:
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)); }
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)); }
fit::result
supports ergonomic error handling and propagation.
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(); } }
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; }
fit::result
supports a variety of relational operator variants.
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(); }
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(); }
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"; }
fit::result
Use the following guidelines to make the most of the result type's ergonomic and safety features.
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);
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_; };
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:
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}); }