什么是对象切片?

Fra*_*nia 709 c++ inheritance c++-faq object-slicing

有人在IRC中提到它作为切片问题.

Dav*_*ben 586

"切片"是指将派生类的对象分配给基类的实例,从而丢失部分信息 - 其中一些信息被"切片"掉.

例如,

class A {
   int foo;
};

class B : public A {
   int bar;
};
Run Code Online (Sandbox Code Playgroud)

所以类型的对象B有两个数据成员,foobar.

然后,如果你写这个:

B b;

A a = b;
Run Code Online (Sandbox Code Playgroud)

然后b关于成员的信息bar丢失了a.

  • 非常有用,但请参阅http://stackoverflow.com/questions/274626#274636以获取在方法调用期间如何进行切片的示例(这强调了比普通分配示例更好的危险). (66认同)
  • 有趣.我已经用C++编程了15年,这个问题从来没有发生在我身上,因为我总是通过引用传递对象作为效率和个人风格的问题.去展示好习惯如何帮助你. (52认同)
  • 这不是"切片",或者至少是它的良性变体.如果你做`B b1,就会出现真正的问题; B b2; A&b2_ref = b2; b2 = b1`.您可能认为已将`b1`复制到`b2`,但您还没有!你已经将`b1`的*部分*复制到`b2`('b1`的部分'B'继承自`A`),并且保持`b2`的其他部分不变.`b2`现在是一个弗兰肯斯坦生物,由几个"b1"组成,后面跟着一些"b2".啊! Downvoting因为我认为答案非常具有误导性. (35认同)
  • @fgp你的评论应该是'B b1; B b2; A&b2_ref = b2; b2_ref = b1`"_如果you_"......从具有非虚拟赋值运算符的类派生,则会出现真正的问题."A"甚至打算用于派生吗?它没有虚拟功能.如果从类型派生,则必须处理可以调用​​其成员函数的事实! (23认同)
  • @Felix谢谢,但我不认为反投(因为不是指针算术)会起作用,`A a = b;``a`现在是`A`类型的对象,它有'B :: foo`的副本.我认为现在把它归还是错误的. (9认同)
  • @Karl:在分配的情况下它没有帮助.`B&b = xxx; b = someDerivedClass();`仍然引发切片.通常情况下,问题会被忽视. (5认同)
  • 这个答案是错误的。这里不会丢失任何信息。`a` 只能保存一个字段,并从 `b` 获取该字段的副本。它永远不应该处理“b”可能具有的任何其他字段。 (4认同)
  • @Hades:我明白你的意思,我在考虑指点.你是对的,赋值当然不是强制转换 - 实际上,在堆栈上分配了一个新对象.然后`b`中的`bar`根本没有被破坏,根本不被编译器生成的赋值运算符复制,所以`a`现在是一个类型为'A`的全新对象,成员`a.foo`设置为与`b.foo`相同的值. (3认同)
  • 另一个解释切片但不是问题的答案。 (2认同)

fgp*_*fgp 476

这里的大多数答案都无法解释切片的实际问题是什么.他们只解释切片的良性情况,而不是危险的情况.假设,像其他的答案,你正在处理两班AB,其中B导出(公开)的A.

在这种情况下,C++,您可以通过一个实例BA的赋值运算符(同时也拷贝构造函数).这是有效的,因为一个实例B可以转换为a const A&,这就是赋值运算符和复制构造函数期望它们的参数.

良性的情况

B b;
A a = b;
Run Code Online (Sandbox Code Playgroud)

没有什么不好的事情发生在那里 - 你要求一个实例A是副本B,而这正是你得到的.当然,a不会包含一些b成员,但应该怎么做?这是一个A,毕竟不是一个B,所以它甚至还没有听说过关于这些成员,更不用说将能够存储它们.

奸诈的情况

B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
Run Code Online (Sandbox Code Playgroud)

您可能认为这b2将是b1之后的副本.但是,唉,事实并非如此!如果你检查它,你会发现它b2是一个科学怪人的生物,由一些块b1(B从中继承的块A)和一些b2(仅B包含的块)组成.哎哟!

发生了什么?好吧,默认情况下,C++不会将赋值运算符视为virtual.因此,该行将a_ref = b1调用赋值运算符A,而不是B.这是因为对于非虚函数,声明的类型(即A&)确定调用哪个函数,而不是实际类型(Ba_ref引用实例以来B).现在,A的赋值运算符显然只知道声明的成员A,因此它只会复制那些成员,使成员的添加B不变.

一个办法

仅分配给对象的某些部分通常没什么意义,但遗憾的是,C++没有提供禁止内容的内置方法.但是,您可以自己动手.第一步是使赋值运算符成为虚拟.这将保证它始终是被调用的实际类型的赋值运算符,而不是声明的类型.第二步是用于dynamic_cast验证分配的对象是否具有兼容类型.第三步是在(受保护!)成员中进行实际任务assign(),因为B他们assign()可能想要使用A's assign()来复制A成员.

class A {
public:
  virtual A& operator= (const A& a) {
    assign(a);
    return *this;
  }

protected:
  void assign(const A& a) {
    // copy members of A from a to this
  }
};

class B : public A {
public:
  virtual B& operator= (const A& a) {
    if (const B* b = dynamic_cast<const B*>(&a))
      assign(*b);
    else
      throw bad_assignment();
    return *this;
  }

protected:
  void assign(const B& b) {
    A::assign(b); // Let A's assign() copy members of A from b to this
    // copy members of B from b to this
  }
};
Run Code Online (Sandbox Code Playgroud)

请注意,为了方便起见,它们Boperator=共同覆盖返回类型,因为它知道它正在返回一个实例B.

  • 我不明白你的"奸诈"案件中有什么坏事.你声明你想要:1)获得对A类对象的引用,2)将对象b1转换为A类并将其东西复制到A类的引用中.这里真正错误的是背后的正确逻辑给定的代码.换句话说,你拍了一个小图像框(A),把它放在一个更大的图像(B)上,然后你画过那个框架,后来抱怨你的大图现在看起来很难看:)但是如果我们只考虑那个框架区域,它看起来很不错,正如画家想要的那样,对吧?:) (15认同)
  • 另一种常见方法是简单地禁用复制和赋值运算符.对于继承层次结构中的类,通常没有理由使用值而不是引用或指针. (14认同)
  • 什么?我不知道运营商可以被标记为虚拟 (13认同)
  • 不同的是,问题是C++默认假设一种非常强大的*可替代性* - 它要求基类的操作在子类实例上正常工作.甚至对于编译器自动生成的操作也是如此.因此,在这方面不要搞砸自己的操作是不够的,还必须明确禁用编译器生成的错误操作.或者当然,远离公共继承,这通常是一个很好的建议;-) (12认同)
  • 恕我直言,问题在于可以通过继承隐含两种不同的可替代性:任何`derived`值都可以给予期望`base`值的代码,或者任何派生的引用可以用作基本引用.我希望看到一种带有类型系统的语言,它可以分别处理这两个概念.在许多情况下,派生引用应该可替代基本引用,但派生实例不应该替换为基本引用; 在许多情况下,实例应该是可转换的,但引用不应该替代. (11认同)
  • "奸诈"的案件确实是危险的,我之前没有考虑过.但是你所谓的"良性"根本就不是良性的 - 几乎100%的时候,编写这段代码是错误的,并且语言应该被设计成产生编译错误.LSP意味着派生对象在被视为基类对象时应*行为正确,但"行为"仅表示"对公共方法调用的响应".LSP允许*内部状态*(什么`operator =()`副本)在`B`中任意重新定义,因此将其复制到`A`实例甚至可以产生UB. (4认同)
  • 这里的许多答案,特别是这个答案,很好地解释了_what_对象切片.但考虑到这个Q&A的受欢迎程度,还可以提到一个人可以通过确保_non-leaf基类应该是抽象的来完全避免这个问题(参见例如Scott Meyers中的第33项_More Effective C++). (3认同)
  • 在.NET中,每个结构类型都有一个可以隐式转换的关联堆对象类型.如果结构类型实现接口,则可以在"就地"结构上调用该接口的方法.如果有一个带有`T`类型字段的结构`FooStruct <T>`实现接口`IFoo <T>`包含可以读写字段的类型`T`属性`Foo`,那么如果`FooStruct < Cat>`继承自`FooStruct <Animal>`,它应该实现`IFoo <Animal>`,暗示一个人应该能够存储一个`Animal`. (2认同)
  • @ Ark-kun:我试图指出如果允许这种事情*将会发生的问题。如果有一种方法可以声明不能“作为其自身类型”装箱的结构类型(仍可以通过将其存储到一个单元素数组中来“装箱”),那么.NET就可以支持对于每个结构类型,运行时还有效地定义了具有相同继承关系的堆对象类型,这阻碍了当前的继承方案。任何不能与堆对象一起使用的关系,也不能与结构一起使用。 (2认同)
  • @ Ark-kun它不会从自身继承,`FooStruct &lt;Cat&gt;`和`FooStruct &lt;Animal&gt;`*不会*是同一类型。在Java中,您不能这么做,因为就运行时而言,这两个*是*相同的类型。但是在例如C ++中,两者是完全不同的类型。您应该能够使用模板专门化功能来让一种专门化类型(在这种情况下为`FooStruct &lt;Cat&gt;`)与另一种专门化类型(在这种情况下为`FooStruct &lt;Animal&gt;`)。尽管不确定.NET,但它确实具有模板专业化AFAIK,但我认为它不像C ++那样具有通用性。 (2认同)
  • @j_random_hacker我不同意.对于具有复杂的按值类型和继承的语言,C++的行为非常明智.我同意,如果你来自一种语言,其中对象总是具有像Java或.NET那样的引用行为,那么这可能会令人惊讶,但是学习这些事情只是学习在C++中有效编码的一部分.我不知道`A a = b`是如何产生UB的,而B b2 = b`也不会发生. (2认同)
  • @j_random_hacker关于为什么C++必须阻止良性情况的一个可能更有说服力的论点,考虑A的赋值运算符的签名 - 它的`A&operator =(const A&)`.因此,避免该运算符使用参数作为B的实例的唯一方法是不自动将B的实例转换为"const A&".但这**会干扰LSP ...... (2认同)
  • 如果 A 的一个或多个数据成员被 B 重新利用,则 UB 可以很容易地从“A a = b”得出。(您可以说这是糟糕的设计/实现,但它不会违反 LSP,前提是公共方法继续“正常运行”,并且通常很有用。)作为一个具体示例,假设 A 包含两个 int 成员 x 和y,一个 int\* p,在其构造函数中设置 p = &amp;x,并要求表达式 \*p 始终有效。(也许 A 中的其他代码有时会执行 p = &amp;y。)B 添加第三个 int 成员 z,并在其构造函数中设置 p = &amp;z。在`A a = b` 和b 的生命周期结束后, ap 不再指向任何东西。 (2认同)
  • @metamorphosis如果a是一个引用,则“ a = b”不会使“ a”引用对象成为“ b”(如果“ a”是一个指针,就像“ a =&b”一样)!它调用由“ a”引用的对象的赋值运算符,该操作符(通常)将继续使用对象“ b”的内容覆盖引用的对象。 (2认同)

Bla*_*ack 152

如果您有基类A和派生类B,则可以执行以下操作.

void wantAnA(A myA)
{
   // work with myA
}

B derived;
// work with the object "derived"
wantAnA(derived);
Run Code Online (Sandbox Code Playgroud)

现在该方法wantAnA需要一份副本derived.但是,该对象derived无法完全复制,因为该类B可能会发明不在其基类中的其他成员变量A.

因此,要调用wantAnA,编译器将"切掉"派生类的所有其他成员.结果可能是您不想创建的对象,因为

  • 它可能不完整,
  • 它的行为就像一个对象A(该类的所有特殊行为B都会丢失).

  • @fgp:这很令人惊讶,因为你**不会将A**传递给该函数. (78认同)
  • C++是__not__ Java!如果`wantAnA`(顾名思义!)想要一个'A`,那就是它得到的东西.而'`'的例子,呃,就像一个'A`.这有多令人惊讶? (40认同)
  • 问题主要在于编译器从`derived`到类型`A`执行的自动转换.隐式转换始终是C++中意外行为的来源,因为通过在本地查看转换发生的代码通常很难理解. (14认同)
  • @fgp:行为类似.但是,对于普通的C++程序员来说,它可能不太明显.据我所知,这个问题没有人"抱怨".它只是关于编译器如何处理这种情况.Imho,最好通过传递(const)引用来避免切片. (10认同)
  • @ThomasW不,我不会抛弃继承,但使用引用.如果wantAnA的签名是**void wantAnA(const A&myA)**,那么就没有切片.而是传递对调用者对象的只读引用. (7认同)
  • 所以你要抛弃继承,支持'移动'语义?坦率地说,我对这被视为一般的"设计偏好"感到惊讶. (3认同)
  • @ user1902689对于要在派生类中重载的函数,必须使其为"虚拟",例如,virtual void print()const {....} (3认同)
  • @Black:但是想要安娜说它*想要一个A,所以这就是它所得到的.它与声明一个函数取一个int,传递0.1,然后抱怨函数收到0 ... (2认同)
  • @Black:恕我直言,这是危险的建议。如果稍后复制该值,则通过引用传递会阻止移动语义发挥作用,因为即使该值最初是右值,它也将是函数内的左值。 (2认同)

