Подтвердить что ты не робот

Перегрузка нескольких объектов функций по ссылке

В С++ 17 тривиально реализовать функцию overload(fs...), которая при любом количестве аргументов fs..., удовлетворяющих FunctionObject возвращает новый объект функции, который ведет себя как перегрузка fs.... Пример:

template <typename... Ts>
struct overloader : Ts...
{
    template <typename... TArgs>
    overloader(TArgs&&... xs) : Ts{forward<TArgs>(xs)}...
    {
    }

    using Ts::operator()...;
};

template <typename... Ts>
auto overload(Ts&&... xs)
{
    return overloader<decay_t<Ts>...>{forward<Ts>(xs)...};
}

int main()
{
    auto o = overload([](char){ cout << "CHAR"; }, 
                      [](int) { cout << "INT";  });

    o('a'); // prints "CHAR"
    o(0);   // prints "INT"
}

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


Так как вышеприведенный overloader наследует от Ts..., ему нужно либо скопировать, либо переместить объекты функции для работы. Я хочу что-то, что обеспечивает одно и то же поведение перегрузки, но только ссылки на объекты переданной функции.

Позвоните в эту гипотетическую функцию ref_overload(fs...). Моя попытка заключалась в использовании std::reference_wrapper и std::ref следующим образом:

template <typename... Ts>
auto ref_overload(Ts&... xs)
{
    return overloader<reference_wrapper<Ts>...>{ref(xs)...};
}

Кажется, достаточно просто, не так ли?

int main()
{
    auto l0 = [](char){ cout << "CHAR"; };
    auto l1 = [](int) { cout << "INT";  };

    auto o = ref_overload(l0, l1);

    o('a'); // BOOM
    o(0);
}

error: call of '(overloader<...>) (char)' is ambiguous
 o('a'); // BOOM
      ^

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

Причина, по которой он не работает, прост: std::reference_wrapper::operator() - это шаблон вариационной функции, который не играет хорошо с перегрузка.

Чтобы использовать синтаксис using Ts::operator()..., мне нужно Ts... выполнить FunctionObject. Если я попытаюсь создать свою собственную упаковку FunctionObject, я столкнусь с той же проблемой:

template <typename TF>
struct function_ref
{
    TF& _f;
    decltype(auto) operator()(/* ??? */);
};

Поскольку нет способа выражения "компилятора", заполните ??? теми же аргументами, что и TF::operator() ", мне нужно использовать шаблон вариационной функции, не решая ничего.

Я также не могу использовать что-то вроде boost::function_traits, потому что одна из функций, переданных в overload(...), может быть шаблоном функции или перегруженной функцией объект сам!

Поэтому мой вопрос: есть способ реализации функции ref_overload(fs...), которая при любом числе объектов функции fs... возвращает новый объект функции, который ведет себя как перегрузка fs..., но означает fs... вместо копирования/перемещения?

4b9b3361

Ответ 1

Хорошо, вот план: мы собираемся определить, какой объект функции содержит перегрузку operator(), которая была бы выбрана, если бы мы использовали перегружатель bare-bones на основе наследования и использования объявлений, как показано в вопросе. Мы собираемся сделать это (в необоснованном контексте), заставив двусмысленность в преобразовании с производной базой для параметра неявного объекта, что происходит после успешного разрешения перегрузки. Это поведение указано в стандарте, см. N4659 [namespace.udecl]/16 и 18.

В принципе, мы собираемся добавить каждый объект функции в свою очередь как дополнительный подобъект базового класса. Для вызова, для которого выполняется разрешение перегрузки, создание базовой двусмысленности для любого из объектов функций, которые не содержат выигрышной перегрузки, ничего не изменит (вызов все равно будет успешным). Однако вызов будет недействительным для случая, когда дублируемая база содержит выбранную перегрузку. Это дает нам интерфейс SFINAE для работы. Затем мы переадресуем вызов через соответствующую ссылку.

#include <cstddef>
#include <type_traits>
#include <tuple>
#include <iostream>

template<class... Ts> 
struct ref_overloader
{
   static_assert(sizeof...(Ts) > 1, "what are you overloading?");

