Skip to content

A minimal std::expected<T, E> #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
BasedOnStyle: Google
---
Language: Cpp
DerivePointerAlignment: false
PointerAlignment: Left
30 changes: 30 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
cmake_minimum_required(VERSION 3.13)

enable_testing()
find_package(GTest MODULE REQUIRED)

# Include directories accessible from here.
include_directories(.)

# Require C++17 with no extensions.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Add the test to the std::expected implementation.
add_executable(expected_test netlib/expected_test.cc)
target_link_libraries(expected_test PRIVATE GTest::GTest GTest::Main)
add_test(expected_test expected_test)
29 changes: 29 additions & 0 deletions netlib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# netlib Directory

This is the main source directory for the project. The intent is to keep this
directory flat with subdirectories for logical groupings. This means all the
headers, implementation, and test code should be co-hosted in this directory.

## Structure

All implementation files must end with the `.cc` filename extension, and all
headers must end in `.h`. If we have a file named `connection.cc` the header
must be `connection.h` and the test(s) should be in `connection_test.cc`.

We shall control the installed headers through our CMake configuration
instead of assuming that all headers are publicly accessible. When including
files, we should always include headers in the netlib repository by relative
inclusion with the `netlib/` directory (based off the root of the
repository). As an example:

```c++
// In connection.cc and connection_test.cc.
#include "netlib/connection.h"
```

## Subdirectories

We can introduce subdirectories for logical grouping, each one following the
same structure rules as described here. For instance, if we have a
subdirectory of encoding/decoding, we can introduce a `coding/` subdirectory
with all the encoders and decoders implemented.
474 changes: 474 additions & 0 deletions netlib/expected.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,474 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef CPP_NETLIB_EXPECTED_H_
#define CPP_NETLIB_EXPECTED_H_

#include <cassert>
#include <type_traits>
#include <utility>