geh*_*geh 38

这些都是很好的答案.我想在按值和引用传递对象时添加一个执行示例:

#include <iostream>

using namespace std;

// Base class
class A {
public:
    A() {}
    A(const A& a) {
        cout << "'A' copy constructor" << endl;
    }
    virtual void run() const { cout << "I am an 'A'" << endl; }
};

// Derived class
class B: public A {
public:
    B():A() {}
    B(const B& a):A(a) {
        cout << "'B' copy constructor" << endl;
    }
    virtual void run() const { cout << "I am a 'B'" << endl; }
};

void g(const A & a) {
    a.run();
}

void h(const A a) {
    a.run();
}

int main() {
    cout << "Call by reference" << endl;
    g(B());
    cout << endl << "Call by copy" << endl;
    h(B());
}
Run Code Online (Sandbox Code Playgroud)

输出是:

Call by reference
I am a 'B'

Call by copy
'A' copy constructor
I am an 'A'
Run Code Online (Sandbox Code Playgroud)


The*_*aul 31

谷歌"C++切片"的第三场比赛给了我这篇维基百科文章http://en.wikipedia.org/wiki/Object_slicing和这个(加热,但前几个帖子定义了问题):http://bytes.com/论坛/ thread163565.html

所以当你将一个子类的对象分配给超类时.超类对子类中的附加信息一无所知,也没有足够的空间来存储它,因此附加信息会被"切掉".

