C++中最接近“追溯地”定义已定义类的超类是什么?

31

假设我有这个类:

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

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

我在我的代码和其他人的代码中定义和使用了A。现在,我想编写一些库,可以很好地操作A,但实际上更通用,并且能够操作:

class B {
protected:
    int y;
    double z;

public:
 void bar();
};
我希望我的库是通用的,因此我定义了一个B类,并且它的API采用这种方式。
我想要告诉编译器 - 不在我不再控制的A的定义中,而在其他地方,可能在B的定义中:
请尝试将B视为A的超类。 因此,请按特定方式排列内存,以便如果我将A *重新解释为B *,则期望B *的代码将起作用。 然后请实际接受A *作为B *(及A和B&等)。
在C ++中,我们可以采用相反的方式,即如果B是我们不控制的类,则可以使用class A:public B {…};执行“子类化已知类”操作;并且我知道C ++没有相反的机制-“通过新类B超类化已知类A”。 我的问题是-最接近实现这种机制的逼近是什么?
注: - 这全部都是严格的编译时,而不是运行时。 - 对于class A来说,不能有任何更改。 我只能修改有关A和B的定义以及知道两者的代码。 其他人仍将使用class A,如果我想让我的代码与他们的交互,我也会使用它。 - 最好可以扩展到多个超类。 因此,也许我还有类C{protected:int x; double w; public:void baz();},它也应该像A的超类一样运行。

@NathanOliver:是的。稍微修改一下那句话,我希望在知道它们两个的代码中,A和B的行为就像A继承了B一样。 - einpoklum
4
你的B类更像是一个契约(contract),你可以在使用该库的代码中使用概念(concepts)/模板(templates)吗? - Zdeněk Jelínek
2
@Galik CRTP只有在基类和派生类都知道发生了什么情况时才有用。A不是一个模板,所以你不能使用CRTP从它继承,而且它已经不从B继承。 - Daniel H
1
而不是使用类似继承的指针,如果编译器直接知道如何从A转换为B,那么它是否会起作用? 如果是这样,在 B 中,您可以编写一个隐式转换构造函数。 - Daniel H
1
@ZdeněkJelínek可能有最好的想法。您不需要按语言定义的实际合同;只需使所有需要B的内容成为模板,并像所有必需的成员都在那里一样运行。如果它们不存在,则编译将失败。 - Daniel H
显示剩余10条评论
5个回答

28
您可以执行以下操作:
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(); }
};

该想法是使用类型擦除(这就是InterfaceInterfacer<T>类)在底层,允许C接受您可以调用bar的任何东西,然后您的库将获取类型为C的对象。


接口应该有一个const T& t字段吗?复制可能会带来一些问题。 - einpoklum
7
有了引用,你就会遇到对象生命周期的困难。选择权在于你。 - SirGuy
1
@einpoklum 或者说,取决于你的情况哪个更好。我认为如果你选择引用的路线,每个派生类型都会有可预测的大小。我觉得你可以利用这一点来避免使用 new 分配内存。 - SirGuy
3
请注意,这种技术完全可以在不进行动态分配的情况下完成,特别是如果您只想要一个现有对象的“视图”或引用,而没有寿命延长。 - Yakk - Adam Nevraumont
为什么不彻底做到底,让API采用一个显式的“接口”,B实现它,并提供一个围绕A的“包装器”来实现它呢? - jpmc26
@jpmc26,“接口作为实现细节”及其与类型擦除的鸭子类型之间的关系有许多优点。您的解决方案肯定也可以工作,但我喜欢避免实现接口(即使只是因为我不必让所有类都具有虚拟析构函数和/或克隆方法)。 - SirGuy

15

我知道C++没有相反的机制 - “超类一个已知的类”

哦,它确实有:

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

然后你就可以出发了。不用说,这一切都在编译时完成。


如果你有一个不能更改的 class A 并且需要将其插入继承结构中,则可以使用以下方法:

template<class Superclass>
class Class : public A, public Superclass
{
};
请注意,dynamic_cast 可以将 Superclass* 指针转换为 A* 指针,反之亦然。同样适用于 Class* 指针。这时,您已经逐渐接近组合、特征和概念的概念。

4
OP提到他无法更改派生类的定义。 - Zdeněk Jelínek
1
@ZdeněkJelínek是正确的,也许我会编辑以强调这一点。如果您仍然认为基于CRTP的机制可以解决问题,请更具体地说明。 - einpoklum
1
在我看来,由于缺少太多具体信息,很难给出完整的解决方案。我的意图是在一般意义上提供一系列可能性。但是如果我漏掉了什么显而易见的东西,请务必进行 DV(投反对票)。如果得分低于零,我承诺删除。 (我个人不喜欢 +10 / -2 的不对称性)。 - Bathsheba
1
我想知道那是否是标准的C++语言。不要犹豫,可以向社区咨询。 - Bathsheba
2
这是我关于这个问题的链接 - SirGuy
显示剩余8条评论

5

普通模板可以做到这一点,编译器会在您使用不正确时通知您。

而不是

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 接口来完成某些操作的类型。


  1. 但是如果我使用模板,难道不应该更喜欢void Bconsumer(B& b)BConsumer(Blike& b)这种引用版本,而不是指针版本吗?
  2. 你对于ABSubclass之间的关系有什么建议?
- einpoklum
1
  1. 这些特定的函数是修改后的示例。
  2. BSubclass<A>::xplusz 使用基类 A::x 和 A::z 成员。它存在的目的是为了表明任何出现 B 的地方都可以替换为 A
- Caleth

4
没有办法在不改变类的情况下改变其行为。一旦定义了A,就没有添加父类的机制。
引用:我只能修改B的定义和知道A和B的代码。
你不能改变A,但可以更改使用A的代码。因此,你可以使用另一个继承自B的类(我们称之为D),而不是使用A。我认为这是最接近所需机制的方法。
如果有用的话,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的行为方式与仅继承BCA类的行为方式完全相同。
公共继承A将允许消除所有的委托样板代码。
class D : public A, public B, public C {
public:
    D(const A&);
};

然而,我认为这可能会在使用不了解B的代码和了解B(因此使用D)的代码之间造成混淆。使用D的代码可以轻松处理A,但反过来则不行。完全不继承A,而是使用成员变量,则可以避免复制A以创建D,而是引用现有的A。
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();}
};

然而,这只是一个选项,前提是其他不知道 BD 的代码也使用了共享指针。


已经有使用 A 的代码了,不仅仅是 A 的定义本身。如果没有的话,我可以忘记 A 并定义其他东西,只使用那个定义。 - einpoklum
@einpoklum,是什么阻止了你?为什么不改变代码呢? - eerorika
  1. 因为那会使我的问题无意义。
  2. 因为它不是我要修改的代码;事实证明世界上还有其他人... :-P
- einpoklum
1
如果您既不能更改A,也不能更改使用A的代码,那么您实际上无法更改代码的行为(至少在C++范围内,除非A提供了一些在运行时修改其行为的钩子)。因此,如果这是您的情况,那么也许真正的问题就是无意义的。看起来您无法得到想要的结果。 - eerorika
@einpoklum,请编写B和D。使用D并使A存在在其他地方。其他不需要更改 - 但您也不能使它们表现得好像A继承了B一样。 - eerorika
显示剩余4条评论

3
这似乎更像是静态多态而非动态多态。正如@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");
}

这里提供的代码应该能够满足您的需求(即对任何类都使用bar函数),而不需要使用虚函数查找,也不需要修改类。这种间接性应该被内联优化,以达到运行时直接调用bar函数的效率。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接