【问题标题】:Can we outsmart object slicing?我们可以超越对象切片吗?
【发布时间】:2017-12-28 18:52:24
【问题描述】:

免责声明:仅当您想了解它是如何完成时才继续阅读。显然这是错误的,当你看到它时你会患上眼癌(我免疫,因为我是菜鸟:)


所以我考虑了一段时间 C++ 中的运行时多态性,我想出了以下内容。考虑一下这个

#include <iostream>

struct Animal
{
    virtual ~Animal(){}
    void make_noise() const {return do_make_noise();}
    protected:
    Animal( ){}
    virtual void do_make_noise()const{std::cout << "Meh\n";}
};
struct Cat:public Animal
{
    void do_make_noise()const{ std::cout<< "meow\n";}
};
struct Dog:public Animal
{
    void do_make_noise()const{ std::cout<< "woof\n";}
};

int main()
{
    Cat cat;
    Dog dog;
    Animal a((const Animal&)cat); //every tutorial teaches us that this is bad!!
    a.make_noise();
    a = (const Animal&)dog;
    a.make_noise();
    return 0;
}

如你所料,输出是

Meh
Meh

好的,对象切片很糟糕:/
但是现在让我稍微改变一下 Base 类:

struct Animal
{
    virtual ~Animal(){}
    void make_noise() const {return ptr_->do_make_noise();}
    protected:
    Animal( ):ptr_(this){}
    Animal* ptr_;
    virtual void do_make_noise()const{std::cout << "Meh\n";}
};

那么结果就是

meow
woof

哈哈。螺丝对象切片...
你怎么看呢?在我看来,能够按值复制并仍然获得多态行为有很多优点。人们不使用它有什么原因吗?还是我只是在这里重新发明轮子?

【问题讨论】:

  • 这听起来像是无效指针取消引用灾难的秘诀。
  • 现在你有一个对象 A 包含一个指向对象 B 的(非拥有的)指针。如果对象 B 先被销毁,你就有问题了。
  • 如果你的子类有任何成员变量,切片仍然会搞砸。
  • 不要认为你获得的收益相对于你必须投入的工作以使其稳定。您需要修复三规则违规,并且您将始终遇到上面讨论的不匹配的生命周期问题。再加上 Animal` 的所有后代都随身携带 ptr_ 并且永远不会使用它。我可以推荐使用std::unique_ptr 吗?
  • 生命周期依赖在Animal * a = &amp;cat; 中很明显,但对于您的Animal a = cat; 则不是。

标签: c++ inheritance object-slicing


【解决方案1】:

您编写的代码不会表现出未定义的行为,并且确实会“阻止对象切片”;相反,它非常脆弱且无法维护。

现在每个Animal 都拥有一个非托管指针,指向它所充当的某个 ur-Animal(有时是它自己)。您的代码违反了合理复制和移动构造的含义。

实际上,它避免了崩溃和未定义的行为,但只是偶然。看似无害的微小调整,您的程序就会中断。

写一个返回动物的函数?您的代码会中断,具体取决于省略。从本地 Animal 复制到传入的引用,然后返回?破碎的。把它们放在标准向量中?彻底的灾难。

创建不受切片影响的值语义多态类型看起来有点像这样,但是您将虚拟层次结构从 pImpl 拥有的值类型中分离出来,而实例拥有 pImpl 指向的内容。

struct IAnimal{
  virtual void speak() const=0;
  virtual std::unique_ptr<IAnimal> clone() const = 0;
  virtual ~IAnimal(){}
};
struct Animal {
  std::unique_ptr<IAnimal> pImpl;
  void speak() const { if (pImpl) pImpl->speak(); }
  explicit operator bool()const{return (bool)pImpl;}
  Animal(Animal&&)=default;
  Animal& operator=(Animal&&)=default;
  Animal(Animal const&o):
    Animal(o.pImpl?Animal(o.pImpl->clone()):Animal())
  {}
  Animal()=default;
  Animal& operator=(Animal const& o){
    Animal tmp(o);
    swap(pImpl, tmp.pImpl);
    return *this;
  }
  Animal(std::unique_ptr<IAnimal> x):pImpl(std::move(x)){}
};

现在Animal 不能被切片,可以是多态的。

struct Dog:Animal{
  struct IDog:IAnimal{
    std::unique_ptr<IAnimal> clone()const final override{ return std::make_unique<IDog>(*this); }
    void speak()const final override { std:cout <<"woof\n"; }
  };
  Dog():Animal(std::make_unique<IDog>()){}
};  
struct Cat:Animal{
  struct ICat:IAnimal{
    std::unique_ptr<IAnimal> clone()const final override{ return std::make_unique<ICat>(*this); }
    void speak()const final override { std:cout <<"meow\n"; }
  };
  Cat():Animal(std::make_unique<ICat>()){}
};

除非我们得到 tpyos:

Cat cat;
Dog dog;
Animal a((const Animal&)cat); //every tutorial teaches us that this is bad!!
a.speak();
a = (const Animal&)dog;
a.speak();

工作。

而且我的复制/移动 ctor 是明智的,而且我在向量中工作,而且我没有悬空指针。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-11
    • 1970-01-01
    • 2011-04-29
    • 2018-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多