什么是对象切片?

Posted

技术标签:

【中文标题】什么是对象切片?【英文标题】:What is object slicing? 【发布时间】:2008-11-08 11:10:13 【问题描述】:

有人在 IRC 中提到它是切片问题。

【问题讨论】:

【参考方案1】:

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

例如,

class A 
   int foo;
;

class B : public A 
   int bar;
;

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

如果你要这样写:

B b;

A a = b;

那么b中关于成员bar的信息在a中丢失了。

【讨论】:

信息量很大,但请参阅 ***.com/questions/274626#274636 以了解在方法调用期间如何进行切片(这比简单的赋值示例更能强调危险)。 有趣。我已经用 C++ 编程 15 年了,我从来没有想过这个问题,因为我总是通过引用传递对象作为效率和个人风格的问题。去展示好习惯如何帮助你。 @Felix 谢谢,但我不认为回滚(因为不是指针算术)会起作用,A a = b; a 现在是 A 类型的对象,它有 B::foo 的副本.我想现在把它扔回去是错误的。 这不是“切片”,或者至少是它的良性变体。如果您执行B b1; B b2; A& b2_ref = b2; b2 = b1,就会出现真正的问题。您可能认为您已将b1 复制到b2,但您没有!您已将b1部分 复制到b2b1 的一部分,B 继承自A),而b2 的其他部分保持不变。 b2 现在是一个科学怪人,由几位 b1 和一些 b2 组成。啊!投反对票,因为我认为答案非常具有误导性。 @fgp 你的评论应该是B b1; B b2; A& b2_ref = b2; b2_ref = b1 "真正的问题发生在你" ...派生自一个带有非虚拟赋值运算符的类。 A 甚至是用于派生的吗?它没有虚函数。如果你从一个类型派生,你必须处理它的成员函数可以被调用的事实!【参考方案2】:

这里的大多数答案都无法解释切片的实际问题是什么。他们只解释了切片的良性案例,而不是危险的案例。假设与其他答案一样,您正在处理两个类AB,其中B(公开)派生自A

在这种情况下,C++ 允许您将B 的实例传递给A 的赋值运算符(以及复制构造函数)。这是可行的,因为B 的实例可以转换为const A&,这是赋值运算符和复制构造函数所期望的参数。

良性案例

B b;
A a = b;

那里没有什么不好的事情发生 - 你要求一个 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!

您可能会认为b2 之后将是b1 的副本。但是,唉,它不是!如果你检查它,你会发现b2 是一个科学怪人生物,由b1 的一些块(B 继承自A 的块)和b2 的一些块(块只有B 包含)。哎哟!

发生了什么?好吧,默认情况下,C++ 不会将赋值运算符视为virtual。因此,a_ref = b1 行将调用A 的赋值运算符,而不是B 的赋值运算符。这是因为,对于非虚函数,declared(正式:static)类型(即A&)决定调用哪个函数,而不是 actual(正式:dynamic)类型(应该是B,因为a_ref 引用了B 的一个实例)。现在,A 的赋值运算符显然只知道A 中声明的成员,所以它只会复制那些,而B 中添加的成员保持不变。

解决方案

仅分配给对象的一部分通常没有什么意义,但不幸的是,C++ 没有提供禁止这种情况的内置方法。但是,您可以自己滚动。第一步是使赋值运算符虚拟。这将保证调用的总是 actual 类型的赋值运算符,而不是 declared 类型的。第二步是使用dynamic_cast 来验证分配的对象是否具有兼容的类型。第三步是在(受保护的!)成员assign() 中进行实际分配,因为Bassign() 可能想要使用Aassign() 来复制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
  
;

请注意,为了方便起见,Boperator= 协变地覆盖了返回类型,因为它知道它正在返回 B 的一个实例。

【讨论】:

恕我直言,问题在于继承可能隐含两种不同类型的可替代性:可以将任何derived 值赋予期望base 值的代码,或者任何派生引用可以作为基础参考。我希望看到一种具有类型系统的语言,它可以分别处理这两个概念。在很多情况下,派生引用应该可以替代基础引用,但派生实例不应该替代基础引用;在很多情况下,实例应该是可转换的,但引用不应该替代。 我不明白你的“奸诈”案件有什么不好。您说您想要:1)获取对 A 类对象的引用和 2)将对象 b1 转换为 A 类并将其内容复制到 A 类的引用。这里实际错误的是背后的正确逻辑给定的代码。换句话说,你拍摄了一个小图像框架(A),将它放在一个更大的图像(B)上,然后你在那个框架上绘画,后来抱怨你的大图像现在看起来很丑:) 但是如果我们只考虑那个框架区域,它看起来很不错,正如画家想要的那样,对吧? :) 问题在于,换一种说法,C++ 默认假定一种非常强的可替代性——它需要基类的操作才能在子类实例上正确工作。甚至对于编译器自动生成的操作,如赋值。因此,在这方面不搞砸自己的操作是不够的,您还必须显式禁用编译器生成的错误操作。或者当然,远离公共继承,这通常是一个很好的建议;-) 另一种常见的方法是简单地禁用复制和赋值运算符。对于继承层次结构中的类,通常没有理由使用值来代替引用或指针。 什么?我不知道运营商可以被标记为虚拟【参考方案3】:

