Функция Variadic Overloading и SFINAE — устранение неоднозначности для имитации скрытия по подписи

Я хотел бы скрыть по подписи, а не по имени в С++. Поэтому я написал макрос, который определяет функцию с переменным числом аргументов, которая делегирует все вызовы своему базовому классу, если они существуют. Я не могу использовать объявление using, потому что я не хочу, чтобы оно потерпело неудачу, если в базовом классе нет метода с таким именем, а унаследованные методы следует рассматривать только в том случае, если нет прямых совпадений членов. И это работает в большинстве случаев, потому что реализовано функцией с переменным числом переменных, которые всегда являются худшими кандидатами по сравнению с функциями без переменных. Но у меня проблема, когда дочерний класс тоже имеет вариативную функцию -> вызов становится неоднозначным.

Получается следующая ситуация (упрощенно - без сфинаев, макросов...):

#include <type_traits>
#include <iostream>

class A{
public:
  void Do(){
      std::cout << "A::Do()\n";
  }
};

class B : public A
{
public:
  template<
    typename... TX,
    typename SomeSFINAE = int
  >
  void Do(TX...){
      std::cout << "B::Do()\n";
  }

  template<typename... T>
  void Do(T...){
      A::Do();
  }
};

int main(){
  B b;
  b.Do();
  return 0;
}

См. его на godbolt.

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


Обновлять

Кажется, не совсем понятно, чего я действительно хочу достичь. Итак, вот псевдокод с комментариями:

#include <type_traits>
#include <iostream>

class A{
public:
  void Do(){
      std::cout << "A::Do()\n";
  }
};

class B : public A
{
public:
  template<
    typename... TX
  >
  void Do(TX...){
      std::cout << "B::Do()\n";
  }

  using A::Do; //<--- This should be considered only if no direct match is found in B
  //Variadic function should win, because it is defined in B not in A - it should hide A.Do
  //It should even work if A has NO method Do
};

int main(){
  B b{};
  b.Do(); //-> B::Do should be called, not A::Do
  return 0;
}

Обновлять

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

Например:

#include <iostream>

void Do(int a){
    std::cout << "better";
}

template<typename... T> 
void Do(int a, T...){
  //this is worse
  std::cout << "worse";
}


int main(){
  Do(42);
  return 0;
}

Есть ли что-то, что может сделать вариативную функцию еще хуже?

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

#define NATIVE_DO_NOT_HIDE_INHERITED_(AMETHOD, ...) \
    private: template<typename $T, typename... $Args> \
    using CallHiding$ ## AMETHOD = decltype(::std::declval<$T*>()->AMETHOD (::std::declval<$Args>()...)); \
    \
    public: template< \
        typename... $Args \
        , typename $Dependent = __VA_ARGS__ \
        , bool $Detected = ::CORE_NATIVE_NS ::is_detected_v<CallHiding$ ## AMETHOD, $Dependent, $Args...> \
        , typename = typename ::std::enable_if_t<$Detected > \
    > \
    constexpr decltype(auto) AMETHOD ($Args&&... args) \
    { \
        /*allow virtual call*/ \
        return static_cast<$Dependent*>(this) -> AMETHOD (::std::forward<$Args>(args)...); \
    } \
    \
    private: template<typename $T, typename $FktArgsTuple, typename $ValueArgsTuple> \
    class CallHidingGeneric$ ## AMETHOD : public ::std::bool_constant<false> {\
    };\
    \
    private: template<typename $T, typename... $FktArgs, typename... $ValueArgs> \
    class CallHidingGeneric$ ## AMETHOD<$T, ::std::tuple<$FktArgs...>, ::std::tuple<$ValueArgs...>> \
    {\
        template<typename AType> \
        using ATemplate = decltype(::std::declval<AType>().template AMETHOD <$FktArgs...> (::std::declval<$ValueArgs>()...)); \
    public: \
        constexpr static bool value = ::CORE_NATIVE_NS ::is_detected_v<ATemplate, $T> ; \
    }; \
    \
    public: template< \
        typename... $FktArgs \
        , typename... $Args \
        , typename $Dependent = __VA_ARGS__ \
        , typename = ::std::enable_if_t<(sizeof...($FktArgs) > 0)> \
        , typename = ::std::enable_if_t< \
                CallHidingGeneric$ ## AMETHOD<$Dependent, typename ::std::template tuple<$FktArgs...>,  typename ::std::template tuple<$Args...>>::value \
            > \
    > \
    constexpr decltype(auto) AMETHOD ($Args&&... args) \
    { \
        return $Dependent ::template AMETHOD <$FktArgs...> (::std::forward<$Args>(args)...); \
    }

#define NATIVE_DO_NOT_HIDE_INHERITED(AMETHOD) NATIVE_DO_NOT_HIDE_INHERITED_(AMETHOD, $Next)
#define NATIVE_DO_NOT_HIDE_INHERITED2(AMETHOD, ...) NATIVE_DO_NOT_HIDE_INHERITED_(AMETHOD, typename ::CORE_NATIVE_NS::type_container_t< __VA_ARGS__ >:: $Next)

