С++ с использованием объявления для пакета параметров

Я хотел бы определить класс, который наследуется от группы классов, но не скрывает некоторые конкретные методы этих классов.

Представьте себе следующий код:

template<typename... Bases>
class SomeClass : public Bases...
{
public: 
  using Bases::DoSomething...;

  void DoSomething(){
    //this is just another overload
  }
};

Теперь проблема в том, что если хотя бы в одном классе нет члена с именем DoSomething, я получаю сообщение об ошибке. Я уже пытался эмулировать «игнорировать, если не определено, используя» с помощью макроса и SFINAE, но для обработки всех случаев это становится очень большим и уродливым! У вас есть идея решить эту проблему?

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

Вот пример кода: Godbolt


person Bernd    schedule 05.04.2020    source источник
comment
Не полностью конкретизировано, но вы можете определить шаблон оболочки для классов, от которых вы наследуете, который добавляет DoSomething, если он отсутствует (если вы можете использовать концепции С++ 20, что должно быть легко), а затем class SomeClass : public Wrapper<Bases>...   -  person MikeMB    schedule 05.04.2020
comment
К сожалению, это непросто - вам нужно обрабатывать поля, статические методы и методы экземпляра как с универсальными аргументами, так и без них... Кроме того, это несуществующее/искусственное DoSomething не должно вызываться   -  person Bernd    schedule 05.04.2020
comment
Поместите void DoSomething() во вспомогательный класс и наследуйте его вместе с Bases....   -  person Igor Tandetnik    schedule 05.04.2020
comment
Затем я получаю член ошибки, найденный при поиске неоднозначного имени, см.: godbolt.org/z/_oQFJ5   -  person Bernd    schedule 05.04.2020


Ответы (4)


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

struct A { void DoSomething(int); };
struct B { void DoSomething(double); };
SomeClass<A, B>().DoSomething(42); // error ambiguous

Единственный способ сохранить разрешение перегрузки — использовать наследование.

Ключевым моментом является завершение того, что начал ecatmur. Но как выглядит HasDoSomething? Подход в ссылке работает только при наличии одного, не перегруженного, не шаблонного. Но мы можем сделать лучше. Мы можем использовать тот же механизм, чтобы определить, существует ли DoSomething, который требует using для начала: имена из разных областей не перегружаются.

Итак, мы вводим новый базовый класс, который имеет DoSomething, который никогда не будет по-настоящему выбран, и мы делаем это, создавая свой собственный явный тип тега, который мы единственные, кто когда-либо будет создавать. За неимением лучшего имени, я назову его в честь моей собаки Вести:

struct westie_tag { explicit westie_tag() = default; };
inline constexpr westie_tag westie{};
template <typename T> struct Fallback { void DoSomething(westie_tag, ...); };

И сделайте его вариативным для хорошей меры, просто чтобы сделать его как можно меньше. Но на самом деле это не имеет значения. Теперь, если мы введем новый тип, например:

template <typename T> struct Hybrid : Fallback<T>, T { };

Затем мы можем вызвать DoSomething() на гибриде именно тогда, когда T нет перегрузки DoSomething — любого типа. Это:

template <typename T, typename=void>
struct HasDoSomething : std::true_type { };
template <typename T>
struct HasDoSomething<T, std::void_t<decltype(std::declval<Hybrid<T>>().DoSomething(westie))>>
    : std::false_type
{ };

Обратите внимание, что обычно в этих трейтах первичным является false, а специализация — true — здесь все наоборот. Ключевое различие между этим ответом и ответом ecatmur заключается в том, что перегрузка резервного варианта должна быть каким-то образом вызвана - и использовать эту возможность для ее проверки - просто она не будет фактически вызываться для любого типа, который пользователь будет фактически использовать.

Проверка таким образом позволяет нам правильно определить, что:

struct C {
    void DoSomething(int);
    void DoSomething(int, int);
};

действительно удовлетворяет HasDoSomething.

А затем используем тот же метод, который показал ecatmur:

template <typename T>
using pick_base = std::conditional_t<
    HasDoSomething<T>::value,
    T,
    Fallback<T>>;

template<typename... Bases>
class SomeClass : public Fallback<Bases>..., public Bases...
{
public: 
  using pick_base<Bases>::DoSomething...;

  void DoSomething();
};

И это работает независимо от того, как выглядят все перегрузки Bases DoSomething, и правильно выполняет разрешение перегрузок в первом упомянутом мной случае.

Демо

person Barry    schedule 06.04.2020
comment
Спасибо за ваше хорошее решение, но я все еще не доволен им. Если A имеет приватный DoSomething, он не компилируется. Это делает использование бесполезным для меня... (см. godbolt.org/z/ba5aM6) - person Bernd; 06.04.2020
comment
@BerndBaumanns Есть A друг SomeClass. Что-то должно где-то дать. - person Barry; 07.04.2020
comment
Быстрый ответ и хорошая идея! Но, к сожалению, я не могу изменить класс А. - person Bernd; 07.04.2020
comment
@BerndBaumanns, имеющий одно и то же имя с частным и публичным доступом, нарушит декларацию использования и в целом является плохой практикой. Единственный способ обойти это — написать оболочку для A, которая перенаправляет общедоступные функции-члены. - person ecatmur; 07.04.2020
comment
Есть ли наилучшая практика для обработки закрытых/защищенных членов в С++? Обычно я не хочу, чтобы частная функция скрывала унаследованную общедоступную/защищенную функцию. - person Bernd; 07.04.2020
comment
Что я часто видел, так это то, что имена защищенных функций начинаются с чего-то вроде doXXX, в то время как общедоступная функция остается XXX. Поэтому мы кодируем модификатор видимости в имени, чтобы избежать этих проблем. - person Bernd; 07.04.2020

