С ++ вариационные шаблоны: реализовать вариационный функтор - PullRequest
2 голосов
/ 01 апреля 2019

Мой коллега предоставил мне «небольшую викторину», которую он заставил своих учеников решить один раз.Кажется, что мой слабый разум просто не в состоянии постичь всю красоту современных возможностей C ++.

Subj:

Реализация функции join, принятие произвольных функторови возвращение другого функтора, который ведет себя как любой из них.Пример:

{
    auto result = std::visit(custom::join(
        [](std::string const& s) { return "it's a string"; },
        [](std::pair<int, int> const& p) { return "it's a pair"; }
    ), var);

    assert(result == "it's a string");

    var = std::make_pair(10, 20);

    auto lvalue_lambda = [](std::string const& s) { return "it's a string"; };
    result = std::visit(custom::join(
        lvalue_lambda,
        [](std::pair<int, int> const& p) { return "it's a pair"; }
    ), var);

    assert(result == "it's a pair");
}

Хорошо, после недолгих раздумий я понял, что std::variant означает «один из перечисленных», поскольку это «типобезопасный союз», поэтому японадобится кортежПопробовал что-то вроде этого:

namespace custom
{
    template<typename ...Functors>
    class ResultFunctor
    {
    public:
        ResultFunctor(Functors&&... funcs)
            : m_funcs(std::make_tuple(std::move(funcs)...))
        {}

        template<typename ...Params>
        auto operator()(Params... params) // that's where I got stuck
        {
//            return std::get<void(Params...)>(m_funcs)(params...); // No, the return type spoils this idea
            return std::get<0>(m_funcs)(params...);  // Now I need to choose the correct functor
        }

    private:
        std::tuple<Functors...> m_funcs;
    };

    template<typename ...Functors>
    ResultFunctor<Functors...> join(Functors&&... funcs)
    {
        return ResultFunctor(std::move(funcs)...);
    }
}

Если бы это было только для функторов с void типом возврата, я бы легко получил желаемый элемент кортежа.Но, похоже, нет способа определить это, тип возвращаемого значения не может быть выведен из заданных параметров (очевидно).

Другая идея состояла в том, чтобы использовать какой-то трюк SFINAE, чтобы выбрать правильную operator()() версию, но так или иначе мне придется «пробежаться» по всем элементам кортежа (что неприятно, но все же может бытьgoogled), а затем проверьте, подходит ли этот элемент, на основе заданных параметров пакета.

Ну, вот где я сделал паузу, чтобы тщательно обдумать это.Если у кого-нибудь (кто лучше разбирается во всем этом разнообразном) есть какие-либо идеи, я был бы очень благодарен.

Ответы [ 2 ]

6 голосов
/ 01 апреля 2019

Это очень простое решение, которое не включает SFINAE или метапрограммирование шаблонов (только обычные шаблоны).

Первым шагом является написание функтора, который представляет набор совместной перегрузки.Этого легко достичь с помощью наследования, и поскольку все функторы, используемые в качестве входных данных , должны иметь разные типы , нам не нужно делать что-то причудливое.

// This represents overload set
template<class F1, class F2>
struct Joint : public F1, public F2 {
    using F1::operator(); 
    using F2::operator(); 
}; 

Для удобства пользователя мы можем добавить руководство по выводам:

template<class F1, class F2>
Joint(F1, F2) -> Joint<F1, F2>; 

Поскольку Joint является агрегатным типом в C ++ 17 и выше, у нас нетчтобы предоставить конструктор, потому что мы можем использовать агрегатную инициализацию:

// This code magically works
auto result = std::visit(Joint{
    [](std::string const& s) { return "it's a string"; },
    [](std::pair<int, int> const& p) { return "it's a pair"; }
}, var);

Написание функции custom::join одинаково просто:

template<class F1, class F2>
auto join(F1&& f1, F2&& f2) {
    return Joint { std::forward<F1>(f1), std::forward<F2>(f2) }; 
}

Теперь, когда у нас есть базовый случай, мы можем обобщитьэто довольно легко:

template<class F, class F2, class... Fs>
auto join(F&& f, F2&& f2, Fs&&... fs) {
    return Joint{
        std::forward<F>(f),
        join(std::forward<F2>(f2), std::forward<Fs>(fs)...)
    };
}

Обращение к потенциальной критике

  • Почему бы не определить конструктор для Joint? Агрегированная инициализация является наиболее эффективной формой инициализации, потому чтокогда вы не определяете конструктор, компилятор может присваивать значения на месте без необходимости копировать или перемещать их.
  • Зачем использовать множественное наследование? Если мы полагаемся на SFINAE, это увеличивает время компиляции, увеличивает сложность кода и в некоторых случаях не работает должным образом.С SFINAE вы должны проверить каждый элемент набора перегрузки, чтобы увидеть, подходит ли он.В некоторых случаях из-за неявного преобразования выбирается худшая перегрузка, потому что это совпадение.Используя наследование, мы можем использовать встроенное в языки сопоставление с образцом для вызовов функций.
  • Зачем добавлять руководства по выводам? Они делают код чище, и в этом случае они работают точно так же, как и ожидалось: аргументы хранятся по значению
4 голосов
/ 01 апреля 2019
namespace custom {
  template<class...Fs>
  struct overloaded : Fs... {
      using Fs::operator()...;
  };
  template<class...Fs>
  overloaded(Fs...)->overloaded<Fs...>;

  template<class F>
  F&& as_obj( F&& f ){ return std::forward<F>(f); }
  template<class R, class...Args>
  auto as_obj( R(*f)(Args...) {
    struct helper {
      R(*f)(Args...);
      R operator()(Args...args) const { return f(std::forward<Args>(args)...); }
    };
    return helper{f};
  }

  template<class...Fs>
  auto join( Fs&&...fs ){
    return overloaded{as_obj(std::forward<Fs>(fs))...};
  }
}

Я добавил в качестве бонуса поддержку не перегруженных указателей функций.

...