【问题标题】:What's the closest thing in C++ to retroactively defining a superclass of a defined class?C++ 中最接近于追溯定义已定义类的超类的是什么?
【发布时间】:2017-11-03 21:41:21
【问题描述】:

假设我有课

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*s 就可以工作。然后请实际接受A* 作为B*(和A& 作为B& 等)。

在 C++ 中,我们可以用另一种方式做到这一点,即如果 B 是我们无法控制的类,我们可以使用 class A : public B { ... } 执行“子类化已知类”操作;而且我知道 C++ 没有相反的机制——“通过新的 B 类对已知的 A 类进行超类化”。我的问题是 - 这种机制最接近的可实现近似值是什么?

注意事项:

  • 这都是严格的编译时,而不是运行时。
  • class A 可能没有任何更改。我只能修改B 的定义和知道AB 的代码。其他人仍将使用 A 类,如果我希望我的代码与他们的代码交互,我也会如此。
  • 这应该最好“可扩展”到多个超类。所以也许我也有class C { protected: int x; double w; public: void baz(); },它的行为也应该像A 的超类。

【问题讨论】:

  • 那么,您希望AB 继承而不实际更改A
  • 你的class B不是更多的合同吗?那么你能在消费库端使用概念/模板吗?
  • @Galik CRTP 只有在基础和派生都知道发生了什么的情况下才有用。 A 不是模板,所以不能使用 CRTP 继承它,它已经不继承自 B
  • 如果编译器只是新的如何直接从A 转换为B,而不是使用类似继承的指针,它会起作用吗?如果是这样,那么在B,你可以写一个隐式转换构造函数。
  • @ZdeněkJelínek 可能有最好的主意。您不需要语言定义的实际合同;只需将所有需要B 的东西都设为模板,并表现得像所有必需的成员都在那里。否则编译将失败。

标签: c++ superclass subclassing idioms


【解决方案1】:

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

哦,是的:

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

然后你就走了。不用说,都是在编译时。


如果您有一个无法更改的class A,并且需要将其插入到继承结构中,请使用以下行中的内容

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

请注意,dynamic_cast 将在给定 Superclass* 指针的情况下到达 A* 指针,反之亦然。同上Class* 指针。在这一点上,您已经接近 CompositionTraitsConcepts

【讨论】:

  • OP提到他不能改变派生类的定义。
  • @ZdeněkJelínek 是对的;也许我会编辑以强调这一点。如果您仍然认为基于 CRTP 的机制可以解决问题,请更加具体。
  • 恕我直言,由于缺少太多细节,因此很难为该问题提供完整的解决方案。我在这里的目的是提供一系列一般意义上的可能性。但是,如果我错过了一些显而易见的事情,请 DV。如果分数低于零,我将承诺删除。 (我个人不喜欢 +10 / -2 不对称)。
  • 如果A没有虚方法,dynamic_cast会找到A吗?
  • 好吧,here's 我有这个问题
【解决方案2】:

您可以执行以下操作:

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&lt;T&gt; 类)以允许C 获取您可以调用bar 的任何内容,然后您的库将获取对象输入C

【讨论】:

  • 接口不应该有const T&amp; t 字段吗?制作副本可能会比较麻烦。
  • @einpoklum 有了引用,您就会遇到对象生命周期的困难。由你决定。
  • @einpoklum 或者更确切地说,取决于你的情况,哪个更好。我认为如果你走参考路线,每个派生类型都会有一个可预测的大小。我觉得您也可以利用这一点来避免使用 new 分配内存。
  • 请注意,这种技术可以在没有完全动态分配的情况下完成,特别是如果您只想要一个“视图”或对现有对象的引用而不需要延长生命周期。
  • 为什么不全力以赴,让 API 采用 B 实现的显式“接口”,然后在 A 周围提供一个“包装器”来实现它?
【解决方案3】:

如果不改变类,就无法改变类的行为。在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 继承了BC

公开继承A 将允许摆脱所有委托样板:

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

但是,我认为这可能会在不知道 B 的情况下使用 A 的代码和使用知道 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();}
};

但是,如果不知道 BD 的其他代码也使用共享指针,这可能只是一种选择。

【讨论】:

  • 已经有代码使用了 A,而不仅仅是定义 A 本身。如果没有,我可以忘记 A 并定义其他东西,只使用它......
  • @einpoklum 那么是什么阻止了你?为什么不改代码?
  • 1.因为那会让我的问题变得毫无意义。 2. 因为改变的不是我的代码;原来世界上还有其他人...... :-P
  • @einpoklum 如果你既不能改变A,也不能改变使用A的代码,那么你真的不能改变代码的行为方式(至少在C++的范围内,除非A提供了一些在运行时修改其行为的钩子)。因此,如果这是您的情况,那么真正的问题可能 没有实际意义。看来你得不到你想要的。
  • 我正在写B。然后会有新的代码,它依赖于B,我可以对其施加限制,或者至少是指导方针。但是 A 本身就存在,并且被那些不应该在我的帐户上更改代码的人愉快地使用。
【解决方案4】:

普通模板会这样做,当你使用不当时编译器会通知你。

而不是

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&lt;A&gt;BSubclass&lt;B&gt;,作为使用B 接口做某事的类型。

【讨论】:

  • 1.但是,如果我使用模板,我会不会更喜欢void Bconsumer(B&amp; b)BConsumer(Blike&amp; b) 而不是指针版本? 2.您建议ABSubclass的关系是什么?
  • 1.特定功能是修改外观的示例。 2. BSubclass&lt;A&gt;::xplusz 使用基本的 A::x 和 A::z 成员。它的存在表明B 出现的任何地方都可以替换为A
【解决方案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");
}

这应该可以满足您的需求(即对任何类使用 bar),而无需借助 vtable 查找,也无需修改类。这种间接级别应该通过设置适当的优化标志来内联。换句话说,您将拥有直接调用 bar 的运行时效率。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-03
    • 2019-07-21
    • 1970-01-01
    • 2018-03-25
    相关资源
    最近更新 更多