Оператор перегрузки в шаблонном классе - PullRequest
3 голосов
/ 05 июня 2019

Здесь приведено определение класса Result, целью которого является симуляция логики монады Either из Haskell (Left - Failure; Right - Success).

#include <string>
#include <functional>
#include <iostream>

template <typename S, typename F>
class result
{
  private:
    S succ;
    F fail;
    bool pick;

  public:
    /// Chain results of two computations.
    template <typename T>
    result<T,F> operator&&(result<T,F> _res) {
      if (pick == true) {
        return _res;
      } else {
        return failure(fail);
      }
    }

    /// Chain two computations.
    template <typename T>
    result<T,F> operator>>=(std::function<result<T,F>(S)> func) {
      if (pick == true) {
        return func(succ);
      } else {
        return failure(fail);
      }
    }

    /// Create a result that represents success.
    static result success(S _succ) {
      result res;
      res.succ = _succ;
      res.pick = true;

      return res;
    }

    /// Create a result that represents failure.
    static result failure(F _fail) {
      result res;
      res.fail = _fail;
      res.pick = false;

      return res;
    }
};

При попытке составить два результата с помощью оператора && все хорошо:

int
main(int argc, char* argv[])
{
  // Works!
  auto res1 = result<int, std::string>::success(2);
  auto res2 = result<int, std::string>::success(3);
  auto res3 = res1 && res2;
}

Но при попытке связать вычисления поверх результата появляется ошибка компиляции:

result<int, std::string>
triple(int val)
{
  if (val < 100) {
    return result<int, std::string>::success(val * 3);
  } else {
    return result<int, std::string>::failure("can't go over 100!");
  }
}

int
main(int argc, char* argv[])
{
  // Does not compile!
  auto res4 = result<int, std::string>::success(2);
  auto res5a = res4 >>= triple;
  auto res5b = res4 >>= triple >>= triple;
}

Ошибка от clang++ выглядит следующим образом:

minimal.cpp:82:21: error: no viable overloaded '>>='
  auto res5a = res4 >>= triple;
               ~~~~ ^   ~~~~~~
