关于C++:什么是对象切片?

What is object slicing?

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


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

例如,

1
2
3
4
5
6
7
class A {
   int foo;
};

class B : public A {
   int bar;
};

因此,B类型的对象有两个数据成员:foobar

如果你要写这封信:

1
2
3
B b;

A a = b;

然后,B中关于成员bar的信息在a中丢失。


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

在这种情况下,C++允许您将EDCOX1 OR 1的实例传递给EDCOX1·0的赋值运算符(以及复制构造函数)。这是因为可以将B的一个实例转换为const A&,这就是赋值运算符和复制构造函数希望它们的参数是什么。

良性案例

1
2
B b;
A a = b;

没有什么不好的事情发生——你找了一个A的例子,它是B的副本,这正是你得到的。当然,A不会包含B的一些成员,但是应该怎么做呢?毕竟,它是一个A,而不是一个B,所以它甚至没有听说过这些成员,更不用说能够存储它们了。

险情

1
2
3
4
5
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!

你可能会认为b2之后会是b1的副本。但是,唉,这不是!如果你检查它,你会发现b2是一种法兰克人的生物,由b1的一些块(BA继承的块)和b2的一些块(只有B包含的块)制成。哎哟!

发生了什么事?默认情况下,C++不把赋值操作符当作EDCOX1(22)来对待。因此,行a_ref = b1将调用A的赋值运算符,而不是B的赋值运算符。这是因为对于非虚拟函数,声明的类型(即A&决定调用哪个函数,而不是实际类型(即B,因为a_ref引用B的实例)。现在,A的赋值操作符显然只知道A中声明的成员,所以它只复制这些成员,保留B中添加的成员不变。

解决方案

只分配对象的一部分通常没有什么意义,但是C++不幸地没有提供内置的方式来禁止这个。不过,你可以自己滚。第一步是使分配运算符成为虚拟的。这将确保它始终是调用的实际类型的赋值运算符,而不是声明的类型。第二步是使用dynamic_cast验证分配的对象是否具有兼容的类型。第三步是在(受保护的!)成员assign(),因为Bassign()可能希望使用Aassign()复制A的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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的一个实例。


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

1
2
3
4
5
6
7
8
void wantAnA(A myA)
{
   // work with myA
}

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

现在,方法wantAnA需要一份derived的副本。但是,对象derived不能完全复制,因为类B可以发明不在其基类a中的附加成员变量。

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

  • 可能不完整,
  • 它的行为类似于a对象(类B的所有特殊行为都将丢失)。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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());
}

输出是:

1
2
3
4
5
6
Call by reference
I am a 'B'

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


谷歌在"C++切片"中的第三次匹配给了我这篇维基百科文章HTTP://E.WiKiTo.Org/WiKi/Objista切片,这个(被加热了,但前几篇文章定义了问题):HTTP://ByTest.COM/FUMUM/THEADR163565.HTML

所以当你将一个子类的对象赋给这个超类时。超类对子类中的附加信息一无所知,并且没有存储空间,因此附加信息被"切掉"。

如果这些链接没有提供足够的信息来回答"好答案",请编辑您的问题,让我们知道您还需要什么。


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

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


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

1
2
3
4
5
6
7
8
9
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
}

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


1。切片问题的定义

如果d是基类b的派生类,则可以将派生类型的对象赋给基类型的变量(或参数)。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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。如何解决切片问题

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

例子

1
2
3
4
5
6
7
Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;        
ptrD->name ="Tommy";
ptrD->breed ="Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed;

在这种情况下,动态变量的数据成员或成员函数都不是被ptrd(后代类对象)指向的将丢失。此外,如果需要使用函数,则该函数必须是虚拟函数。


所以…为什么丢失派生信息很糟糕?…因为派生类的作者可能已经更改了表示方式,从而将额外的信息切片会更改对象所表示的值。如果派生类用于缓存对某些操作更有效但转换回基表示形式代价高昂的表示形式,则可能发生这种情况。

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

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

您还可以在基显式上标记复制构造函数,如果需要,它允许显式切片。


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

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


在我看来,切片并不是什么问题,除非你自己的类和程序的架构/设计不好。

如果我将一个子类对象作为一个参数传递给一个方法,这个方法接受一个超类类型的参数,我当然应该知道这一点并知道它的内部,被调用的方法将只处理超类(也就是baseclass)对象。

在我看来,在请求基类的情况下提供子类会导致子类特定的结果,从而导致切片成为一个问题,这只是不合理的期望。它要么是方法使用中的设计不好,要么是子类实现不好。我猜这通常是牺牲良好的OOP设计而有利于方便或提高性能的结果。


好吧,我会在阅读了很多文章解释对象切片之后尝试一下,但不会解释它是如何变得有问题的。

可能导致内存损坏的恶性情况如下:

  • 类对多态基类提供(意外地,可能是由编译器生成的)赋值。
  • 客户端复制并切片派生类的实例。
  • 客户机调用一个虚拟成员函数,该函数访问被切掉的状态。

在这里找到类似的答案:http://sickprogrammerarea.blogspot.in/2014/03/technical-interview-questions-on-c_6.html

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

说明:考虑以下类声明:

1
2
3
4
5
6
7
8
9
10
11
           class baseclass
          {
                 ...
                 baseclass & operator =(const baseclass&);
                 baseclass(const baseclass&);
          }
          void function( )
          {
                baseclass obj1=m;
                obj1=m;
          }

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}


将派生类对象分配给基类对象时,派生类对象的其他属性将从基类对象中剥离(放弃)。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}


将派生类对象分配给基类对象时,派生类对象的所有成员都将复制到基类对象,但不在基类中的成员除外。这些成员被编译器分割开。这叫做对象切片。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#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<<"
"
;
    cout<<d.b<<"
"
;
    cout<<d.c<<"
"
;
    cout<<d.d<<"
"
;
    cout<<d.e<<"
"
;


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

它将产生:

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


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

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

比如说,我们有一些东西可以分派行动。例如,Control Center UI。此UI需要获取当前可以调度的内容的列表。所以我们定义了一个包含调度信息的类。我们称之为Action。所以Action有一些成员变量。为了简单起见,我们只有2个,分别是一个std::string name和一个std::function f。然后它有一个void activate()来执行f成员。

所以用户界面得到一个std::vector。想象一下一些功能,比如:

1
void push_back(Action toAdd);

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

所以这个家伙从Action衍生来添加他自己的味道。他将自己的自制类的一个实例传递给了push_back,但随后程序就失控了。

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

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

我希望这个例子能让那些在讨论AB的时候不能真正想象的人有所启发。