Что самое близкое в С++ для ретроактивного определения суперкласса определенного класса?
Предположим, что у меня есть класс
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
.
Ответы
Ответ 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 и не имея возможности изменять классы. Этот уровень косвенности должен быть установлен с установленными флажками оптимизации. Другими словами, у вас будет эффективность выполнения непосредственно вызывающего бара.