Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions doc/src/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,42 @@ title = "Home"
Outcome is a set of tools for reporting and handling function failures in contexts where using C++ exception handling is unsuitable. Such contexts include:

- programs, or parts thereof, that are compiled with exceptions disabled;

- parts of program that have a lot of branches depending on types of failures,
where if-statements are cleaner than try-catch blocks;
where if-statements are cleaner than try-catch blocks;

- requirement that failure path of execution should not cost more than the successful path of execution;

- situations, like in [`filesystem`](http://www.boost.org/doc/libs/1_64_0/libs/filesystem/doc/index.htm) library, where the decision whether the failure should be handled remotely
(use exceptions) or locally cannot be made inside the function and needs to be moved onto the caller,
and in the latter case launching stack unwinding is not desireable for the aforementioned reasons;
and in the latter case launching stack unwinding is not desirable for the aforementioned reasons;

- parts of the programs/frameworks that themselves implement exception handling and prefer
to not use exceptions to propagate failure reports across thread, tasks, fibers;

- propagating exceptions trough layers that do not implement exception throw safety;

- external requirement (such as company-wide policy) that failure handling paths are explicitly indicated in the code.
Outcome addresses failure handling through returning a special type form functions, which is able to store either a succesfully computed value (or `void`) or the information about failure. Outcome also comes with a set of idioms for dealing with such types.

Outcome addresses failure handling through returning a special type from functions, which is able to store either a successfully computed value (or `void`) or the information about failure. Outcome also comes with a set of idioms for dealing with such types.


## Sample usage

One of the tools in the Outcome library is `result<T>`: it represents either a succesfully computed value of type `T` or an `std::error_code` representing the reason for failure. You use it in the function's return type:
One of the tools in the Outcome library is `result<T>`: it represents either a successfully computed value of type `T` or an `std::error_code` representing the reason for failure. You use it in the function's return type:

{{% snippet "intro_example.cpp" "signature" %}}

It is possible to inspect the state manualy:
It is possible to inspect the state manually:

{{% snippet "intro_example.cpp" "inspect" %}}

Or, if this function is called in another function that also returns `result<T>` you can use a dedicated control statement:

{{% snippet "intro_example.cpp" "implementation" %}}

`OUTCOME_TRY` is a control statement. If the returned `result<T>` contains an error information, the enclosing function is immediatelly returned with `result<U>` containing the same failure information; otherwise object of type `T` is move-constructed on the stack.
`OUTCOME_TRY` is a control statement. If the returned `result<T>` object contains an error information, the enclosing function is immediately returned with `result<U>` containing the same failure information; otherwise an automatic object of type `T`
is available in scope.

{{% notice note %}}
This is the v2 Outcome designed in response to feedback from a [Boost peer review held in
Expand Down
59 changes: 59 additions & 0 deletions doc/src/content/motivation/errno.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
+++
title = "errno"
description = "errno with their good and bad sides."
weight = 20
+++

## `errno`

The idiom of returning, upon failure, a special value and storing an error code
(an `int`) inside a global (or thread-local) object `errno` is inherited from C,
and used in its Standard Library:

```c++
int readValue(const char * filename)
{
FILE* f = fopen(filename, "r");
if (f == NULL)
return 0; // special value indicating failure
// keep errno value set by fopen()

int i;
int r = fscanf(f, "%d", &i);
if (r == 0 || r == EOF) { // special values: i not read
errno = ENODATA; // choose error value to return
return 0;

fclose(f);
errno = 0; // clear error info (success)
return i;
}
```

One advantage (to some and a disadvantage to others) of this technique is that it
uses familiar control statements (`if` and `return`) to indicate all execution
paths that handle failures. When we read this code we know when and under what
conditions it can exit without producing the expected result.


### Downsides


Because on failure, as well as success, we write into a global (or thread-local)
object, our functions are not *pure*: they have *side effects*. This means many
useful compiler optimizations (like common subexpression elimination) cannot be
applied. This chows that it is not only C++ that chooses suboptimal solutions
for reporting failures.

Whatever type we return, we always need a special value to spare, which is
sometimes troublesome. In the above example, if the successfully read value of
`i` is `0`, and we return it, our callers will think it is a failure even though
it is not.

Error propagation using `if` statements and early `return`s is manual. We can easily
forget to check for the failure, and incorrectly let the subsequent operations
execute, potentially causing damage to the program state.

Upon nearly each function call layer we may have to change error code value
so that it reflects the error condition adequate to the current layer. If we
do so, the original error code is gone.
51 changes: 51 additions & 0 deletions doc/src/content/motivation/error_codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
+++
title = "Error codes"
description = "Error codes with their good and bad sides."
weight = 30
+++

## Error codes

Error codes are reasonable error handling technique, also working in C.
In this case the information is also stored as an `int`, but returned by value,
which makes it possible to make functions pure (side-effect-free and referentially
transparent).

```c++
int readInt(const char * filename, int& val)
{
FILE* fd;
int r = openFile(filename, /*out*/ fd);
if (r != 0)
return r; // return whatever error openFile() returned

r = readInt(fd, /*out*/ val);
if (r != 0)
return READERRC_NOINT; // my error code

return 0; // success
}
```

Because the type of the error information (`int`) is known statically, no memory
allocation or type erasure is required. This technique is very efficient.


### Downsides

All failure paths written manually can be considered both an advantage and a
disadvantage. Forgetting to put a failure handling `if` causes bugs.

If I need to substitute an error code returned by lower-level function with mine
more appropriate at this level, the information about the original failure is
gone.

Also, all possible error codes invented by different programmers in different
third party libraries must fit into one `int` and not overlap with any other error
code value. This is quite impossible and does not scale well.

Because errors are communicated through returned values, we cannot use function's
return type to return computed values. Computed values are written to function
*output* parameters, which requires objects to be created before we have values
to put into them. This requires many objects in unintended state to exist. writing
to output parameters often requires an indirection and can incur some run-time cost.
44 changes: 40 additions & 4 deletions doc/src/content/motivation/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,47 @@ int a()
}
```

In the Standard C++ terms this means that `f()` is *sequenced before* `g()`.
In the C++ Standard terms this means that `f()` is *sequenced before* `g()`.
This makes failure handling extremely easy: in a lot of cases you do not have
to do anything.

But there is also cost to be paid when using exceptions.
Also, while next operations are being canceled, the exception object containing
the information about the initial failure is kept on the side. When at some point
the cancellation cascade is stopped by an exception handler, the exception object
can be inspected. It can contain arbitrarily big amount of data about the failure
reason, including the entire call stack.

This tutorial is in the process of being written. Once done we will explain in detail
why it is sometimes beneficial not to use exceptions to signal failures.

### Downsides

There are two kinds of overheads caused by the exception handling mechanism. The
first is connected with storing the exceptions on the side. Stack unwinding works
independently in each thread of execution; each thread can be potentially handling
a number of exceptions (even though only one exception can be active in one thread).
This requires being prepared for storing an arbitrary number of exceptions of arbitrary
types per thread. Additional things like jump tables also need to be stored in the
program binaries.

Second overhead is experienced when throwing an exception and trying to find the
handler. Since nearly any function can throw an exception of any time, this is
a dynamic memory allocation. The type of an exception is erased an a run-time type
identification (RTTI) is required to asses the type of the active exception object.
The worse time required for matching exceptions against handlers cannot be easily
predicted and therefore exceptions are not suitable for real-time or low-latency
systems.

Another problem connected with exceptions is that while they are good for program
flows with linear "success dependency", they become inconvenient in situations where
this success dependency does not occur. One such notable example is releasing acquired
resources which needs to be performed even if previous operations have failed.
Another example is when some function `c()` depends on the success of at least one
of two functions `a()` and `b()` (which try, for instance, to store user data by
two different means), another example is when implementing a strong exception safety
guarantee we may need to apply some fallback actions when previous operations have
failed. When failures are reported by exceptions, the semantics of canceling all
subsequent operations is a hindrance rather than help; these situations require special
and non-trivial idioms to be employed.

For these reasons in some projects using exceptions is forbidden. Compilers offer
switches to disable exceptions altogether (they refuse to compile a `throw`, and turn
already compiled `throw`s into calls to `std::abort()`).
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
+++
title = "Custom error codes"
title = "Plugging a library into std::error_code"
description = "Illustrates how you can hook into the std::error_code system from the Standard Library in order to work with your own set of error codes."
weight = 91
+++
Expand Down Expand Up @@ -49,8 +49,7 @@ contextual conversion to `bool` (which some people use to check if there was an
only checks for the numeric value of the error code (without looking at error domain (category)).
{{% /notice %}}

[1]: The only documentation I'm aware of is the quite old guide by Chris Kohlhoff, founder of
ASIO and the Networking TS:
[1]: The only documentation I'm aware of is the quite old guide by Chris Kohlhoff, founder of ASIO and the Networking TS:

- http://blog.think-async.com/2010/04/system-error-support-in-c0x-part-1.html
- http://blog.think-async.com/2010/04/system-error-support-in-c0x-part-2.html
Expand Down
65 changes: 65 additions & 0 deletions doc/src/content/motivation/std_error_code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
+++
title = "std::error_code"
description = "Overview of std::error_code"
weight = 40
+++

## `std::error_code`

Type `std::error_code` has been designed to be sufficiently small and trivial
to be cheaply passed around, and at the same time be able to store sufficient
information to represent any error situation from any library/sub-system in the
world without a clash. Its representation is basically:

```c++
class error_code
{
error_category* domain; // domain from which the error originates
int value; // numeric value of error within the domain
};
```

Here, `domain` indicates the library from which the error originates. It is a
pointer to a global object representing a given library/domain. Different
libraries will be represented by different pointers to different globals.
Each domain is expected to be represented by a global object derived from
`std::error_category`. The uniqueness of the domain pointer value is guaranteed
by the uniqueness of addresses of different global objects.

Now, `value` represents a numeric value of a particular error situation within
the domain. Thus, different domains can use the same numeric value `1` to
indicate different error situations, but two `std::error_code` objects will be
different because the pointers representing domains will be different.

`std::error_code` comes with additional tools: a facility for defining custom
domains with their set of error codes, and a facility for building predicates
that allow classifying errors.

Once created and passed around (either inside a thrown exception or returned from functions by value) there is never a need to change the value of `error_code`
object at any level. But at different levels one can use different predicates
for classifying error situations appropriately to the program layer.

When a new library needs to represent its own set of error situations in an
`error_code` it first has to declare the list of numeric value as an enumeration:

```c++
enum class ConvertErrc
{
StringTooLong = 1, // 0 should not represent an error
EmptyString = 2,
IllegalChar = 3,
};
```

Then it has to put some boiler-plate code to plug the new enumeration into the
`std::error_code` system. Then, it can use the enum as an `error_code`:

```c++
std::error_code ec = ConvertErrc::EmptyString;
assert(ec == ConvertErrc::EmptyString);
```

Member `value` is mapped directly from the numeric value in the enumeration, and
member `domain` is mapped from the type of the enumeration. Thus, this is a form
of type erasure, but one that does allow type `std::error_code` to be trivial
and standard-layout.
12 changes: 6 additions & 6 deletions doc/src/content/tutorial/outcome/_index.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
+++
title = "outcome<T, EC, EP>"
title = "outcome<>"
description = "Success-or-failure return types where failure can take two forms, expected/handled failure and unexpected/abort failure."
weight = 20
tags = ["outcome"]
+++

## `outcome<>`

Type {{< api "outcome/#standardese-outcome_v2_xxx__outcome-R-S-P-NoValuePolicy-" "outcome<T, EC, EP>" >}} represets either a successfully computed value of type `T` or a reason for failure. Failure can be represented by `EC` or `EP` or both. Although usually it will either be an `EC` or an `EP`. `EC` defaults to `std::error_code` and `EP` defaults to `std::exception_ptr`. The distinction is made into two types, `EC` and `EP`:
Type {{< api "outcome/#standardese-outcome_v2_xxx__outcome-R-S-P-NoValuePolicy-" "outcome<T, EC, EP, NVP>" >}} represets either a successfully computed value of type `T` or a reason for failure. Failure can be represented by `EC` or `EP` or both. Although usually it will either be an `EC` or an `EP`. `EC` defaults to `std::error_code` and `EP` defaults to `std::exception_ptr`. The distinction is made into two types, `EC` and `EP`:

- `EC` represents a failue from lower-layer function which was retured through {{< api "result/#standardese-outcome_v2_xxx__result-R-S-NoValuePolicy-" "result<T, EC>" >}}.
- `EP` represents pointer to an exception thrown in a lower-layer function to signal a failure; but at the current level we do not want to proceed with stack unwinding.


`outcome<T, EC, EP>` is useful for transporting exceptions across layers of the program that were never designed with exception safety in mind.
`outcome` is useful for transporting exceptions across layers of the program that were never designed with exception safety in mind.

Consider a program consisting of three layers:

Expand All @@ -23,7 +23,7 @@ graph BT
L2["Layer2_old"] --> L3
L1["Layer1"] --> L2
{{</mermaid>}}

The highest-level layer, `Layer3`, uses exceptions for signalling failures. The middle layer, `Layer2_old`,
was not designed with exception safety in mind and functions need to return information about failures in return value.
But down in the implementation details, in `Layer1`, another library is used that again throws exceptions. The goal is
Expand All @@ -36,9 +36,9 @@ In `Layer1` we have two functions from two libraries: one reports failures by th

In `Layer2_old` we cannot use exceptions, so its function `h` uses return type `outcome<>` to report failures. It is using functions `f` and `g` and reports their failures inside `outcome<>`:

{{% snippet "using_outcome.cpp" "def_h" %}}
{{% snippet "using_outcome.cpp" "def_h" %}}

#1. Operator `TRY` can forward failures encoded in `result<T, EC>` as `outcome<T, EC, EP>` without any loss in information. You can also use `TRY` to forward failure from one `outcome<>` to another.
#1. Operator `TRY` can forward failures encoded in `result<T, EC>` as `outcome<T, EC, EP>` without any loss in information. You can also use `TRY` to forward failure from one `outcome<>` to another.

#2. You can store the current exception through `std::exception_ptr` inside `outcome<T, EC, EP>` without any loss in information
(provided that `EP` is `std::exception_ptr`).
20 changes: 12 additions & 8 deletions doc/src/content/tutorial/result/_index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
+++
title = "result<T, EC>"
title = "result<>"
description = "Gentle introduction to writing code with simple success-or-failure return types."
weight = 10
tags = ["result", "try", "namespace"]
Expand All @@ -23,20 +23,24 @@ if it does we want to communicate the failure reason.

{{% snippet "using_result.cpp" "convert_decl" %}}

Class template {{< api "result/#standardese-outcome_v2_xxx__result-R-S-NoValuePolicy-" "result<T, EC>" >}} has two template parameters. The first (`T`) represents the type of the object
returned from the function upon success; the second (`EC`) is the type of object containing information about the reason
for failure when the function fails. A `result<T, EC>` object either stores a `T` or an `EC` at any given moment,
and is therefore conceptually similar to `variant<T, EC>`. `EC` is defaulted to `std::error_code`.
If both `T` and `EC` are trivially copyable, `result<T, EC>` is also trivially copyable.
Class template {{< api "result/#standardese-outcome_v2_xxx__result-R-S-NoValuePolicy-" "result<T, EC, NVP>" >}}
has three template parameters, but the last two have default values. The first
(`T`) represents the type of the object returned from the function upon success.
The second (`EC`) is the type of object containing information about the reason
for failure when the function fails. A result object stores either a `T` or an
`EC` at any given moment, and is therefore conceptually similar to `variant<T, EC>`. `EC` is defaulted to `std::error_code`. The third parameter (`NVP`) is called a
no-value policy. We will cover it later.

If both `T` and `EC` are trivially copyable, `result<T, EC, NVP>` is also trivially copyable.

Now, we will define an enumeration describing different failure situations during conversion.

{{% snippet "using_result.cpp" "enum" %}}

Assume we have plugged it into `std::error_code` framework, as described in [this section](../error_code).
Assume we have plugged it into `std::error_code` framework, as described in [this section](../../motivation/plug_error_code).

One notable effect of such plugging is that `ConversionErrc` is now convertible to `std::error_code`.
Now we can implement function `convert` as follows:
Now we can implement function `convert` as follows:

{{% snippet "using_result.cpp" "convert" %}}

Expand Down