要在堆上创建实例并保持多态,这将给出正确的答案:
class Father
{
public:
virtual void Say()
{
cout << "Father say hello" << endl;
}
};
class Son : public Father
{
public:
void Say()
{
cout << "Son say hello" << endl;
}
};
int main()
{
std::vector<Father*> v;
std::cout << 1 << std::endl;
for(int i(0); i<5; i++)
{
auto p = new Son(); ---------------on heap
v.emplace_back(p);
}
for(auto p : v)
{
p->Say();
}
}
但是当我想在堆栈上创建一个实例时,这似乎并不容易:
版本1:
class Father
{
public:
virtual void Say()
{
cout << "Father say hello" << endl;
}
};
class Son : public Father
{
public:
void Say()
{
cout << "Son say hello" << endl;
}
};
int main()
{
std::vector<Father> v;
for(int i(0); i<5; i++)
{
auto o = Son(); ---------------on stack
v.emplace_back(o);---------------now "o" is cast to Father type
}
for(auto o : v)
{
o.Say();------------------------only output "Father say hello"
}
}
第2版:
class Father
{
public:
virtual void Say()
{
cout << "Father say hello" << endl;
}
};
class Son : public Father
{
public:
void Say()
{
cout << "Son say hello" << endl;
}
};
int main()
{
std::vector<Father*> v;
for(int i(0); i<5; i++)
{
auto p = &Son(); --------------On the stack
v.emplace_back(p);---------------Now "o" is cast to Father type
}
for(auto p : v)
{
p->Say();------------------------Since "p" now is a Wild pointer, it'll fail too
}
}
这可以解决吗?还是仅仅是一个死胡同:如果我想使用多态性,那么我必须在堆上创建一个对象。
答案 0 :(得分:2)
通常,多态不需要动态分配。这是一个常见的误解,因此这里有一个反例:
void foo(const Father& f) { f.Say(); }
Son s;
foo(s);
您必须将Say
声明为const
才能使其正常工作,但是它将打印预期的Son say hello
。您需要用于多态的引用或指针,而不必动态分配!
话虽如此,当您想要一个派生类的容器时,std::vector<Father>
不会这样做。公共继承对“ is-a”关系进行建模,因此Son
是Father
,但是Father
不是Son
(请注意,父亲-儿子比喻是?!?)。因此,当您将Son
放入Father
s的向量中时,对象将被切片,并且仅Father
部分将被存储在向量中(有关“对象切片”的信息)。 / p>
此外,auto p= &Son();
是错误的,因为创建的对象是临时对象,并且其生存期在该行的结尾处结束。您存储在向量中的指针是悬空的(它指向寿命已结束的对象)。
要将指针存储在容器中,可以使用动态分配。例如,使用std::unique_ptr
s:
int main()
{
std::vector<std::unique_ptr<Father>> v;
for(int i(0);i<5;i++){
v.emplace_back(new Son);
}
for(auto& p:v){
p->Say();
}
}
请注意,您必须对基于循环的范围使用auto&
,因为unique_ptr
不会被复制。 unique_ptr
做的工作很脏:unique_ptr
被销毁时(即向量超出范围时),对象将被自动删除。
答案 1 :(得分:1)
这是一个反复出现的问题/难题:您可以牺牲一些样板代码来维护值语义。这是这种想法的一个最小的可行示例:
#include <iostream>
#include <memory>
#include <vector>
class Father
{
protected:
struct Father_Interface
{
virtual void
Say() const
{
std::cout << "Father say hello" << std::endl;
}
};
using pimpl_type = std::shared_ptr<const Father_Interface>;
pimpl_type _pimpl;
Father(const Father_Interface* p) : _pimpl(p) {}
public:
Father() : Father{new Father_Interface{}} {}
void Say() const { _pimpl->Say(); }
};
class Son : public Father
{
protected:
class Son_Interface : public Father_Interface
{
void
Say() const override
{
std::cout << "Son say hello" << std::endl;
}
};
public:
Son() : Father{new Son_Interface{}} {}
Son& operator=(const Father&) = delete; // fight against object slicing
};
int
main()
{
std::vector<Father> v;
v.emplace_back(Father());
v.emplace_back(Son());
v.emplace_back(Father());
for (const auto& v_i : v)
{
v_i.Say();
}
}
打印:
Father say hello Son say hello Father say hello
您还可以阅读以下内容:
答案 2 :(得分:0)
您做错了很多事情。首先,这是您如何正确执行操作:
myData()[...
基本上,您需要在堆栈上分配对象,以便这些对象只要向量就可用。
现在您所做的是未定义的行为,因为基本上(在向量中)有一个指针已指向已取消分配的对象(即使您修复了注释中已经说过的内容)。
int main()
{
Father father1;
Son son1;
Father father2;
Son son2;
std::vector<Father*> v;
v.emplace_back(&father1);
v.emplace_back(&son1);
v.emplace_back(&father2);
v.emplace_back(&son2);
for (auto p : v) {
p->Say();
}
}
为此: for(int i(0);i<5;i++){
Son s;
v.emplace_back(&s);
// s lifetime end here, but the vector still has pointers to objects that are de-allocated
}
。
我认为您尝试了完全不同的方法:父亲对象v.emplace_back(p);---------------now "o" is cast to Father type
的向量,并且如果尝试向其中添加Son元素,则会得到 object slicing // //我只是添加了一点您可以查找它,这不是这里的重点
答案 3 :(得分:0)
这个问题与堆栈无关。您实际上是在问按值存储时如何实现多态。如果您可以使用C ++ 17并因此提供std::variant
,这并不是很难。
实现非常简单:
#include <algorithm>
#include <cassert>
#include <variant>
#include <vector>
enum class Who { Father, Son };
struct ISayer {
virtual Who Me() const = 0;
virtual ~ISayer() {};
};
struct Father final : ISayer {
Who Me() const override { return Who::Father; }
};
struct Son final : ISayer {
Who Me() const override { return Who::Son; }
};
struct AnySayer0 : std::variant<Father, Son>
{
using variant_type = std::variant<Father, Son>;
using variant_type::variant;
operator const ISayer &() const {
return std::visit([](auto &val) -> const ISayer &{ return val; },
static_cast<const variant_type &>(*this));
}
operator ISayer &() {
return std::visit([](auto &val) -> ISayer &{ return val; },
static_cast<variant_type &>(*this));
}
const ISayer *operator->() const { return &static_cast<const ISayer &>(*this); }
ISayer *operator->() { return &static_cast<ISayer &>(*this); }
};
using AnySayer = AnySayer0;
int main()
{
std::vector<AnySayer> people;
people.emplace_back(std::in_place_type<Father>);
people.emplace_back(std::in_place_type<Son>);
assert(people.front()->Me() == Who::Father);
assert(people.back()->Me() == Who::Son);
}
另一种实现方式AnySayer1
需要更多样板,也许会更快一些-但它也会更小吗?
struct AnySayer1
{
template <typename ...Args>
AnySayer1(std::in_place_type_t<Father>, Args &&...args) :
father(std::forward<Args>(args)...), ref(father) {}
template <typename ...Args>
AnySayer1(std::in_place_type_t<Son>, Args &&...args) :
son(std::forward<Args>(args)...), ref(son) {}
~AnySayer1() { ref.~ISayer(); }
operator const ISayer &() const { return ref; }
operator ISayer &() { return ref; }
const ISayer *operator->() const { return &static_cast<const ISayer &>(*this); }
ISayer *operator->() { return &static_cast<ISayer &>(*this); }
AnySayer1(AnySayer1 &&o) : ref(getMatchingRef(o)) {
if (dynamic_cast<Father*>(&o.ref))
new (&father) Father(std::move(o.father));
else if (dynamic_cast<Son*>(&o.ref))
new (&son) Son(std::move(o.son));
}
AnySayer1(const AnySayer1 &o) : ref(getMatchingRef(o)) {
if (dynamic_cast<Father*>(&o.ref))
new (&father) Father(o.father);
else if (dynamic_cast<Son*>(&o.ref))
new (&son) Son(o.son);
}
AnySayer1 &operator=(const AnySayer1 &) = delete;
private:
union {
Father father;
Son son;
};
ISayer &ref;
ISayer &getMatchingRef(const AnySayer1 &o) {
if (dynamic_cast<const Father *>(&o.ref))
return father;
if (dynamic_cast<const Son *>(&o.ref))
return son;
assert(false);
}
};
可以使用使std::variant
工作的相同“魔术”来重写它-这样可以减少重复。
但是-它较小吗?
static_assert(sizeof(AnySayer1) == sizeof(AnySayer0));
不。至少在gcc和clang中,两个实现的大小都相同。这很有意义,因为std::variant
不需要存储比我们更多的信息-它只需要保持某种方式来区分类型即可。我们选择使用对ISayer
的引用并使用动态类型信息,因为在转换为接口类型时,这种情况针对常见情况进行了优化-我们存储了可供使用的引用。 std::variant
不能假定类型具有相同的基数,而是存储整数类型索引,并使用生成的代码在该索引上分派。使用访问者返回引用的路径通常会比较慢-但不是必须的,因为编译器可以注意到两种类型的ISayer
vtable指针都位于同一位置,并且可以压缩该类型到“有价值与无价值”的测试。似乎所有主要C ++编译器(gcc,clang和MSVC)的最新版本都可以轻松地处理此问题,并生成与我们的“优化” AnySayer1
一样快的代码。