Чистая и эффективная реализация Result
C ++ может представлять тип Result
так же чисто и эффективно, как haskell.Как и в Haskell, в C ++ есть истинные типы сумм , и мы можем инкапсулировать всю их функциональность с помощью тегового объединения.Кроме того, используя преимущества неявной конструкции, мы можем представлять Success
и Failure
как типы вместо статических функций-членов (это делает вещи намного чище).
Определение Success
и Failure
Это действительно просто.Они просто классы-обёртки, поэтому мы можем реализовать их как агрегаты.Кроме того, используя руководства по выводу шаблонов в C ++ 17, нам не нужно указывать параметры шаблона для Failure
и Success
.Вместо этого мы просто сможем написать Success{10}
или Failure{"Bad arg"}
.
template <class F>
class Failure {
public:
F value;
};
template<class F>
Failure(F) -> Failure<F>;
template <class S>
class Success {
public:
S value;
// This allows chaining from an initial Success
template<class Fun>
auto operator>>(Fun&& func) const {
return func(value);
}
};
template <class S>
Success(S) -> Success<S>;
Определение Result
Result
является типом суммы.Это означает, что это может быть успех или неудача, но не оба.Мы можем представить это с помощью объединения, которое мы пометим с помощью was_success
bool.
template < class S, class F>
class Result {
union {
Success<S> success;
Failure<F> failure;
};
bool was_success = false; // We set this just to ensure it's in a well-defined state
public:
// Result overloads 1 through 4
Result(Success<S> const& s) : success(s), was_success(true) {}
Result(Failure<F> const& f) : failure(f), was_success(false) {}
Result(Success<S>&& s) : success(std::move(s)), was_success(true) {}
Result(Failure<F>&& f) : failure(std::move(f)), was_success(false) {}
// Result overloads 5 through 8
template<class S2>
Result(Success<S2> const& s) : success{S(s.value)}, was_success(true) {}
template<class F2>
Result(Failure<F2> const& f) : failure{F(f.value)}, was_success(false) {}
template<class S2>
Result(Success<S2>&& s) : success{S(std::move(s.value))}, was_success(true) {}
template<class F2>
Result(Failure<F2>&& f) : failure{F(std::move(f.value))}, was_success(false) {}
// Result overloads 9 through 10
Result(Result const&) = default;
Result(Result&&) = default;
template<class S2>
Result<S2, F> operator&&(Result<S2, F> const& res) {
if(was_success) {
return res;
} else {
return Failure{failure};
}
}
template<class Fun, class Ret = decltype(valueOf<Fun>()(success.value))>
auto operator>>(Fun&& func) const
-> Ret
{
if(was_success) {
return func(success.value);
} else {
return failure;
}
}
~Result() {
if(was_success) {
success.~Success<S>();
} else {
failure.~Failure<F>();
}
}
};
Объяснение Result(...)
Результат создается из успеха или неудачи.
- Перегрузки с 1 по 4 просто обрабатывают базовую копию и переносят конструкцию из
Success
и Failure
объектов; - Перегрузки 5 хотя 8 обрабатывают случай, когда мы хотим сделатьнеявное преобразование (как в случае строкового литерала в
std::string.
- Перегрузки 9 и 10, перемещение ручки и копирование конструкции
Result
.
Объяснение operator>>
Это очень похоже на вашу реализацию operator>>=
, и я объясню причины моих изменений.
template<class Fun, class Ret = decltype(valueOf<Fun>()(success.value))>
auto operator>>(Fun&& func) const
-> Ret
{
if(was_success) {
return func(success.value);
} else {
return failure;
}
}
Почему бы не использовать std::function
? std::function
- это обертка для стирания типов. Это означает, что она использует вызовы виртуальных функций изнутри, что замедляет работу. Используя неограниченный шаблон, мы значительно упрощаем компилятору оптимизацию.
Зачем использовать >>
вместо >>=
? Я использовал >>
, потому что >>=
имеет странное поведение, так как это оператор присваивания. Оператор a >>= b >>= c
на самом деле a >>= (b >>= c)
, который не являетсячто мы намеревались.
WЧто делает class Ret = decltype(valueOf<Fun>()(success.value))
do? Это определяет параметр шаблона, который по умолчанию равен типу возвращаемого значения функции, которую вы передаете.Это позволяет нам избегать использования std::function
, а также использовать лямбды.
Объяснение ~Result()
Поскольку Result
содержит объединение, мы должны вручную указать, как его уничтожить.(Классы, содержащие Result
, не должны будут делать это - как только мы укажем это внутри Result
, все будет вести себя как обычно).Это довольно просто.Если он содержит объект Success
, мы уничтожаем его.В противном случае мы уничтожим failure
один.
// Result class
~Result() {
if(was_success) {
success.~Success<S>();
} else {
failure.~Failure<F>();
}
}
Обновление вашего примера для моей реализации
Теперь, когда мы написали класс Result
, мы можем обновить ваши определения triple
и main
.
Новое определение triple
Довольно просто;мы просто заменили ваши функции success
и failure
на типы Success
и Failure
.
auto triple(int val) -> Result<int, std::string>
{
if (val < 100) {
return Success{val * 3};
} else {
return Failure{"can't go over 100"};
}
}
Новое определение main
Я добавил функцию print
, чтобы мы могли увидеть некоторые результаты.Это просто лямбда.Выполнены два вычисления: одно для ans
, а другое для ans2
.Один для ans
печатает 18, потому что triple
не увеличивает число свыше 100, а один для ans2
ничего не печатает, потому что это приводит к сбою.
int main(int argc, char* argv[])
{
auto print = [](auto value) -> Result<decltype(value), std::string> {
std::cout << "Obtained value: " << value << '\n';
return Success{value};
};
auto ans = Success{2} >> triple >> triple >> print;
auto ans2 = Success{2} >> triple >> triple >> triple >> triple >> triple >> print;
}