Zen C++ Libraries
Zero-dependency re-usable components for C++
Loading...
Searching...
No Matches
either.hpp File Reference

Encapsulation for computations that may fail. More...

#include <condition_variable>
#include <type_traits>
#include <utility>
#include "zen/config.hpp"
#include "zen/formatting.hpp"

Go to the source code of this file.

Classes

struct  dummy
class  either< L, R >
 A type for computations that may fail. More...

Macros

#define ZEN_TRY(value)
 Return a left-valued immediately so only a right-valued either type remains.
#define ZEN_TRY_DISCARD(expr)
 The same as ZEN_TRY but the expression is immediately dropped.

Functions

left_t< void > left ()
template<typename L>
left_t< L & > left (L &value)
template<typename L>
left_t< L > left (L &&value)
right_t< void > right ()
template<typename R>
right_t< R & > right (R &value)
template<typename R>
right_t< R > right (R &&value)

Detailed Description

Encapsulation for computations that may fail.

A common idiom is to use the type defined in this header on functions that can fail, as an alternatve to exception handling. Some hold that this is a good practice for several reasons:

  • Absence of throw-statements may allow compilers to better reason about your program, possibly resulting in faster code.
  • Consumers of your API know immediately that a function might fail, and have to deal with it explicitly.
  • Because the exception is encoded in the type, some bugs can be captured at compile-time that might otherwise be more subtle.

Working With Computations That May Fail

Often, you find yourself interfacing with external systems, such as a network service or the file system. Doing operations on these objects can result in failures, e.g. an ENOENT returned from a call to stat().

In C, it is very common to store the actual result in one of the function's parameters and return an error code, like so:

int read_some(const char* filename, char* output) {
int fd, error;
fd = open(in, O_RDONLY);
if (fd < 0) {
return -1;
}
error = read(fd, output, 4);
if (error < 0) {
close(fd);
return -1;
}
return 0;
}

In C++ another common idiom is returning a nullptr whenever a heap-allocated object could not be created. These approaches have obvious drawbacks. In the case of returning an error code instead of the result, we have to make sure our variable can be kept as a reference, leading to more code.

The generic solution to this problem is to introduce a new type, called either, that can hold both a result and an error code, without wasting precious memory. This is exactly what zen::either<L, R> was made for.

either<int, std::string> writeSome(std::string filename) {
int fd = open(in, O_RDONLY);
if (fd < 0) {
return zen::left(-1)
}
char buf[4];
read(fd, buf, 4);
return zen::right(std::string(output, 4));
}
A type for computations that may fail.
Definition either.hpp:124

We can further improve upon our code snippet by declaring an enum that lists all possible errors that might occur. The errors might even be full classes using virtual inheritance; something which we'll see later on.

enum class Error {
OpenFailed,
ReadFailed,
}
either<int, std::string> writeSome(std::string filename) {
int fd = open(in, O_RDONLY);
if (fd < 0) {
return zen::left(Error::OpenFailed)
}
char buf[4];
if (read(fd, buf, 4) < 0) {
return zen::left(Error::ReadFailed)
}
return zen::right(std::string(output, 4));
}

Finally, we encapsulate our error type in a custom Result-type that will be used thoughout our application:

template<typename T>
using Result = zen::either<Error, T>;

That's it! You've learned how to write simple C++ code the Zen way!

Macro Definition Documentation

◆ ZEN_TRY

#define ZEN_TRY ( value)
Value:
if (value.is_left()) { \
return ::zen::left(std::move(value).take_left()); \
}
Definition value.hpp:34

Return a left-valued immediately so only a right-valued either type remains.

The remaining value can be safely unwrapped.

Examples

zen::either<std::string, std::vector<char32_t>> decode_utf8_string(const std::string_view& str) {
std::vector<char32_t> out;
const unsigned char* iter = reinterpret_cast<const unsigned char*>(str.data());
const unsigned char* end = iter + str.size();
for (; iter != end;) {
auto result = decode_utf8_char(iter);
ZEN_TRY(result);
out.push_back(*result); // may now dereference the result
}
return zen::right(std::move(out));
}

◆ ZEN_TRY_DISCARD

#define ZEN_TRY_DISCARD ( expr)
Value:
{ \
auto zen__either__result = (expr); \
if (zen__either__result.is_left()) { \
return ::zen::left(std::move(zen__either__result.left())); \
} \
}

The same as ZEN_TRY but the expression is immediately dropped.

Function Documentation

◆ left() [1/3]

left_t< void > left ( )
inline

Construct a left-valued either type that has no contents.

Usually, this means that the computation failed but no particular error needed to be specified.

In Rust, one would return Err(()).

◆ left() [2/3]

template<typename L>
left_t< L > left ( L && value)

Construct a left-valued either type. The provided value will be moved into the either type.

Usually, this means that a computation has failed and an error should be returned.

In Rust, one would write Err(value).

◆ left() [3/3]

template<typename L>
left_t< L & > left ( L & value)

Construct a left-valued either type. The provided value will be copied into the either type.

Usually, this means that a computation has failed and an error should be returned.

In Rust, one would write Err(value).

◆ right() [1/3]

right_t< void > right ( )
inline

Construct a right-valued either type that has no contents.

Usually, this means that the computation was successful but no particular value was generated during its run.

In Rust, one would return Ok(()).

◆ right() [2/3]

template<typename R>
right_t< R > right ( R && value)

Construct a right-valued either type. The provided value will be copied into the either type.

Usually, this means that the computation was successful and generated exactly one value.

In Rust, one would return Ok(value).

◆ right() [3/3]

template<typename R>
right_t< R & > right ( R & value)

Construct a right-valued either type. The provided value will be moved into the either type.

Usually, this means that the computation was successful and generated exactly one value.

In Rust, one would return Ok(value).