Как насчет условного использования запасного варианта?

Создайте не вызываемые реализации каждого метода:

template<class>
struct Fallback {
    template<class..., class> void DoSomething();
};

Наследовать от Fallback один раз для каждого базового класса:

class SomeClass : private Fallback<Bases>..., public Bases...

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

using std::conditional_t<HasDoSomething<Bases>::value, Bases, Fallback<Bases>>::DoSomething...;

Пример.

person ecatmur    schedule 05.04.2020
comment
Ваши трейты недостаточно хороши, хотя Demo, например, сбой при перегрузках. - person Jarod42; 06.04.2020
comment
@ Jarod42, конечно, я оставил реализацию HasDoSomething в качестве упражнения для читателя. Существуют хорошо известные методы борьбы с перегрузками, см., например, stackoverflow.com/a/39750372/567292 - person ecatmur; 07.04.2020

Вы можете добавить оболочку, которая обрабатывает основные случаи, пересылая вместо using:

template <typename T>
struct Wrapper : T
{
    template <typename ... Ts, typename Base = T>
    auto DoSomething(Ts&&... args) const
    -> decltype(Base::DoSomething(std::forward<Ts>(args)...))
    {
        return Base::DoSomething(std::forward<Ts>(args)...);
    }

    template <typename ... Ts, typename Base = T>
    auto DoSomething(Ts&&... args)
    -> decltype(Base::DoSomething(std::forward<Ts>(args)...))
    {
        return Base::DoSomething(std::forward<Ts>(args)...);
    }

    // You might fix missing noexcept specification
    // You might add missing combination volatile/reference/C-elipsis version.
    // And also special template versions with non deducible template parameter...
};

template <typename... Bases>
class SomeClass : public Wrapper<Bases>...
{
public: 
  using Wrapper<Bases>::DoSomething...; // All wrappers have those methods,
                                        // even if SFINAEd

  void DoSomething(){ /*..*/ }
};

Демо

Как заметил Барри, есть и другие недостатки, поскольку изменилось разрешение перегрузки, что сделало некоторые вызовы неоднозначными...

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

person Jarod42    schedule 05.04.2020
comment
Кажется, ваше решение работает, но оно пропускает (как вы уже упоминали в своих комментариях) некоторые случаи (статические и нестатические, параметры шаблона...). Раньше я делал что-то подобное — но вместо базового класса-обертки я написал Макрос с кучей функций делегирования и SFINAE, потому что с этими методами переадресации вам действительно не нужно использование. - person Bernd; 05.04.2020
comment
статический/нестатический обрабатывается. Единственная часть, которую нельзя полностью обработать и которой необходимо знать информацию с помощью Bases, - это невыводимый шаблон. что не часто (std::get<I>(..). и даже для них вы можете вручную добавить перегрузку в Wrapper (действительно не идеально). Кстати, даже функция шаблона может иметь проблемы с using. - person Jarod42; 05.04.2020
comment
оба метода не являются статическими - поэтому они не обрабатываются или? - person Bernd; 06.04.2020
comment
Я имел в виду, что метод-оболочка может вызывать статический метод, поскольку Base::f() можно интерпретировать двумя способами: статический вызов или вызов базового класса (this->Base::f()) Демонстрация, но на самом деле метод больше не является статическим в SomeClass/Wrapper. - person Jarod42; 06.04.2020

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

#include<type_traits>

template<class,class> struct cons;  // not defined
template<class ...TT> struct pack;  // not defined

namespace detail {
  template<template<class> class,class,class,class>
  struct sift;
  template<template<class> class P,class ...TT,class ...FF>
  struct sift<P,pack<>,pack<TT...>,pack<FF...>>
  {using type=cons<pack<TT...>,pack<FF...>>;};
  template<template<class> class P,class I,class ...II,
           class ...TT,class ...FF>
  struct sift<P,pack<I,II...>,pack<TT...>,pack<FF...>> :
    sift<P,pack<II...>,
         std::conditional_t<P<I>::value,pack<TT...,I>,pack<TT...>>,
         std::conditional_t<P<I>::value,pack<FF...>,pack<FF...,I>>> {};

  template<class,class=void> struct has_something : std::false_type {};
  template<class T>
  struct has_something<T,decltype(void(&T::DoSomething))> :
    std::true_type {};
}

template<template<class> class P,class ...TT>
using sift_t=typename detail::sift<P,pack<TT...>,pack<>,pack<>>::type;

Затем разложите результат и наследуйте от отдельных классов:

template<class> struct C;
template<class ...MM,class ...OO>  // have Method, Others
struct C<cons<pack<MM...>,pack<OO...>>> : MM...,OO... {
  using MM::DoSomething...;
  void DoSomething();
};

template<class T> using has_something=detail::has_something<T>;

template<class ...TT> using C_for=C<sift_t<has_something,TT...>>;

Обратите внимание, что has_something здесь поддерживает только неперегруженные методы (на базовый класс) для простоты; см. ответ Барри для обобщения этого.

person Davis Herring    schedule 06.04.2020