#c #c 17 #variadic-templates #sfinae
#c #c 17 #шаблоны переменных #sfinae
Вопрос:
Мой коллега предоставил мне «небольшую викторину», которую он однажды заставил своих студентов решить. Кажется, что мой слабый ум просто неспособен постичь всю красоту современных возможностей C .
Subj:
Реализация join
функции, принимающей произвольные функторы и возвращающей другой функтор, который ведет себя как любой из них. Например.:
{
auto result = std::visit(custom::join(
[](std::string constamp; s) { return "it's a string"; },
[](std::pair<int, int> constamp; p) { return "it's a pair"; }
), var);
assert(result == "it's a string");
var = std::make_pair(10, 20);
auto lvalue_lambda = [](std::string constamp; s) { return "it's a string"; };
result = std::visit(custom::join(
lvalue_lambda,
[](std::pair<int, int> constamp; p) { return "it's a pair"; }
), var);
assert(result == "it's a pair");
}
Хорошо, после небольшого размышления у меня появилась идея, что std::variant
означает «один из перечисленных», поскольку это «типобезопасное объединение», поэтому мне понадобится кортеж. Пробовал что-то подобное:
namespace custom
{
template<typename ...Functors>
class ResultFunctor
{
public:
ResultFunctor(Functorsamp;amp;... 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(Functorsamp;amp;... funcs)
{
return ResultFunctor(std::move(funcs)...);
}
}
Если бы это было только для функторов с void
типом возвращаемого значения, я бы легко получил желаемый элемент tuple. Но, похоже, нет способа определить это, возвращаемый тип не может быть выведен из заданных параметров (очевидно).
Другая идея заключалась в том, чтобы использовать какой-нибудь трюк SFINAE для выбора правильной operator()()
версии, но так или иначе, мне придется «прогнать» весь элемент кортежа (который неприятен, но все еще можно погуглить), а затем проверить, подходит ли этот элемент, на основе заданного пакета параметров.
Ну, вот тут я взял паузу, чтобы все тщательно обдумать. Если у кого-нибудь (кто лучше разбирается во всем этом вариационном материале) возникнут какие-либо идеи, я был бы очень благодарен.
Комментарии:
1. Используется ли
overloaded
шаблон на cppreference.com справочная страница дляstd::visit
вам знакома? Ваш коллега просто стащил реализациюoverloaded
из cppreference.com . Я жду, чтобы увидеть, кто достаточно отчаялся в karma, чтобы скопировать-вставить это в ответ здесь и заявить, что это собственное изобретение.
Ответ №1:
Это действительно простое решение, которое не включает 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 constamp; s) { return "it's a string"; },
[](std::pair<int, int> constamp; p) { return "it's a pair"; }
}, var);
Написание custom::join
функции одинаково просто:
template<class F1, class F2>
auto join(F1amp;amp; f1, F2amp;amp; f2) {
return Joint { std::forward<F1>(f1), std::forward<F2>(f2) };
}
Теперь, когда у нас есть базовый вариант, мы можем довольно легко его обобщить:
template<class F, class F2, class... Fs>
auto join(Famp;amp; f, F2amp;amp; f2, Fsamp;amp;... fs) {
return Joint{
std::forward<F>(f),
join(std::forward<F2>(f2), std::forward<Fs>(fs)...)
};
}
Устранение потенциальных критических замечаний
- Почему бы не определить конструктор для Joint?Агрегированная инициализация является наиболее эффективной формой инициализации, потому что, когда вы не определяете конструктор, компилятор способен присваивать значения на месте без необходимости их копирования или перемещения.
- Зачем использовать множественное наследование?Если мы полагаемся на SFINAE, это увеличивает время компиляции, увеличивает сложность кода, и в некоторых случаях это работает не так, как ожидалось. С SFINAE вы должны проверить каждый элемент набора перегрузки, чтобы увидеть, подходит ли он. В некоторых случаях из-за неявного преобразования выбирается худшая перегрузка, потому что это было совпадение. Используя наследование, мы можем использовать встроенное в языки сопоставление с шаблоном для вызовов функций.
- Зачем добавлять руководства по вычету?Они делают код более чистым, и в этом случае они работают точно так, как ожидалось: аргументы хранятся по значению
Ответ №2:
namespace custom {
template<class...Fs>
struct overloaded : Fs... {
using Fs::operator()...;
};
template<class...Fs>
overloaded(Fs...)->overloaded<Fs...>;
template<class F>
Famp;amp; as_obj( Famp;amp; 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( Fsamp;amp;...fs ){
return overloaded{as_obj(std::forward<Fs>(fs))...};
}
}
Я добавил в качестве бонуса поддержку неперегруженных указателей на функции.
Комментарии:
1. О, большое спасибо! Это было именно то, где я безнадежно застрял после того, как немного познакомился с
overloaded
трюком — указателями на функции. И источникиstd::overloaded
предложения оказались слишком запутанными. Вы сделали меня счастливым человеком, еще раз спасибо!