如果您有一个基类A 和一个派生类B,那么您可以执行以下操作。

void wantAnA(A myA)

   // work with myA


B derived;
// work with the object "derived"
wantAnA(derived);

现在wantAnA 方法需要derived 的副本。但是,对象derived 不能被完全复制,因为类B 可以发明其基类A 中没有的其他成员变量。

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

可能不完整, 它的行为类似于A-object(B 类的所有特殊行为都丢失了)。

【讨论】:

C++ 不是 Java!如果wantAnA(顾名思义!)想要A,那么它就是这样。 A 的一个实例会,呃,表现得像一个A。这有什么令人惊讶的? @fgp:这很令人惊讶,因为您没有将 A 传递给函数。 @fgp:行为类似。然而,对于普通的 C++ 程序员来说,这可能不太明显。据我了解这个问题,没有人“抱怨”。这只是关于编译器如何处理这种情况。恕我直言,最好通过传递(const)引用来避免切片。 @ThomasW 不,我不会抛弃继承,而是使用引用。如果wantAnA 的签名是void wantAnA(const A & myA),那么就没有切片。而是传递对调用者对象的只读引用。 问题主要出在编译器从derivedA 类型的自动转换上。隐式转换始终是 C++ 中意外行为的来源,因为通过查看本地代码通常很难理解发生了转换。【参考方案4】:

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

#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());

输出是:

Call by reference
I am a 'B'

Call by copy
'A' copy constructor
I am an 'A'

【讨论】:

你好。很好的答案,但我有一个问题。如果我做这样的事情 ** dev d; base* b = &d;** 切片也发生了? @Adrian 如果您在派生类中引入了一些新的成员函数或成员变量,则无法直接从基类指针访问这些成员函数或成员变量。但是,您仍然可以从重载的基类虚函数内部访问它们。看到这个:godbolt.org/z/LABx33【参考方案5】:

谷歌中“C++ 切片”的第三场比赛给了我这篇 Wikipedia 文章 http://en.wikipedia.org/wiki/Object_slicing 和这个(激烈,但前几篇文章定义了问题):http://bytes.com/forum/thread163565.html

因此,当您将子类的对象分配给超类时。超类对子类中的附加信息一无所知,并且没有空间存储它,因此附加信息被“切掉”。

如果这些链接没有提供足够的信息来获得“好的答案”,请编辑您的问题,让我们知道您还在寻找什么。

【讨论】:

【参考方案6】:

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

考虑 A 类和从 A 派生的 B 类。如果 A 部分具有指针 p,并且 B 实例将 p 指向 B 的附加数据,则可能发生内存损坏。然后,当额外的数据被切掉时,p 指向垃圾。

【讨论】:

请解释内存损坏是如何发生的。 我忘了copy ctor会重置vptr,我的错。但是,如果 A 有一个指针,并且 B 将其设置为指向 B 的被切掉的部分,您仍然会受到损坏。 这个问题不仅仅局限于切片。任何包含指针的类都将具有默认赋值运算符和复制构造函数的可疑行为。 @Weeble - 这就是为什么您在这些情况下覆盖默认析构函数、赋值运算符和复制构造函数的原因。 @Weeble: 使对象切片比一般指针修复更糟糕的原因是,为了确定您已经防止切片发生,基类必须为每个派生类提供转​​换构造函数 . (为什么?任何遗漏的派生类都容易被基类的复制 ctor 拾取,因为Derived 可以隐式转换为Base。)这显然与开闭原则背道而驰,并且需要大量维护负担。【参考方案7】:

在 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

对象切片发生在派生类对象被分配给基类对象时,派生类对象的附加属性被切分以形成基类对象。

【讨论】:

【参考方案8】:

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

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???

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

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

 A& a2=b;
 a2.Say(); // I am B

A* a2 = &b;
a2->Say(); // I am B

【讨论】:

【参考方案9】:

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

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