namespace cppnetlib {

/// This is a minimal implementation of the proposed P0323R3
/// std::unexpected<...> API.
template <class E>
// requires EqualityComparable<E> && (CopyConstructible<E> ||
// MoveConstructible<E>)
class unexpected {
public:
static_assert(!std::is_same_v<E, void>, "unexpected<void> is not supported.");

unexpected() = delete;
constexpr explicit unexpected(const E& e) : value_(e) {}
constexpr explicit unexpected(E&& e) : value_(std::move(e)) {}
constexpr const E& value() const& { return value_; }
constexpr E& value() & { return value_; }
constexpr E&& value() && { return std::move(value_); }
constexpr const E&& value() const&& { return std::move(value_); }

// This is a deviation from the proposal which suggests that these functions
// are namespace-level functions, instead of ADL-only found comparison
// operators (defined friend free functions).
template <class F>
friend constexpr bool operator==(const unexpected<F>& lhs,
const unexpected<F>& rhs) {
return lhs.value_ == rhs.value_;
}

template <class F>
friend constexpr bool operator!=(const unexpected<F>& lhs,
const unexpected<F>& rhs) {
return lhs.value_ == rhs.value_;
}

private:
E value_;
};

struct unexpect_t {
unexpect_t() = default;
};
inline constexpr unexpect_t unexpect{};

template <class E>
class bad_expected_access;

template <>
class bad_expected_access<void> {
public:
virtual const char* what() const noexcept {
return "cppnetlib::bad_expected_access";
}
virtual ~bad_expected_access() {}
};

template <class E>
class bad_expected_access : public bad_expected_access<void> {
public:
explicit bad_expected_access(E e) : val_(e) {}
const char* what() const noexcept override {
return "cppnetlib::bad_expected_access<E>";
}
const E& error() const&;
E& error() &;
E&& error() &&;

private:
E val_;
};

/// This is a minimal implementation of the proposed P0323R3
/// std::expected<...> API.
// TODO: Implement a specilisation of expected<void, E>.
// TODO: Implement more member functions as needed.
template <class T, class E>
class [[nodiscard]] expected {
public:
using value_type = T;
using error_type = E;
using unexpected_type = unexpected<E>;

template <class U>
struct rebind {
using type = expected<U, error_type>;
};

constexpr expected() : has_value_(false) {
new (&union_storage_) T();
has_value_ = true;
}
constexpr expected(const expected& other)
: union_storage_(other.union_storage_), has_value_(other.has_value_) {}

constexpr expected(expected && other,
std::enable_if_t < std::is_move_constructible_v<T> &&
std::is_move_constructible_v<E>> * =
0) noexcept(std::is_nothrow_move_constructible_v<T> &&
std::is_nothrow_move_constructible_v<E>)
: has_value_(other.has_value_) {
if (other.has_value_)
new (union_storage_)
T(std::move(*reinterpret_cast<T*>(other.union_storage_)));
else
new (union_storage_)
unexpected<E>(std::move(*reinterpret_cast<E*>(other.union_storage_)));
}

private:
template <class U, class G>
using constructor_helper_t =
std::enable_if_t<std::is_constructible_v<T, const U&> and
std::is_constructible_v<E, const G&> and
!std::is_constructible_v<T, expected<U, G>&> and
!std::is_constructible_v<T, expected<U, G>&&> and
!std::is_constructible_v<T, const expected<U, G>&> and
!std::is_constructible_v<T, const expected<U, G>&&> and
!std::is_convertible_v<expected<U, G>&, T> and
!std::is_convertible_v<expected<U, G>&&, T> and
!std::is_convertible_v<const expected<U, G>&, T> and
!std::is_convertible_v<const expected<U, G>&&, T>>;

public:
// TODO: c++20 has support for conditional explicit, use that instead of the
// duplication.
template <class U, class G,
std::enable_if_t<!(std::is_convertible_v<const U&, T> and
std::is_convertible_v<const G&, E>),
bool> = false>
explicit constexpr expected(const expected<U, G>& other,
constructor_helper_t<U, G>* = 0)
: has_value_(other.has_value_) {
if (other.has_value_)
new (&union_storage_) T(*other);
else
new (&union_storage_) unexpected<E>(other.error());
}

template <class U, class G,
std::enable_if_t<(std::is_convertible_v<const U&, T> and
std::is_convertible_v<const G&, E>),
bool> = false>
constexpr expected(const expected<U, G>& other,
constructor_helper_t<U, G>* = 0)
: has_value_(other.has_value_) {
if (other.has_value_)
new (&union_storage_) T(*other);
else
new (&union_storage_) unexpected<E>(other.error());
}

template <class U, class G,
std::enable_if_t<!(std::is_convertible_v<const U&, T> and
std::is_convertible_v<const G&, E>),
bool> = false>
explicit constexpr expected(const expected<U, G>&& other,
constructor_helper_t<U, G>* = 0)
: has_value_(other.has_value_) {
if (other.has_value_)
new (&union_storage_) T(std::move(*other));
else
new (&union_storage_) unexpected<E>(std::move(unexpected(other.error())));
}

template <class U = T,
std::enable_if_t<!std::is_convertible_v<U&&, T>, bool> = false>
explicit constexpr expected(
U && value,
std::enable_if_t<std::is_constructible_v<T, U&&> and
!std::is_same_v<std::decay_t<U>, std::in_place_t> and
!std::is_same_v<expected<T, E>, std::decay_t<U>> and
!std::is_same_v<unexpected<E>, std::decay_t<U>>>* = 0)
: has_value_(true) {
new (&union_storage_) U(std::forward<U>(std::move(value)));
}

template <class U = T,
std::enable_if_t<std::is_convertible_v<U&&, T>, bool> = false>
constexpr expected(
U && value,
std::enable_if_t<std::is_constructible_v<T, U&&> and
!std::is_same_v<std::decay_t<U>, std::in_place_t> and
!std::is_same_v<expected<T, E>, std::decay_t<U>> and
!std::is_same_v<unexpected<E>, std::decay_t<U>>>* = 0)
: has_value_(true) {
new (&union_storage_) U(std::forward<U>(value));
}

template <class G = E,
std::enable_if_t<!std::is_convertible_v<const G&, E>, bool> = false>
explicit constexpr expected(unexpected<G> const& e) : has_value_(false) {
new (&union_storage_) unexpected<E>(e);
}

template <class G = E,
std::enable_if_t<std::is_convertible_v<const G&, E>, bool> = false>
constexpr expected(unexpected<G> const& e) : has_value_(false) {
new (&union_storage_) unexpected<E>(e);
}

template <class G = E,
std::enable_if_t<!std::is_convertible_v<G&&, E>, bool> = false>
explicit constexpr expected(unexpected<G> && e) noexcept(
std::is_nothrow_move_constructible_v<E, G&&>)
: has_value_(false) {
new (&union_storage_) unexpected<E>(std::move(e.value()));
}

template <class G = E,
std::enable_if_t<std::is_convertible_v<G&&, E>, bool> = false>
constexpr expected(unexpected<G> &&
e) noexcept(std::is_nothrow_constructible_v<E, G&&>)
: has_value_(false) {
new (&union_storage_) unexpected<E>(std::move(e.value()));
}

// Destructor.
~expected() {
if constexpr (!std::is_trivially_destructible_v<T>) {
if (has_value_)
reinterpret_cast<T*>(&union_storage_)->~T();
else
reinterpret_cast<unexpected_type*>(&union_storage_)->~unexpected_type();
}
}

// Assignment.
expected& operator=(const expected& other) {
if (has_value_ and other.has_value_) {
**this = *other;
return *this;
}

if (!has_value_ and !other.has_value_) {
(*reinterpret_cast<unexpected_type*>(&union_storage_)) =
unexpected(other.error());
return *this;
}

if (has_value_ and !other.has_value_) {
reinterpret_cast<T*>(&union_storage_)->~T();
new (&union_storage_) unexpected_type(unexpected(other.error()));
has_value_ = false;
return *this;
}

assert(!has_value_ and other.has_value_);
if constexpr (std::is_nothrow_copy_constructible_v<T>) {
reinterpret_cast<unexpected_type*>(&union_storage_)->~unexpected_type();
new (&union_storage_) T(other.value());
has_value_ = true;
} else if constexpr (std::is_nothrow_move_constructible_v<T>) {
T tmp = *other;
reinterpret_cast<unexpected_type*>(&union_storage_)->~unexpected_type();
new (&union_storage_) T(tmp);
has_value_ = true;
} else if constexpr (std::is_nothrow_move_constructible_v<E>) {
unexpected_type tmp = unexpected(std::move(error()));
reinterpret_cast<unexpected_type*>(&union_storage_)->~unexpected_type();
try {
new (&union_storage_) T(*other);
has_value_ = true;
} catch (...) {
new (&union_storage_) unexpected_type(std::move(tmp));
throw;
}
}

return *this;
}

expected& operator=(expected&& other) noexcept(
std::is_nothrow_move_assignable_v<T> and
std::is_nothrow_move_constructible_v<T>) {
if (has_value_ and other.has_value_) {
**this = std::move(*other);
return *this;
}

if (!has_value_ and !other.has_value_) {
(*reinterpret_cast<unexpected_type*>(&union_storage_)) =
unexpected(std::move(other.error()));
return *this;
}

if (has_value_ and !other.has_value_) {
reinterpret_cast<T*>(&union_storage_)->~T();
new (&union_storage_) unexpected_type(
std::move(std::forward<expected<T, E>>(other).error()));
has_value_ = false;
return *this;
}

assert(!has_value_ and other.has_value_);
if constexpr (std::is_nothrow_move_constructible_v<T>) {
reinterpret_cast<T*>(&union_storage_)->~T();
new (&union_storage_) T(*std::move(other));
has_value_ = true;
} else if constexpr (std::is_nothrow_move_constructible_v<E>()) {
unexpected_type tmp = unexpected(std::move(error()));
reinterpret_cast<unexpected_type*>(&union_storage_)->~unexpected_type();
try {
new (&union_storage_) T(*std::move(other));
has_value_ = true;
} catch (...) {
new (&union_storage_) unexpected_type(std::move(tmp));
throw;
}
}

return *this;
}

template <std::enable_if_t<std::is_nothrow_copy_constructible_v<E> and
std::is_assignable_v<E&, E>,
bool> = false>
expected& operator=(const unexpected<E>& e) noexcept(
std::is_nothrow_copy_assignable_v<unexpected_type> and
std::is_nothrow_copy_constructible_v<unexpected_type>) {
if (!has_value_) {
(*reinterpret_cast<unexpected_type*>(&union_storage_)) = e;
return *this;
}

reinterpret_cast<T*>(&union_storage_)->~T();
new (&union_storage_)
unexpected_type(unexpected(std::forward<unexpected_type>(e)));
has_value_ = false;
return *this;
}

template <std::enable_if_t<std::is_nothrow_move_constructible_v<E> and
std::is_nothrow_move_assignable_v<E>,
bool> = false>
expected& operator=(unexpected<E>&& e) noexcept(
std::is_nothrow_move_assignable_v<unexpected_type>) {
if (!has_value_) {
(*reinterpret_cast<unexpected_type*>(&union_storage_)) =
unexpected(std::move(std::forward<unexpected<E>>(e)));
return *this;
}

reinterpret_cast<T*>(&union_storage_)->~T();
new (&union_storage_)
unexpected_type(unexpected(std::forward<unexpected_type>(e)));
has_value_ = false;
return *this;
}

template <
class U,
std::enable_if_t<
!std::is_same_v<expected<T, E>, std::decay_t<U>> and
!std::conjunction_v<std::is_scalar<T>,
std::is_same<T, std::decay_t<U>>> and
std::is_constructible_v<T, U> and std::is_assignable_v<T&, U> and
std::is_nothrow_move_constructible_v<E>,
bool> = false>
expected& operator=(U&& value) {
if (has_value_) {
this->value() = std::forward<U>(value);
return *this;
}

if constexpr (std::is_nothrow_constructible_v<T, U&&>) {
reinterpret_cast<unexpected_type*>(&union_storage_)->~unexpected_type();
new (&union_storage_) T(std::forward<U>(value));
has_value_ = true;
} else if constexpr (std::is_nothrow_constructible_v<E, U&&>) {
unexpected_type tmp(std::move(error()));
reinterpret_cast<unexpected_type*>(&union_storage_)->~unexpected_type();
try {
new (&union_storage_) T(std::forward<U>(value));
has_value_ = true;
} catch (...) {
new (&union_storage_) unexpected_type(std::move(tmp));
throw;
}
}
return *this;
}

// Observer functions.
constexpr const T* operator->() const {
if (!has_value_) throw bad_expected_access<E>(error());
return reinterpret_cast<const T*>(&union_storage_);
}

constexpr T* operator->() {
if (!has_value_) throw bad_expected_access<E>(error());
return reinterpret_cast<T*>(&union_storage_);
}

constexpr const T& operator*() const& {
if (!has_value_) throw bad_expected_access<E>(error());
return *reinterpret_cast<const T*>(&union_storage_);
}

constexpr T& operator*()& {
if (!has_value_) throw bad_expected_access<E>(error());
return *reinterpret_cast<T*>(&union_storage_);
}

constexpr const T&& operator*() const&& {
if (!has_value_) throw bad_expected_access<E>(error());
return std::move(*reinterpret_cast<const T*>(&union_storage_));
}

constexpr T&& operator*()&& {
if (!has_value_) throw bad_expected_access<E>(error());
return std::move(*reinterpret_cast<T*>(&union_storage_));
}

constexpr explicit operator bool() const noexcept { return has_value_; }
constexpr bool has_value() const noexcept { return has_value_; }

constexpr const T& value() const& { return **this; }
constexpr T& value()& { return **this; }
constexpr const T&& value() const&& { return std::move(**this); }
constexpr T&& value()&& { return std::move(**this); }

constexpr const E& error() const& {
assert(!has_value_ &&
"expected<T, E> must not have a value when taking an error!");
return reinterpret_cast<const unexpected<E>*>(&union_storage_)->value();
}

constexpr E& error()& {
assert(!has_value_ &&
"expected<T, E> must not have a value when taking an error!");
return reinterpret_cast<unexpected<E>*>(&union_storage_)->value();
}

constexpr const E&& error() const&& {
assert(!has_value_ &&
"expected<T, E> must not have a value when taking an error!");
return std::move(*reinterpret_cast<const unexpected<E>*>(&union_storage_))
->value();
}

constexpr E&& error()&& {
assert(!has_value_ &&
"expected<T, E> must not have a value when taking an error!");
return std::move(*reinterpret_cast<unexpected<E>*>(&union_storage_))
.value();
};

private:
std::aligned_union_t<8, value_type, unexpected_type> union_storage_;
bool has_value_;
};

} // namespace cppnetlib

