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

Что самое близкое в С++ для ретроактивного определения суперкласса определенного класса?

Предположим, что у меня есть класс

class A {
protected:
    int x,y;
    double z,w;

public:
    void foo();
    void bar();
    void baz();
};

определяется и используется в моем коде и в коде других. Теперь я хочу написать некоторую библиотеку, которая вполне может работать на A, но она на самом деле более общая и будет работать:

class B {
protected:
    int y;
    double z;

public:
 void bar();
};

и я хочу, чтобы моя библиотека была общей, поэтому я определяю класс B и что его API-интерфейсы принимают.

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

Посмотрите, пожалуйста, подумайте о B как о суперклассе A. Таким образом, в частности, выложите его в памяти, чтобы, если я переосмыслить A* как B*, мой код, ожидающий B*, будет работать. И, пожалуйста, на самом деле принимайте A* как B*A& как B& и т.д.).

В С++ мы можем сделать это другим способом, т.е. если B - это класс, который мы не контролируем, мы можем выполнить операцию "подкласс известного класса" с class A : public B { ... }; и я знаю, что С++ не имеет противоположного механизма - "суперкласс" известного класса A новым классом B ". Мой вопрос: какая ближайшая достижимая аппроксимация этого механизма?

Примечания:

  • Это все строго компилируемое время, а не время выполнения.
  • Не может быть никаких изменений в class A. Я могу только изменить определение B и код, который знает обо всех A и B. Другие люди по-прежнему будут использовать класс A, и я тоже хочу, чтобы мой код взаимодействовал с ними.
  • Это должно быть "масштабируемо" для нескольких суперклассов. Возможно, у меня также есть class C { protected: int x; double w; public: void baz(); }, который также должен вести себя как суперкласс из A.
4b9b3361

Ответ 1

Вы можете сделать следующее:

class C
{
  struct Interface
  {
    virtual void bar() = 0;
    virtual ~Interface(){}
  };

  template <class T>
  struct Interfacer : Interface
  {
    T t;
    Interfacer(T t):t(t){}
    void bar() { t.bar(); }
  };

  std::unique_ptr<Interface> interface;

  public:
    template <class T>
    C(const T & t): interface(new Interfacer<T>(t)){}
    void bar() { interface->bar(); }
};

Идея состоит в том, чтобы использовать стирание типа (классы Interface и Interfacer<T>) под обложками, чтобы позволить C брать все, что вы можете называть bar, а затем ваша библиотека будет принимать объекты тип C.

Ответ 2

Я знаю, что С++ не имеет противоположного механизма - "суперкласс" известен класс "

О да, это так:

template <class Superclass>
class Class : public Superclass
{    
};

и вы уходите. Разумеется, все время компиляции.


Если у вас есть class A, который не может быть изменен и ему нужно сложить его в структуру наследования, используйте что-то в строках

template<class Superclass>
class Class : public A, public Superclass
{
};

Обратите внимание, что dynamic_cast достигнет указателей A* с указателями Superclass* и наоборот. Тоже Class* указатели. На данный момент вы приближаетесь к композициям, чертам и концепциям.

Ответ 3

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

вместо

void BConsumer1(std::vector<B*> bs)
{ std::for_each(bs.begin(), bs.end(), &B::bar); }

void BConsumer2(B& b)
{ b.bar(); }

class BSubclass : public B 
{
    double xplusz() const { return B::x + B::z; }
}

вы пишете

template<typename Blike>
void BConsumer1(std::vector<Blike*> bs)
{ std::for_each(bs.begin(), bs.end(), &Blike::bar); }

template<typename Blike>
void BConsumer2(Blike& b)
{ b.bar(); }

template<typename Blike>
class BSubclass : public Blike 
{
    double xplusz() const { return Blike::x + Blike::z; }
}

И вы используете BConsumer1 и BConsumer2, например

std::vector<A*> as = /* some As */
BConsumer1(as); // deduces to BConsumer1<A>
A a;
BConsumer2(a); // deduces to BConsumer2<A>

std::vector<B*> bs = /* some Bs */
BConsumer1(bs); // deduces to BConsumer1<B>
// etc

И вы бы имели BSubclass<A> и BSubclass<B>, как типы, которые используют интерфейс B, чтобы что-то сделать.

Ответ 4

Невозможно изменить поведение класса без изменения класса. На самом деле нет механизма для добавления родительского класса после того, как A уже определен.

Я могу только изменить определение B и код, который знает об A и B.

Вы не можете изменить A, но вы можете изменить код, который использует A. Таким образом, вместо A можно использовать другой класс, который наследует от B (назовем его D). Я думаю, что это самое близкое достижение желаемого механизма.

D может повторно использовать A как под-объект (возможно, как базу), если это полезно.

Это должно быть "масштабируемо" для нескольких суперклассов.

D может наследовать столько суперклассов, сколько вам нужно.

Демонстрация:

class D : A, public B, public C {
public:
    D(const A&);
    void foo(){A::foo();}
    void bar(){A::bar();}
    void baz(){A::baz();}
};

Теперь D ведет себя точно так же, как A будет вести себя, если только A унаследовал B и C.

Наследование A публично позволит избавиться от всех шаблонов делегирования:

class D : public A, public B, public C {
public:
    D(const A&);
};

Однако я думаю, что это могло бы создать путаницу между кодом, который использует A без знания B и кода, который использует знает B (и поэтому использует D). Код, который использует D, может легко справиться с A, но не наоборот.

Не наследовать A вообще, но используя элемент вместо этого, вы не должны копировать A для создания D, а вместо этого ссылаться на существующий:

class D : public B, public C {
    A& a;
public:
    D(const A&);
    void foo(){a.foo();}
    void bar(){a.bar();}
    void baz(){a.baz();}
};

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

class D : public B, public C {
    std::shared_ptr<A> a;
public:
    D(const std::shared_ptr<A>&);
    void foo(){a->foo();}
    void bar(){a->bar();}
    void baz(){a->baz();}
};

Однако это, по-видимому, только опция, если другой код, который не знает о B или D, также использует общие указатели.

Ответ 5

Это скорее похоже на статичный полиморфизм, довольно динамичный. Как уже упоминал @ZdeněkJelínek, вы могли бы создать шаблон для обеспечения правильного интерфейса, который был передан во время компиляции.

namespace details_ {
   template<class T, class=void>
   struct has_bar : std::false_type {};

   template<class T>
   struct has_bar<T, std::void_t<decltype(std::declval<T>().bar())>> : std::true_type {};
}

template<class T>
constexpr bool has_bar = details_::has_bar<T>::value;

template<class T>
std::enable_if_t<has_bar<T>> use_bar(T *t) { t->bar(); }

template<class T>
std::enable_if_t<!has_bar<T>> use_bar(T *) {
   static_assert(false, "Cannot use bar if class does not have a bar member function");
}

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