它提出了一个稍微复杂的模式来完全处理这个问题:有一个受保护的复制构造函数、一个受保护的纯虚拟 DoClone 和一个带有断言的公共克隆,它会告诉你(进一步的)派生类是否无法实现 DoClone正确。 (Clone 方法对多态对象进行适当的深拷贝。)

您还可以在基显式上标记复制构造函数,以便在需要时进行显式切片。

【讨论】:

"您也可以在基显式上标记复制构造函数",这根本没有帮助。【参考方案10】:

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

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

【讨论】:

""正常"对象行为"不是“正常对象行为”,这是引用语义。而且它绝不与 C struct、兼容性或任何随机 OOP 牧师告诉你的其他废话有关。 @curiousguy 阿门,兄弟。当值语义是使 C++ 如此强大的原因之一时,看到 C++ 经常因为不是 Java 而受到抨击,这令人难过。 这不是功能,也不是怪癖/错误功能。这是正常的堆栈复制行为,因为使用 arg 调用函数或(相同)分配类型为 Base 的堆栈变量必须在内存中准确占用 sizeof(Base) 字节,并且可能对齐,这就是“分配”的原因(on-stack-copy) 不会复制派生类成员,它们的偏移量在 sizeof 之外。为避免“丢失数据”,只需像其他人一样使用指针,因为指针内存的位置和大小是固定的,而堆栈非常易变 绝对是 C++ 的错误特征。应该禁止将派生对象分配给基对象,而将派生对象绑定到引用或基类的指针应该是可以的。【参考方案11】:

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

虽然上面的赋值是允许的,但是赋值给变量pet的值会丢失它的品种字段。这称为切片问题

2。如何解决切片问题

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

示例

Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;         
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed; 

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

【讨论】:

我了解“切片”部分,但不了解“问题”。不属于类Petbreed 数据成员)的dog 的某些状态没有复制到变量pet 中,这有什么问题吗?代码只对Pet 数据成员感兴趣——显然。如果不需要,切片绝对是一个“问题”,但我在这里看不到。 "((Dog *)ptrP)" 我建议使用static_cast&lt;Dog*&gt;(ptrP) 我建议指出,当通过 'ptrP' 删除时,你会使字符串 'breed' 最终在没有虚拟析构函数的情况下泄漏内存(不会调用 'string' 的析构函数)... 为什么你显示的有问题吗?修复主要是适当的类设计。在这种情况下的问题是,在继承时编写构造函数来控制可见性是乏味且容易忘记的。您的代码不会靠近危险区域,因为没有涉及甚至提到多态性(切片将截断您的对象但不会使您的程序崩溃,在这里)。 -1 这完全无法解释实际问题。 C++ 有值语义,不是像 Java 那样的引用语义,所以这完全是意料之中的。而“修复”确实是真正可怕 C++ 代码的示例。通过诉诸动态分配来“修复”诸如此类切片之类的不存在问题是导致代码错误、内存泄漏和糟糕性能的秘诀。请注意,有 种切片不好的情况,但此答案未能指出它们。提示:如果您通过 references. 进行分配,麻烦就开始了 必须给-1,这是编译时错误,不是运行时错误,Pet::breed 不存在。【参考方案12】:

在我看来,除了您自己的类和程序的架构/设计不佳时,切片并不是什么大问题。

如果我将子类对象作为参数传递给方法,该方法接受超类类型的参数,我当然应该意识到这一点并且知道内部,被调用的方法将与超类(又名基类)一起使用仅对象。

在我看来,只有在请求基类的地方提供子类会以某种方式导致子类特定的结果,会导致切片成为问题,这只是一种不合理的期望。它要么是使用方法的糟糕设计,要么是糟糕的子类实现。我猜这通常是为了获得权宜之计或性能提升而牺牲良好的 OOP 设计的结果。

【讨论】:

但请记住,Minok,您并没有传入该对象的引用。您正在传递该对象的新副本,但在此过程中使用基类复制它。 在基类上受保护的复制/赋值,这个问题就解决了。 你是对的。好的做法是使用抽象基类或限制对复制/分配的访问。然而,一旦它在那里就不容易被发现,而且很容易忘记照顾。如果您在没有访问冲突的情况下逃脱,使用切片 *this 调用虚拟方法可能会发生神秘的事情。 我记得我在大学的 C++ 编程课程中,有常设的最佳实践,对于我们创建的每个类,我们都需要编写默认构造函数、复制构造函数和赋值运算符,以及析构函数。通过这种方式,您可以确保在编写课程时复制构造等以您需要的方式发生......而不是稍后出现一些奇怪的行为。【参考方案13】:

