development

객체 슬라이싱이란 무엇입니까?

big-blog 2020. 9. 30. 09:19
반응형

객체 슬라이싱이란 무엇입니까?


누군가 IRC에서 슬라이싱 문제로 언급했습니다.


"슬라이싱"은 파생 클래스의 개체를 기본 클래스의 인스턴스에 할당하여 정보의 일부를 잃는 것입니다. 일부는 "분할"됩니다.

예를 들면

class A {
   int foo;
};

class B : public A {
   int bar;
};

따라서 유형의 개체 B에는 두 개의 데이터 멤버 foobar.

그런 다음 이것을 작성한다면 :

B b;

A a = b;

그러면 b회원 대한 정보가에서 bar손실됩니다 a.


대부분의 답변은 슬라이싱의 실제 문제가 무엇인지 설명하지 못합니다. 그들은 배신적인 경우가 아니라 썰기의 양성 사례만을 설명합니다. 당신이 두 개의 클래스를 처리하고 있다는, 다른 답변처럼 가정 A하고 B, B도출 (공개)에서 A.

이 상황에서, C ++은 인스턴스 통과 할 수 있습니다 BA의 할당 연산자 (그리고 복사 생성자를). 이는의 인스턴스가 할당 연산자와 복사 생성자가 인수가 될 것으로 예상 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. 이는 가상이 아닌 함수의 경우 선언 된 유형 ( A&)이 실제 유형 ( 의 인스턴스를 참조하므로 ) 과 반대로 호출되는 함수를 결정하기 B때문 입니다. 이제 의 할당 연산자는에서 선언 된 멤버에 대해서만 알고 있으므로 멤버 만 복사하고 추가 된 멤버는 변경되지 않습니다.a_refBAAB

해결책

객체의 일부에만 할당하는 것은 일반적으로 의미가 없지만 C ++는이를 금지하는 기본 제공 방법을 제공하지 않습니다. 그러나 직접 굴릴 수 있습니다. 첫 번째 단계는 할당 연산자를 가상으로 만드는 것 입니다. 이것은 항상 호출되는 실제 유형의 할당 연산자이며 선언 된 유형이 아니라는 것을 보장합니다 . 두 번째 단계는 dynamic_cast할당 된 개체에 호환 가능한 유형이 있는지 확인하는 데 사용 합니다. 세 번째 단계는 (보호!) 멤버의 실제 할당을하는 것입니다 assign()때문에, B의는 assign()아마도 사용하는 것이 좋습니다 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


기본 클래스 A와 파생 클래스 B가있는 경우 다음을 수행 할 수 있습니다.

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

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

이제 메서드 wantAnAderived. 그러나 derived클래스 B가 기본 클래스에없는 추가 멤버 변수를 발명 할 수 있으므로 객체 를 완전히 복사 할 수 없습니다 A.

따라서을 호출하기 wantAnA위해 컴파일러는 파생 클래스의 모든 추가 멤버를 "분할"합니다. 결과는 생성하고 싶지 않은 객체 일 수 있습니다.

  • 불완전 할 수 있습니다.
  • A-object 처럼 동작합니다 (클래스의 모든 특수 동작 B이 손실 됨).

이것들은 모두 좋은 대답입니다. 값과 참조로 객체를 전달할 때 실행 예제를 추가하고 싶습니다.

#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'

"C ++ 슬라이싱"에 대한 Google의 세 번째 일치는이 Wikipedia 기사 http://en.wikipedia.org/wiki/Object_slicing 을 제공합니다. (열렸지만 처음 몇 개의 게시물이 문제를 정의합니다) : http://bytes.com/ forum / thread163565.html

따라서 슈퍼 클래스에 하위 클래스의 개체를 할당 할 때입니다. 수퍼 클래스는 서브 클래스의 추가 정보를 전혀 알지 못하며이를 저장할 공간이 없으므로 추가 정보가 "분할"됩니다.

해당 링크가 "좋은 답변"에 대한 충분한 정보를 제공하지 않는 경우 질문을 수정하여 더 많은 정보를 찾고있는 것을 알려주십시오.


슬라이싱 문제는 메모리 손상을 초래할 수 있기 때문에 심각하며 프로그램이이 문제로 고통받지 않는다는 것을 보장하는 것은 매우 어렵습니다. 언어에서 디자인하려면 상속을 지원하는 클래스는 값이 아닌 참조로만 액세스 할 수 있어야합니다. D 프로그래밍 언어에는이 속성이 있습니다.

클래스 A와 A에서 파생 된 클래스 B를 고려하십시오. A 부분에 포인터 p가 있고 p가 B의 추가 데이터를 가리키는 B 인스턴스가 있으면 메모리 손상이 발생할 수 있습니다. 그런 다음 추가 데이터가 분리되면 p는 쓰레기를 가리 킵니다.


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
}

개체 분할은 파생 클래스 개체가 기본 클래스 개체에 할당되고 파생 클래스 개체의 추가 특성이 분리되어 기본 클래스 개체를 형성 할 때 발생합니다.