#endif // CPP_NETLIB_EXPECTED_H_
102 changes: 102 additions & 0 deletions netlib/expected_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "netlib/expected.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

namespace cppnetlib {
namespace {

using ::testing::Eq;

using error_code = int;

enum errors : error_code { undefined };

expected<bool, error_code> f(bool b) {
if (b) return true;
return unexpected(errors::undefined);
}

// For testing support for larger types.
struct test_data {
int64_t first;
int64_t second;
};

expected<test_data, error_code> g(bool b, int64_t first, int64_t second) {
if (b) return unexpected{errors::undefined};
return test_data{first, second};
}

TEST(ExpectedTest, Construction) { expected<bool, error_code> e; }

TEST(ExpectedTest, Unexpected) {
auto e1 = f(true);
ASSERT_TRUE(e1);

auto e2 = f(false);
ASSERT_FALSE(e2);
ASSERT_THROW(*e2, bad_expected_access<error_code>);

auto e3 = g(true, 1, 2);
ASSERT_FALSE(e3);
EXPECT_THAT(e3.error(), Eq(errors::undefined));
ASSERT_THROW(*e3, bad_expected_access<error_code>);

e3 = g(false, 2, 3);
ASSERT_TRUE(e3);
ASSERT_THAT(e3->first, Eq(2));
ASSERT_THAT(e3->second, Eq(3));
}

TEST(ExpectedTest, AssignmentSimple) {
expected<int, error_code> e;
ASSERT_NO_THROW(e = 1);
EXPECT_THAT(e.value(), Eq(1));
ASSERT_NO_THROW(e = {});
EXPECT_THAT(e.value(), Eq(int{}));
ASSERT_NO_THROW(e = unexpected{errors::undefined});
ASSERT_THROW(*e, bad_expected_access<error_code>);
EXPECT_THAT(e.error(), Eq(errors::undefined));
}

struct throwing_copy_exception : public std::exception {
const char* what() const noexcept override {
return "throwing copy exception";
}
};

struct throwing_copy {
throwing_copy() = default;
throwing_copy(const throwing_copy& e) { throw throwing_copy_exception(); }
throwing_copy& operator=(const throwing_copy&) {
throw throwing_copy_exception();
}
throwing_copy(throwing_copy&&) = default;
throwing_copy& operator=(throwing_copy&&) = default;
~throwing_copy() = default;
};

TEST(ExpectedTest, AssignmentThrowingCopy) {
expected<throwing_copy, error_code> instance;
ASSERT_NO_THROW(instance = throwing_copy{});
throwing_copy tmp;
ASSERT_NO_THROW(instance = std::move(tmp));
ASSERT_THROW(instance = tmp, throwing_copy_exception);
ASSERT_THROW(*instance = tmp, throwing_copy_exception);
}

} // namespace
} // namespace cppnetlib