多态和 shared_ptr 成员函数

7

测试多态性&虚函数&共享指针,我试图理解以下最小示例描述的情况。

class B{
public:
  // Definition of class B
  virtual void someBMethod(){
   // Make a burger
  };
};

class C : public B {
public:
  // Definition of class C
  void someBMethod(){
   // Make a pizza
  };
};

class A{
public:
  A(B& SomeB) : Member(std::make_shared<B>(SomeB)){};
  std::shared_ptr<B> Member;
};

现在,主要的内容可以是:

int main(){
  C SomeC;
  A SomeA(SomeC);
  A.Member->someBMethod(); // someBMethod from B is being executed.
};

除非我在最小示例中遗漏了一些错误,否则我认为SomeC被切片到B,或者至少在最后一行调用了BsomeBMethod。问题是:应该如何正确初始化Member,以便调用CsomeBMethod方法?

Member(std::make_shared<B>(SomeB)) 通过隐式的 B(const B&) 拷贝构造函数创建了一个新的 B 实例,该实例与原始的 C 实例没有任何关系。 - Piotr Skotnicki
谢谢 @PiotrS。你知道我的问题的答案吗?即,正确的初始化方式是什么? - Kae
你的目标是什么?std::make_shared<B> 明确表示“构建一个 B 类型的实例”,而不是“构建一个与 SomeB 相同类型的实例,然后将其向上转换并存储在我的 shared_ptr 中”。 - Piotr Skotnicki
@PiotrS。我的目标是让类A拥有一个指向B的成员,但我可以将任何派生自B的类放在那里并调用其方法(派生类的方法)。 - Kae
2个回答

8
你正在通过调用std::make_shared<B>(SomeB)执行切片。这将构造一个指向B类型的新对象的shared_ptr,并使用B上的复制构造函数B::B(const B& b)构造该对象,从而削减有关SomeB的C特性的所有信息。
将A更改为:
class A{
public:
  A(const std::shared_ptr<B>& pB) : pMember(pB) {}
  std::shared_ptr<B> pMember;
};

和主要:

int main(){
  A SomeA(std::make_shared<C>());
  A.pMember->someBMethod(); // someBMethod from C is being executed.
}

谢谢。你知道我的问题的答案吗?即,初始化的正确方式是什么? - Kae
@PiotrS. 我会称之为切片:将多态对象复制到一个新的超类型对象中。 - BeyelerStudios
切片是指将子类的实例按值传递/分配给超类类型。 - Piotr Skotnicki
2
@PiotrS. 这是通过使用超类的复制构造函数完成的 - 你能解释一下这里的区别吗? - BeyelerStudios

3
我认为 SomeC 被切片到了一个 B
这就是正在发生的事情。make_shared 会创建一个指定类型的新对象,并将其参数转发给适当的构造函数。因此,这会创建一个新的 B,使用其拷贝构造函数初始化以复制 SomeCB 子对象。
在这种情况下,正确的初始化 Member 的方式应该是什么,以便从 C 中调用 someBMethod 方法?
这很棘手:虽然 C 不是共享的,但 Member 是共享的,你不能两者兼得。最好让用户传入一个共享指针,表明它将与该类共享:
A(std::shared_ptr<B> SomeB) : Member(SomeB){}

如果你真的想允许它使用非共享对象,你可以创建一个带有虚拟删除器的共享指针,这样它就不会尝试共享所有权:

A(B& SomeB) : Member(std::shared_ptr<B>(&SomeB, [](B*){})){}

但要注意,现在您需要负责确保在 A 和它的任何副本不再需要 C 之前,C 不被销毁。您已经失去了“拥有”共享指针的安全性。
无论您做什么,都不要简单地从 &SomeB 创建一个共享指针。默认删除器将尝试删除它,这是一个错误,因为它不是动态创建的。

有趣!我还有一个问题,但我认为它太琐碎了,如果我将其作为独立帖子发布,它不会受到好评。我的想法是创建一个类A,其中包含一个像插件一样工作的成员。这样,A可以使用插件的动态类型功能。您能否扩展您的答案,包括一些关于此的想法? - Kae
当然,您对A构造函数的更改现在避免了对C的切片。所以,它正在做我想要的事情。我在上面的评论中想问的是,这是否是拥有“插件成员”的常见方式? - Kae
@Karene:那么我认为答案是一样的:最好的选择是让A使用shared_ptr<Plugin>,并始终使用智能指针来管理插件。 - Mike Seymour
抱歉,我想我找到了一种方法,它看起来更好。你能帮我判断它是否令人满意吗?这个想法是:在A的构造函数中,在初始化中放置A() : Member(&SomeB){};。这样指针Member就得到了SomeB的地址,就这样。 - Kae
@Karene:不,那样会出大问题——共享指针将尝试删除对象,但是该对象不应被删除,因为它并非动态分配的。如果您真的、真的想要使用非动态对象,出于某种原因,请像我第二个建议中那样给共享指针一个虚拟删除器。并且请非常小心地处理对象和指针的生命周期。 - Mike Seymour
我明白了。当我运行它时,我看到了后果。多态函数被调用得很好,但当程序开始封闭时,它会抱怨。 - Kae

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