如果这些链接没有为"正确答案"提供足够的信息,请编辑您的问题,让我们知道您还在寻找什么.


Wal*_*ght 28

切片问题很严重,因为它可能导致内存损坏,并且很难保证程序不会受到影响.要使用该语言进行设计,支持继承的类应该只能通过引用访问(而不是通过值).D编程语言具有此属性.

考虑A类,从B派生B类.如果A部分有一个指针p,则会发生内存损坏,而B实例将p指向B的附加数据.然后,当附加数据被切掉时,p指向垃圾.

  • 这个问题不仅限于切片.任何包含指针的类都将具有默认赋值运算符和复制构造函数的可疑行为. (18认同)
  • @Weeble:使对象切片比一般指针修正更糟的是确保你已经阻止切片发生,基类必须为每个派生类*提供转换构造函数*.(为什么?任何遗漏的派生类都容易被基类的副本ctor拾取,因为`Derived`可以隐式转换为`Base`.)这显然与开闭原理相反,并且维护很大负担. (7认同)
  • 我忘了复制ctor会重置vptr,我的错误.但是如果A有一个指针,你仍然可以得到腐败,并且B设置它指向被切掉的B部分. (4认同)
  • 请解释内存损坏是如何发生的. (3认同)
  • 给定: class A { virtual void foo() { } }; B 类:A { int *p; 无效 foo() { *p = 3; } }; 现在,当 B 被分配给 A 时对其进行切片,调用 foo(),它又调用 B::foo(),瞧!通过 B::p 的垃圾值分配内存损坏。 (2认同)
  • @Weeble - 这就是在这些情况下覆盖默认析构函数,赋值运算符和复制构造函数的原因. (2认同)
  • 请**显示**内存损坏是如何发生的. (2认同)