好的,我会在阅读了许多解释对象切片的帖子后尝试一下,但不知道它是如何成为问题的。

可能导致内存损坏的恶性场景如下:

类在多态基类上提供(意外地,可能是编译器生成的)赋值。 客户端复制并分割派生类的实例。 客户端调用一个访问分片状态的虚拟成员函数。

【讨论】:

【参考方案14】:

切片意味着当子类的对象通过值或从期望基类对象的函数传递或返回时,子类添加的数据将被丢弃。

解释: 考虑以下类声明:

           class baseclass
          
                 ...
                 baseclass & operator =(const baseclass&);
                 baseclass(const baseclass&);
          
          void function( )
          
                baseclass obj1=m;
                obj1=m;
          

由于基类复制函数对派生类一无所知,因此只复制派生类的基类部分。这通常称为切片。

【讨论】:

【参考方案15】:
class A 
 
    int x; 
;  

class B 
 
    B( ) : x(1), c('a')   
    int x; 
    char c; 
;  

int main( ) 
 
    A a; 
    B b; 
    a = b;     // b.c == 'a' is "sliced" off
    return 0; 

【讨论】:

您介意提供一些额外的细节吗?您的答案与已发布的答案有何不同? 我想多解释一点也不错。【参考方案16】:

当派生类对象分配给基类对象时,派生类对象的附加属性会从基类对象中切掉(丢弃)。

class Base  
int x;
 ;

class Derived : public Base  
 int z; 
 ;

 int main() 

Derived d;
Base b = d; // Object Slicing,  z of d is sliced off

【讨论】:

【参考方案17】:

当派生类对象分配给基类对象时,派生类对象的所有成员都被复制到基类对象中,但基类中不存在的成员除外。这些成员被编译器切掉。 这称为对象切片。

这是一个例子:

#include<bits/stdc++.h>
using namespace std;
class Base

    public:
        int a;
        int b;
        int c;
        Base()
        
            a=10;
            b=20;
            c=30;
        
;
class Derived : public Base

    public:
        int d;
        int e;
        Derived()
        
            d=40;
            e=50;
        
;
int main()

    Derived d;
    cout<<d.a<<"\n";
    cout<<d.b<<"\n";
    cout<<d.c<<"\n";
    cout<<d.d<<"\n";
    cout<<d.e<<"\n";


    Base b = d;
    cout<<b.a<<"\n";
    cout<<b.b<<"\n";
    cout<<b.c<<"\n";
    cout<<b.d<<"\n";
    cout<<b.e<<"\n";
    return 0;

会生成:

[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'

【讨论】:

投反对票,因为这不是一个很好的例子。如果不是将 d 复制到 b,而是使用指针,在这种情况下 d 和 e 仍然存在但 Base 没有这些成员,它也不起作用。您的示例仅表明您无法访问该类没有的成员。 Why should I not #include <bits/stdc++.h>?, Why is “using namespace std;” considered bad practice?【参考方案18】:

我刚遇到切片问题,很快就到了这里。所以让我加两分钱。

让我们举一个“生产代码”(或类似的东西)的例子:


假设我们有一些调度动作的东西。以控制中心 UI 为例。 此 UI 需要获取当前能够分派的事物的列表。所以我们定义了一个包含调度信息的类。我们称之为Action。所以Action 有一些成员变量。为简单起见,我们只有 2 个,分别是 std::string namestd::function&lt;void()&gt; f。然后它有一个void activate(),它只执行f 成员。

所以 UI 获得了一个 std::vector&lt;Action&gt; 提供。想象一些功能,例如:

void push_back(Action toAdd);

现在我们已经确定了从 UI 的角度来看它的外观。到目前为止没有问题。但是其他一些在这个项目上工作的人突然决定在Action 对象中有需要更多信息的特殊操作。因为什么原因。这也可以通过 lambda 捕获来解决。这个例子不是从代码中1-1拿来的。

所以这家伙从Action 派生来添加他自己的味道。 他将自制课程的一个实例传递给push_back,但随后程序出现故障。

那么发生了什么? 正如您可能所猜测的那样:对象已被切片。

来自实例的额外信息已丢失,f 现在容易出现未定义的行为。


我希望这个例子能让那些在谈论As 和Bs 以某种方式派生时无法真正想象事情的人有所启发。

【讨论】:

以上是关于什么是对象切片?的主要内容,如果未能解决你的问题,请参考以下文章

什么是对象切片?

什么是对象切片?

什么是对象切片?

什么是对象切片?

什么是对象切片?

什么是对象切片?