   ref_overloader(Ts&... ts) : refs{ts...} { }
   std::tuple<Ts&...> refs;

   template<class... Us> 
   decltype(auto) operator()(Us&&... us)
   {
      constexpr bool checks[] = {over_fails<Ts, pack<Us...>>::value...};
      static_assert(over_succeeds(checks), "overload resolution failure");
      return std::get<choose_obj(checks)>(refs)(std::forward<Us>(us)...);
   }

private:
   template<class...> 
   struct pack { };

   template<int Tag, class U> 
   struct over_base : U { };

   template<int Tag, class... Us> 
   struct over_base<Tag, ref_overloader<Us...>> : Us... 
   { 
       using Us::operator()...; // allow composition
   }; 

   template<class U> 
   using add_base = over_base<1, 
       ref_overloader<
           over_base<2, U>, 
           over_base<1, Ts>...
       >
   >&; // final & makes declval an lvalue

   template<class U, class P, class V = void> 
   struct over_fails : std::true_type { };

   template<class U, class... Us> 
   struct over_fails<U, pack<Us...>,
      std::void_t<decltype(
          std::declval<add_base<U>>()(std::declval<Us>()...)
      )>> : std::false_type 
   { 
   };

   // For a call for which overload resolution would normally succeed, 
   // only one check must indicate failure.
   static constexpr bool over_succeeds(const bool (& checks)[sizeof...(Ts)]) 
   { 
       return !(checks[0] && checks[1]); 
   }

   static constexpr std::size_t choose_obj(const bool (& checks)[sizeof...(Ts)])
   {
      for(std::size_t i = 0; i < sizeof...(Ts); ++i)
         if(checks[i]) return i;
      throw "something wrong with overload resolution here";
   }
};

template<class... Ts> auto ref_overload(Ts&... ts)
{
   return ref_overloader<Ts...>{ts...};
}


// quick test; Barry example is a very good one

struct A { template <class T> void operator()(T) { std::cout << "A\n"; } };
struct B { template <class T> void operator()(T*) { std::cout << "B\n"; } };

int main()
{
   A a;
   B b;
   auto c = [](int*) { std::cout << "C\n"; };
   auto d = [](int*) mutable { std::cout << "D\n"; };
   auto e = [](char*) mutable { std::cout << "E\n"; };
   int* p = nullptr;
   auto ro1 = ref_overload(a, b);
   ro1(p); // B
   ref_overload(a, b, c)(p); // B, because the lambda operator() is const
   ref_overload(a, b, d)(p); // D
   // composition
   ref_overload(ro1, d)(p); // D
   ref_overload(ro1, e)(p); // B
}

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


Предостережения:

  • Мы предполагаем, что даже если мы не хотим перегружателя, основанного на наследовании, мы могли бы наследовать эти объекты функций, если бы захотели. Такой производный объект не создается, но проверки, выполненные в необоснованных контекстах, полагаются на это. Я не могу думать о другом способе переноса этих перегрузок в один и тот же объем, чтобы к ним можно было применить разрешение перегрузки.
  • Мы предполагаем, что пересылка работает правильно для аргументов вызова. Учитывая, что мы держим ссылки на целевые объекты, я не вижу, как это может работать без какой-либо пересылки, поэтому это кажется обязательным требованием.
  • В настоящее время это работает на Clang. Для GCC похоже, что преобразование с производной базой, на которое мы полагаемся, не является контекстом SFINAE, поэтому он вызывает жесткую ошибку; насколько это возможно, это неверно. MSVC очень полезен и неоднозначно вызывает призыв к нам: похоже, что он просто выбирает подобъект базового класса, который на первом месте; там, он работает - что не нравится? (MSVC менее актуальна для нашей проблемы на данный момент, поскольку она не поддерживает другие возможности С++ 17).
  • Композиция работает с некоторыми специальными мерами предосторожности - при тестировании перегружателя на основе гипотетического наследования, ref_overloader распаковывается в его составные функциональные объекты, так что их operator() участвуют в разрешении перегрузки вместо пересылки operator(). Любой другой перегружатель, пытающийся составить ref_overloader, будет явно терпеть неудачу, если он не сделает что-то подобное.