小智 9

在C++中,派生类对象可以分配给基类对象,但另一种方法是不可能的.

class Base { int x, y; };

class Derived : public Base { int z, w; };

int main() 
{
    Derived d;
    Base b = d; // Object Slicing,  z and w of d are sliced off
}
Run Code Online (Sandbox Code Playgroud)

当派生类对象被分配给基类对象时,会发生对象切片,派生类对象的其他属性将被切除以形成基类对象.


idi*_*dak 8

C++中的切片问题源于其对象的值语义,这主要是由于与C结构的兼容性.您需要使用显式引用或指针语法来实现在执行对象的大多数其他语言中找到的"正常"对象行为,即,对象始终通过引用传递.

简短的答案是通过按值将派生对象分配给基础对象切片对象,即剩余对象只是派生对象的一部分.为了保留价值语义,切片是一种合理的行为,并且具有相对罕见的用途,这在大多数其他语言中是不存在的.有些人认为它是C++的一个特性,而许多人认为它是C++的怪癖/错误特征之一.

  • "_"正常的"对象行为_",这不是"正常的对象行为",即**引用语义**.并且它与任何随机的OOP牧师告诉你的绝不相关**与C`struct`,兼容性或其他无意义. (5认同)
  • @curiousguy阿门,兄弟.令人遗憾的是,当价值语义是使C++如此强大的东西之一时,C++从不是Java变得多么糟糕. (4认同)

