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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ int main() {

With this example, we notice that not all functions need to access the underlying `int` element and operate entirely on `even`. So we validate and convert the `int` element to `even` at the very beginning and only fall back to `int` at the very last moment, when we actually need it. Both operations should ideally at the edges of our applications.

Although we reported errors via `std::optional` in the example, it's possible to customise it, e.g. to throw an exception.

## Requirements

C++20
Expand Down
63 changes: 58 additions & 5 deletions include/rvarago/refined.hpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,60 @@
#include <concepts>
#include <exception>
#include <functional>
#include <optional>
#include <utility>

namespace rvarago::refined {

// Error policies for the fallible refinements factory.
namespace error {

// Models the error reporting mechanism.
template <typename Policy, typename Refinement>
concept policy = requires(Policy p, Refinement refined) {
typename Refinement::value_type;
typename Refinement::predicate_type;

// On success.
{ p.ok(refined) } -> std::same_as<typename Policy::return_type>;

// On error.
{ p.err() } -> std::same_as<typename Policy::return_type>;
};

// Report errors as `std::optional<Refinement>`.
template <typename Refinement> struct to_optional {
using return_type = std::optional<Refinement>;

// Returns an engaged optional.
constexpr auto ok(Refinement refinement) const -> return_type {
return std::optional{std::move(refinement)};
}

// Returns nullopt.
constexpr auto err() const -> return_type { return std::nullopt; }
};

// Report errors as exceptions.
template <typename Refinement> struct to_exception {
using return_type = Refinement;

struct refinement_exception : std::exception {
const char *what() const noexcept override {
return "fail to refine argument due to unsastified predicate";
}
};

// Returns argument unchanged.
constexpr auto ok(Refinement refinement) const -> return_type {
return refinement;
}

// Throws a `refinement_exception`.
constexpr auto err() const -> return_type { throw refinement_exception{}; }
};
} // namespace error

// `refinement<T, Pred>` constraints values `t: T` where `Pred{}(t)` holds.
template <typename T, std::predicate<T const &> Pred> struct refinement {
using value_type = T;
Expand All @@ -15,13 +65,16 @@ template <typename T, std::predicate<T const &> Pred> struct refinement {

// `make(value, pred)` is the only factory to refinements.
//
// It returns an engaged optional when `pred(value)` holds.
static constexpr auto make(T value, Pred pred = {})
-> std::optional<refinement<T, Pred>> {
// If `pred(value)` holds, then produces a valid instance by delegating to
// `policy.ok`. Else reports error via `policy.err`.
template <error::policy<refinement<T, Pred>> Policy =
error::to_optional<refinement<T, Pred>>>
static constexpr auto make(T value, Pred pred = {}, Policy policy = {})
-> Policy::return_type {
if (std::invoke(std::move(pred), value)) {
return std::optional{refinement<T, Pred>{std::move(value)}};
return policy.ok({refinement<T, Pred>{std::move(value)}});
} else {
return std::nullopt;
return policy.err();
}
}

Expand Down
27 changes: 23 additions & 4 deletions test/refined_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,35 @@

#include <rvarago/refined.hpp>

using namespace rvarago::refined;
namespace refined = rvarago::refined;

using even =
refined::refinement<int, decltype([](auto const x) { return x % 2 == 0; })>;

TEST_CASE(
"A refinement type must admit only instance that satisfy its predicate",
"[make]") {
using even =
refinement<int, decltype([](auto const x) { return x % 2 == 0; })>;
"[make][default_policy]") {

STATIC_REQUIRE(std::is_same_v<decltype(even::make(0)), std::optional<even>>);

STATIC_REQUIRE(
std::is_same_v<decltype(even::make<refined::error::to_optional<even>>(0)),
std::optional<even>>);

STATIC_REQUIRE(even::make(0)->value == 0);
STATIC_REQUIRE(even::make(1) == std::nullopt);
STATIC_REQUIRE(even::make(2)->value == 2);
STATIC_REQUIRE(even::make(3) == std::nullopt);
}

TEST_CASE("A to_exception policy should throw on invalid argument",
"[error_policy][to_exception]") {

STATIC_REQUIRE(
std::is_same_v<
decltype(even::make<refined::error::to_exception<even>>(10)), even>);

STATIC_REQUIRE(even::make<refined::error::to_exception<even>>(0).value == 0);
REQUIRE_THROWS_AS(even::make<refined::error::to_exception<even>>(1),
refined::error::to_exception<even>::refinement_exception);
}