Он отлично работает с обычными функциями, но функции, сгенерированные макросом, не считаются хуже...


person Bernd    schedule 05.07.2020    source источник
comment
Задумывались ли вы об использовании бесплатных функций друзей вместо функций-членов?   -  person Oliv    schedule 05.07.2020
comment
если вы отбросите using A::Do; во втором обновлении, A::Do будет скрыто B:Do. Но я предполагаю, что ваша реальная ситуация более сложна, где A::Do на самом деле является вариативным шаблоном, а B::Do - другим вариативным шаблоном. Я думаю, вам нужно привести более реальный пример.   -  person JHBonarius    schedule 09.07.2020
comment
Да, это использование не является использованием С++. Поэтому я добавил комментарий, чтобы объяснить, чего я хочу. В C++ все унаследованные методы скрываются путем введения новой функции с тем же именем в дочернем классе — это называется скрытием по имени. Моя цель — скрыть только унаследованные функции, которые уже можно вызывать с помощью дочерних методов. Это называется скрыть по подписи. Поведение должно быть как в CSharp, а не как в обычном C++. В CSharp это немного сложнее — поэтому игнорируйте модификаторы видимости.   -  person Bernd    schedule 09.07.2020


Ответы (1)


Поскольку вы пометили этот C++20 тегом, вы можете использовать пункт require для ограничения B::Do на возможность вызова B::Do или A::Do, а затем использовать if constexpr в теле:

class B : public A
{
public:
    template <typename... TX>
    void Do(TX... ts)
        requires true || requires (A a) { a.Do(ts...); }
    {
        if constexpr (true) {
            std::cout << "B::Do()\n";
        } else {
            A::Do(ts...);
        }
    }
};

Здесь я использую true вместо условия для вызова B::Do, поэтому просто замените это условие в обоих местах соответствующим образом.

Вы можете уменьшить дублирование, спрятав фактическую реализацию B::Do в какой-либо другой функции:

class B : public A
{
    template <typename... TX>
    void DoImpl(TX... ts) {
        std::cout << "B::Do()\n";
    }
    
public:
    template <typename... TX>
    void Do(TX... ts)
        requires requires (B b) { b.DoImpl(ts...); }
              || requires (A a) { a.Do(ts...); }
    {
        if constexpr (requires (B b) { b.DoImpl(ts...); }) {
            B::DoImpl(ts...);
        } else {
            A::Do(ts...);
        }
    }
};

И теперь вам просто нужно ограничить B::DoImpl


Другой подход по-прежнему заключается в использовании чего-то вроде first_of() (поскольку это то, что вы пытаетесь сделать — вызвать первую из ряда функций). Это немного неудобно с функциями-членами, но вы можете заставить это работать с закрытым статическим членом:

class B : public A
{
    template <typename... TX>
    void DoImpl(TX... ts) {
        std::cout << "B::Do()\n";
    }

    static constexpr auto do_impl =
        boost::hof::first_of(
            [](B& b, auto... args) BOOST_HOF_RETURNS(b.DoImpl(args...)),
            [](A& a, auto... args) BOOST_HOF_RETURNS(a.Do(args...)));
    
public:
    template <typename... TX>
    void Do(TX... ts)
        requires requires { do_impl(*this, ts...); }
    {
        return do_impl(*this, ts...);
    }
};
person Barry    schedule 05.07.2020
comment
Спасибо, Барри, за ответ. Ваша реализация приводит более или менее к функции диспетчера. Но то, что я хочу, - это способ ухудшить перегрузку функции с переменным числом аргументов. Я отредактирую свой вопрос. - person Bernd; 05.07.2020
comment
@BerndBaumanns Я понятия не имею, что это значит, и я не понимаю вашего редактирования (ваш Do с ... не худший кандидат из-за ..., он просто вообще не кандидат). Вы не можете просто ухудшить одну функцию, если только A не знает заранее, что B существует... и тогда B последняя, ​​или есть еще и C? - person Barry; 06.07.2020
comment
Извините, вы правы - это была ошибка. Я изменил его снова. Я хочу определить макрос, который определяет функцию, которая выбирается только в том случае, если нет лучшего совпадения. Да, может быть C и даже D... - person Bernd; 06.07.2020
comment
И A ничего не должен знать о B. B не должен быть изменен только потому, что A добавил метод с именем, уже определенным в B. - person Bernd; 06.07.2020
comment
@BerndBaumanns, который выбирается только в том случае, если нет лучшего соответствия - но это противоположно тому, что вы просили, вы хотели, чтобы B::Do() вызывался, даже если A::Do() является лучшим соответствием. - person Barry; 06.07.2020
comment
Да, унаследованные методы должны быть последними. Предпочтение всегда следует отдавать прямым членам. - person Bernd; 06.07.2020
comment
Это должно быть даже в том случае, если прямой член потребует неявного преобразования, а унаследованный метод — нет. - person Bernd; 06.07.2020
comment
Таким образом, мы не можем использовать использование — использование сделало бы их доступными на том же уровне. - person Bernd; 06.07.2020