Некоторые полезные биты:

  • Хороший упрощенный пример от Vittorio, демонстрирующий двусмысленную базовую идею в действие.
  • О реализации add_base: частичная специализация over_base для ref_overloader содержит ли "разворачивание", упомянутое выше, для включения ref_overloader, содержащего другие ref_overloader s. При этом я просто использовал его, чтобы построить add_base, который немного взломан, признаюсь. add_base действительно должен быть чем-то вроде inheritance_overloader<over_base<2, U>, over_base<1, Ts>...>, но я не хотел определять другой шаблон, который будет делать то же самое.
  • Об этом странном тесте в over_succeeds: логика заключается в том, что если для нормального случая не будет разрешено перегрузочное решение (добавлена ​​двусмысленная база), то это также потерпит неудачу для всех "инструментальных" случаев, независимо от какая база добавлена, поэтому массив checks будет содержать только элементы true. И наоборот, если для нормального случая будет выполнено разрешение перегрузки, то это также будет успешным для всех других случаев, кроме одного, поэтому checks будет содержать один элемент true со всеми остальными, равными false.

    Учитывая эту однородность в значениях в checks, мы можем посмотреть только на первые два элемента: если оба они true, это указывает на отказ разрешения перегрузки в нормальном случае; все остальные комбинации указывают на успех разрешения. Это ленивое решение; в производственной реализации я бы, вероятно, пошел на всеобъемлющий тест, чтобы убедиться, что checks действительно содержит ожидаемую конфигурацию.


Отчет об ошибках для GCC, представленный Vittorio.

Отчет об ошибке для MSVC.

Ответ 2

В общем случае я не думаю, что такое возможно даже в С++ 17. Рассмотрим наиболее неприятный случай:

struct A {
    template <class T> int operator()(T );
} a;

struct B {
    template <class T> int operator()(T* );
} b;

ref_overload(a, b)(new int);

Как вы могли бы сделать эту работу? Мы можем проверить, что оба типа вызываются с помощью int*, но оба operator() являются шаблонами, поэтому мы не можем выбирать их подписи. Даже если бы мы могли, сами вычисленные параметры совпадали - обе функции принимают int*. Как вы знаете, чтобы позвонить b?

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

struct A' {
    template <class T> index_<0> operator()(T );
};

struct B' {
    template <class T> index_<1> operator()(T* );
};

Тогда мы могли бы использовать decltype(overload(declval<A'>(), declval<B'>()))::value, чтобы выбрать, какую ссылку называть сами.

В простейшем случае - когда A и bC и...) имеют один единственный operator(), который не является шаблоном, это выполнимо - поскольку мы можем фактически проверить &X::operator() и манипулировать этими сигнатурами для создания новых, которые нам нужны. Это позволяет нам использовать компилятор для разрешения перегрузки для нас.

Мы также можем проверить, какой тип overload(declval<A>(), declval<B>(), ...)(args...) дает. Если тип возврата наилучшего соответствия уникален почти всем жизнеспособным кандидатам, мы все равно можем выбрать правильную перегрузку в ref_overload. Это будет больше для нас, так как теперь мы можем корректно обрабатывать некоторые случаи с помощью перегруженных или шаблонных операторов вызовов, но мы будем неправильно отклонять многие вызовы как неоднозначные, а это не так.


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

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

Альтернативным потенциальным будущим решением будет использование перегруженного operator .. Раздел 4.12 включает пример, который указывает, что конструкция позволяет перегружать различные функции-члены по имени через разные operator.() s. Если это предложение пройдет в какой-то подобной форме сегодня, то реализация перегрузки ссылок будет следовать той же схеме, что и перегрузка объектов сегодня, просто заменив разные operator .() на сегодняшний день разными operator () s:

template <class T>
struct ref_overload_one {
    T& operator.() { return r; }
    T& r;
};

template <class... Ts>
struct ref_overloader : ref_overload_one<Ts>...
{
    ref_overloader(Ts&... ts)
    : ref_overload_one<Ts>{ts}...
    { }

    using ref_overload_one<Ts>::operator....; // intriguing syntax?
};