What's the closest thing in C++ to retroactively defining a superclass of a defined class?

对着背影说爱祢 提交于 2019-12-03 08:14:33

问题


Suppose I have the class

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

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

defined and used in my code and the code of others. Now, I want to write some library which could very well operate on A's, but it's actually more general, and would be able to operate on:

class B {
protected:
    int y;
    double z;

public:
 void bar();
};

and I do want my library to be general, so I define a B class and that's what its APIs take.

I would like to be able to tell the compiler - not in the definition of A which I no longer control, but elsewhere, probably in the definition of B:

Look, please try to think of B as a superclass of A. Thus, in particular, lay it out in memory so that if I reinterpret an A* as a B*, my code expecting B*s would work. And please then actually accept A* as a B* (and A& as a B& etc.).

In C++ we can do this the other way, i.e. if B is the class we don't control we can perform a "subclass a known class" operation with class A : public B { ... }; and I know C++ doesn't have the opposite mechanism - "superclass a known class A by a new class B". My question is - what's the closest achievable approximation of this mechanism?

Notes:

  • This is all strictly compile-time, not run-time.
  • There can be no changes whatsoever to class A. I can only modify the definition of B and code that knows about both A and B. Other people will still use class A, and so will I if I want my code to interact with theirs.
  • This should preferably be "scalable" to multiple superclasses. So maybe I also have class C { protected: int x; double w; public: void baz(); } which should also behave like a superclass of A.

回答1:


You can do the following:

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(); }
};

The idea is to use type-erasure (that's the Interface and Interfacer<T> classes) under the covers to allow C to take anything that you can call bar on and then your library will take objects of type C.




回答2:


I know C++ doesn't have the opposite mechanism - "superclass a known class"

Oh yes it does:

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

and off you go. All at compile time, needless to say.


If you have a class A that can't be changed and need to slot it into an inheritance structure, then use something on the lines of

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

Note that dynamic_cast will reach A* pointers given Superclass* pointers and vice-versa. Ditto Class* pointers. At this point, you're getting close to Composition, Traits, and Concepts.




回答3:


Normal templates do this, and the compiler will inform you when you use them incorrectly.

instead of

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; }
}

you write

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; }
}

And you use BConsumer1 & BConsumer2 like

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

And you would have BSubclass<A> and BSubclass<B>, as types that use the B interface to do something.




回答4:


There is no way to change the behaviour of a class without changing the class. There is indeed no mechanism for adding a parent class after A has already been defined.

I can only modify the definition of B and code that knows about both A and B.

You cannot change A, but you can change the code that uses A. So you could, instead of using A, simply use another class that does inherit from B (let us call it D). I think this is the closest achievable of the desired mechanism.

D can re-use A as a sub-object (possibly as a base) if that is useful.

This should preferably be "scalable" to multiple superclasses.

D can inherit as many super-classes as you need it to.

A demo:

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

Now D behaves exactly as A would behave if only A had inherited B and C.

Inheriting A publicly would allow getting rid of all the delegation boilerplate:

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

However, I think that could have potential to create confusion between code that uses A without knowledge of B and code that uses knows of B (and therefore uses D). The code that uses D can easily deal with A, but not the other way 'round.

Not inheriting A at all but using a member instead would allow you to not copy A to create D, but instead refer to an existing one:

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

This obviously has potential to mistakes with object lifetimes. That could be solved with shared pointers:

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();}
};

However, this is presumably only an option if the other code that doesn't know about Bor D also uses shared pointers.




回答5:


This seems more like static polymorphism rather dynamic. As @ZdeněkJelínek has already mentioned, you could you a template to ensure the proper interface is passed in, all during compile-time.

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");
}

This should do what you'd like (i.e. use bar for any class) without having to resort to a vtable lookup and without having the ability to modify classes. This level of indirection should be inlined out with proper optimization flags set. In other words you'll have the runtime efficiency of directly invoking bar.



来源:https://stackoverflow.com/questions/44329583/whats-the-closest-thing-in-c-to-retroactively-defining-a-superclass-of-a-defi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!