Ответ 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
}
Предостережения:
- Мы предполагаем, что даже если мы не хотим перегружателя, основанного на наследовании, мы могли бы наследовать эти объекты функций, если бы захотели. Такой производный объект не создается, но проверки, выполненные в необоснованных контекстах, полагаются на это. Я не могу думать о другом способе переноса этих перегрузок в один и тот же объем, чтобы к ним можно было применить разрешение перегрузки.
- Мы предполагаем, что пересылка работает правильно для аргументов вызова. Учитывая, что мы держим ссылки на целевые объекты, я не вижу, как это может работать без какой-либо пересылки, поэтому это кажется обязательным требованием.
- В настоящее время это работает на 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.