Ste*_*ner 7

那么......为什么丢失衍生信息不好?...因为派生类的作者可能已经更改了表示,因此切掉额外信息会更改对象所表示的值.如果派生类用于缓存对某些操作更有效但对转换回基本表示而言代价高的表示,则会发生这种情况.

还想到有人还应该提到你应该做些什么以避免切片......获取C++编码标准,101规则指南和最佳实践的副本.处理切片是#54.

它提出了一个有点复杂的模式来完全处理这个问题:拥有一个受保护的复制构造函数,一个受保护的纯虚拟DoClone,以及一个带有断言的公共克隆,它将告诉您(某个)派生类是否未能正确实现DoClone.(克隆方法对多态对象进行了适当的深层复制.)

您还可以在基本显式标记复制构造函数,如果需要,可以显式切片.

  • "_你也可以在基础显式标记_上标记复制构造函数",它根本不会帮助**. (2认同)

hab*_*dar 7

1.切片问题的定义

如果D是基类B的派生类,则可以将Derived类型的对象分配给Base类型的变量(或参数).

class Pet
{
 public:
    string name;
};
class Dog : public Pet
{
public:
    string breed;
};

int main()
{   
    Dog dog;
    Pet pet;

    dog.name = "Tommy";
    dog.breed = "Kangal Dog";
    pet = dog;
    cout << pet.breed; //ERROR
Run Code Online (Sandbox Code Playgroud)

尽管允许上述赋值,但分配给变量pet的值会丢失其品种字段.这称为切片问题.

2.如何修复切片问题

为了解决这个问题,我们使用指向动态变量的指针.

Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;         
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed; 
Run Code Online (Sandbox Code Playgroud)

在这种情况下,ptrD(后代类对象)指向的动态变量的数据成员或成员函数都不会丢失.另外,如果需要使用函数,该函数必须是虚函数.

  • -1这完全无法解释实际问题.C++具有值语义,__ not__引用语义如Java,所以这完全是预料之中的.而"修复"确实是一个真正*可怕的*C++代码的例子.通过动态分配"修复"这种类型的切片等不存在的问题是错误代码,泄露内存和可怕性能的一个秘诀.请注意,*是*切片不好的情况,但这个答案很难指出它们.提示:如果通过*references*分配,则会出现问题. (24认同)
  • 我理解"切片"部分,但我不明白"问题".如果不是"宠物"类("品种"数据成员)的一部分"狗"状态的某些状态未被复制到变量"pet"中,该怎么回事?代码只对'Pet`数据成员感兴趣 - 显然.切片绝对是一个"问题",如果它是不需要的,但我在这里看不到. (7认同)
  • "`((Dog*)ptrP)`"我建议使用`static_cast <Dog*>(ptrP)` (4认同)

Sor*_*ush 5

当数据成员被切片时发生对象切片时,我看到所有提到的答案。在这里,我举了一个方法不被覆盖的例子:

class A{
public:
    virtual void Say(){
        std::cout<<"I am A"<<std::endl;
    }
};

class B: public A{
public:
    void Say() override{
        std::cout<<"I am B"<<std::endl;
    }
};

int main(){
   B b;
   A a1;
   A a2=b;

   b.Say(); // I am B
   a1.Say(); // I am A
   a2.Say(); // I am A   why???
}
Run Code Online (Sandbox Code Playgroud)

B(对象b)源自A(对象a1和a2)。b 和 a1,如我们所料,调用它们的成员函数。但是从多态性的角度来看,我们不希望由 b 分配的 a2 不会被覆盖。基本上,a2 只保存 b 的 A 类部分,即 C++ 中的对象切片。

要解决这个问题,应该使用引用或指针

 A& a2=b;
 a2.Say(); // I am B
Run Code Online (Sandbox Code Playgroud)

或者

A* a2 = &b;
a2->Say(); // I am B
Run Code Online (Sandbox Code Playgroud)