그래서 ... 파생 된 정보를 잃어 버리는 것이 왜 나쁜 것일까 요? ... 파생 클래스의 작성자가 표현을 변경하여 추가 정보를 잘라 내면 개체가 나타내는 값이 변경 될 수 있기 때문입니다. 파생 클래스가 특정 작업에 더 효율적이지만 기본 표현으로 다시 변환하는 데 비용이 많이 드는 표현을 캐시하는 데 사용되는 경우 발생할 수 있습니다.

또한 누군가가 슬라이싱을 피하기 위해해야 ​​할 일을 언급해야한다고 생각했습니다. C ++ 코딩 표준, 101 가지 규칙 지침 및 모범 사례 사본을 얻으십시오. 슬라이싱을 다루는 것은 # 54입니다.

문제를 완전히 처리하기위한 다소 정교한 패턴을 제안합니다. 보호 된 복사 생성자, 보호 된 순수 가상 DoClone 및 (추가) 파생 클래스가 DoClone을 올바르게 구현하지 못한 경우 알려주는 어설 션이있는 공용 클론이 있습니다. (Clone 메서드는 다형성 개체의 적절한 전체 복사본을 만듭니다.)

원하는 경우 명시 적 슬라이싱을 허용하는 기본 명시 적에 복사 생성자를 표시 할 수도 있습니다.


C ++의 슬라이싱 문제는 대부분 C 구조체와의 호환성으로 인해 남아있는 객체의 값 의미론에서 발생합니다. 객체를 수행하는 대부분의 다른 언어에서 볼 수있는 "정상적인"객체 동작을 달성하려면 명시 적 참조 또는 포인터 구문을 사용해야합니다. 즉, 객체는 항상 참조로 전달됩니다.

짧은 대답은 기본 개체 파생 개체를 별로 할당하여 개체를 분할하는 것입니다 . 즉, 나머지 개체는 파생 개체의 일부일뿐입니다. 가치 의미를 보존하기 위해 슬라이싱은 합리적인 동작이며 대부분의 다른 언어에는없는 비교적 드문 용도를 가지고 있습니다. 어떤 사람들은 C ++의 기능이라고 생각하는 반면, 많은 사람들은 C ++의 단점 / 잘못된 기능 중 하나로 간주했습니다.


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. 슬라이싱 문제를 해결하는 방법

To defeat the problem, we use pointers to dynamic variables.

EXAMPLE

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

In this case, none of the data members or member functions of the dynamic variable being pointed to by ptrD (descendant class object) will be lost. In addition, if you need to use functions, the function must be a virtual function.


It seems to me, that slicing isn't so much a problem other than when your own classes and program are poorly architected/designed.

If I pass a subclass object in as a parameter to a method, which takes a parameter of type superclass, I should certainly be aware of that and know the internally, the called method will be working with the superclass (aka baseclass) object only.

It seems to me only the unreasonable expectation that providing a subclass where a baseclass is requested, would somehow result in subclass specific results, would cause slicing to be a problem. Its either poor design in the use of the method or a poor subclass implementation. I'm guessing its usually the result of sacrificing good OOP design in favor of expediency or performance gains.


OK, I'll give it a try after reading many posts explaining object slicing but not how it becomes problematic.

The vicious scenario that can result in memory corruption is the following:

  • Class provides (accidentally, possibly compiler-generated) assignment on a polymorphic base class.
  • Client copies and slices an instance of a derived class.
  • Client calls a virtual member function that accesses the sliced-off state.

Slicing means that the data added by a subclass are discarded when an object of the subclass is passed or returned by value or from a function expecting a base class object.

Explanation: Consider the following class declaration:

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

As baseclass copy functions don't know anything about the derived only the base part of the derived is copied. This is commonly referred to as slicing.


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; 
}

when a derived class object is assigned to a base class object, additional attributes of a derived class object are sliced off (discard) form the base class object.

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
}

When a Derived class Object is assigned to Base class Object, all the members of derived class object is copied to base class object except the members which are not present in the base class. These members are Sliced away by the compiler. This is called Object Slicing.

Here is an Example:

#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;
}

It will generate:

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

I just ran across the slicing problem and promptly landed here. So let me add my two cents to this.

Let's have an example from "production code" (or something that comes kind of close):


Let's say we have something that dispatches actions. A control center UI for example.
This UI needs to get a list of things that are currently able to be dispatched. So we define a class that contains the dispatch-information. Let's call it Action. So an Action has some member variables. For simplicity we just have 2, being a std::string name and a std::function<void()> f. Then it has an void activate() which just executes the f member.

So the UI gets a std::vector<Action> supplied. Imagine some functions like:

void push_back(Action toAdd);

Now we have established how it looks from the UI's perspective. No problem so far. But some other guy who works on this project suddenly decides that there are specialized actions that need more information in the Action object. For what reason ever. That could also be solved with lambda captures. This example is not taken 1-1 from the code.

So the guy derives from Action to add his own flavour.
He passes an instance of his home-brewed class to the push_back but then the program goes haywire.

So what happened?
As you might have guessed: the object has been sliced.

The extra information from the instance has been lost, and f is now prone to undefined behaviour.


I hope this example brings light about for those people who can't really imagine things when talking about As and Bs being derived in some manner.

참고URL : https://stackoverflow.com/questions/274626/what-is-object-slicing

반응형