minimal.cpp:26:17: note: candidate template ignored: could not match
      'function<result<type-parameter-0-0, std::__1::basic_string<char,
      std::__1::char_traits<char>, std::__1::allocator<char> > > (int)>' against
      'result<int, std::__1::basic_string<char, std::__1::char_traits<char>,
      std::__1::allocator<char> > > (*)(int)'
    result<T,F> operator>>=(std::function<result<T,F>(S)> func) {
                ^
minimal.cpp:83:32: error: invalid operands to binary expression ('result<int,
      std::string> (int)' and 'result<int, std::string> (*)(int)')
  auto res5b = res4 >>= triple >>= triple;

Есть идеи, как решить эту проблему?

Ответы [ 3 ]

4 голосов
/ 05 июня 2019

Это работает

auto f = std::function< result<int, std::string>(int)>(triple);
auto res5a = res4 >>= f;

Я не могу дать хорошее краткое объяснение, только так: вычет типа не учитывает преобразования, и triple является result<int,std::string>()(int), а не std::function.

Вам не нужно использовать std::function, но вы можете принять любое вызываемое что-то вроде:

template <typename G>
auto operator>>=(G func) -> decltype(func(std::declval<S>())) {
    if (pick == true) {
        return func(succ);
    } else {
        return failure(fail);
    }
}

Демонстрационная версия

Обратите внимание, что std::function идет с некоторыми накладными расходами. Он использует стирание типа, чтобы иметь возможность хранить все виды вызываемых объектов. Если вы хотите передать только один отзыв, нет необходимости оплачивать эту стоимость.

Для второй строки @ Комментарий Иксисарвинена уже суммирует его. Ради полноты я просто процитирую это здесь

auto res5b = res4 >>= triple >>= triple; не будет работать без дополнительный оператор для двух указателей на функции или явных скобок около res4 >>= triple, потому что operator >>= справа налево. Сначала он попытается применить >>= к тройному и тройному.

PS: я не знаю ни того, ни другого, и ваш код немного более функциональный, чем тот, к которому я привык, может быть, вы можете получить подобное из std::conditional?

2 голосов
/ 06 июня 2019

Итак, в C ++ std::function не является базовым классом для чего-либо интересного.Вы не можете вывести тип std::function из функции или лямбды.

Так что ваш:

/// Chain two computations.
template <typename T>
result<T,F> operator>>=(std::function<result<T,F>(S)> func)

будет выводить только при передаче фактического std::function.

Теперь, что вы на самом деле имеете в виду, это «что-то, что принимает S и возвращает result<T,F> для некоторого типа T».

Это не так, как вы говорите в C ++.

Как уже отмечалось, >>= является правоассоциативным.Вместо этого я мог бы предложить ->*, то есть слева направо.

Во-вторых, ваша failure статическая функция не будет работать правильно, так как она часто возвращает неправильный тип.

template<class F>
struct failure {
  F t;
};
template<class F>
failure(F)->failure{F};

затем добавьте конструктор, взяв failure<F>.

/// Chain two computations.
template<class Self, class Rhs,
  std::enable_if_t<std::is_same<result, std::decay_t<Self>>{}, bool> = true
>
auto operator->*( Self&& self, Rhs&& rhs )
-> decltype( std::declval<Rhs>()( std::declval<Self>().succ ) )
{
  if (self.pick == true) {
    return std::forward<Rhs>(rhs)(std::forward<Self>(self).succ);
  } else {
    return failure{std::forward<Self>(self).fail};
  }
}

Теперь я тщательно обращаю внимание на r / lvalue всех типов и буду двигаться, если это возможно.

template<class F>
struct failure {
    F f;
};
template<class F>
failure(F&&)->failure<std::decay_t<F>>;

template<class S>
struct success {
    S s;
};
template<class S>
success(S&&)->success<std::decay_t<S>>;


template <class S, class F>
class result
{
  private:
    std::variant<S, F> state;

  public:
    bool successful() const {
      return state.index() == 0;
    }

    template<class Self,
        std::enable_if_t< std::is_same<result, std::decay_t<Self>>{}, bool> = true
    >
    friend decltype(auto) s( Self&& self ) {
        return std::get<0>(std::forward<Self>(self).state);
    }
    template<class Self,
        std::enable_if_t< std::is_same<result, std::decay_t<Self>>{}, bool> = true
    >
    friend decltype(auto) f( Self&& self ) {
        return std::get<1>(std::forward<Self>(self).state);
    }

    /// Chain results of two computations.
    template<class Self, class Rhs,
        std::enable_if_t< std::is_same<result, std::decay_t<Self>>{}, bool> = true
    >
    friend std::decay_t<Rhs> operator&&(Self&& self, Rhs&& rhs) {
      if (self.successful()) {
        return success{s(std::forward<Rhs>(rhs))};
      } else {
        return failure{f(std::forward<Self>(self))};
      }
    }

    /// Chain two computations.
    template<class Self, class Rhs,
        std::enable_if_t< std::is_same<result, std::decay_t<Self>>{}, bool> = true
    >        
    friend auto operator->*(Self&&self, Rhs&& rhs)
    -> decltype( std::declval<Rhs>()( s( std::declval<Self>() ) ) )
    {
      if (self.successful()) {
        return std::forward<Rhs>(rhs)(s(std::forward<Self>(self)));
      } else {
        return failure{f(std::forward<Self>(self))};
      }
    }

    template<class T>
    result( success<T> s ):
      state(std::forward<T>(s.s))
    {}
    template<class T>
    result( failure<T> f ):
      state(std::forward<T>(f.f))
    {}
    explicit operator bool() const { return successful(); }
};

живой пример .

Использует .

1 голос
/ 06 июня 2019

Чистая и эффективная реализация 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;
}

Вы можете поиграть с кодом здесь!

...