Что такое нарезка объекта?

Nov 08 2008

Кто-то упомянул об этом в IRC как о проблеме нарезки.

Ответы

635 DavidDibben Nov 08 2008 at 18:22

«Нарезка» - это когда вы назначаете объект производного класса экземпляру базового класса, тем самым теряя часть информации - часть ее «нарезается».

Например,

class A {
   int foo;
};

class B : public A {
   int bar;
};

Таким образом, объект типа Bимеет два члена данных fooи bar.

Тогда, если бы вы написали это:

B b;

A a = b;

Тогда информация bо члене barтеряется в a.

530 fgp Jan 22 2013 at 22:00

Большинство ответов здесь не объясняют, в чем заключается настоящая проблема с нарезкой. Они объясняют только безобидные случаи разрезания, но не предательские. Предположим, как и другие ответы, что вы имеете дело с двумя классами Aи B, откуда Bпроисходит (публично) A.

В этой ситуации, C ++ позволяет передавать экземпляр Bдля Aоператора присваивания «s (а также конструктор копирования). Это работает, потому что экземпляр Bможет быть преобразован в a const A&, чего операторы присваивания и конструкторы копирования ожидают от своих аргументов.

Доброкачественный случай

B b;
A a = b;

Здесь ничего плохого не происходит - вы просили экземпляр, Aкоторый является копией B, и это именно то, что вы получаете. Конечно, aне будет содержать некоторых bучастников, но как это сделать? В Aконце концов, это не 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_refссылается на экземпляр B) . Теперь Aоператор присваивания «s , очевидно , знает только члены , объявленные в 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
  }
};

Обратите внимание, что для чистого удобства Bs operator=ковариантно переопределяет возвращаемый тип, поскольку знает, что возвращает экземпляр B.

158 Black Nov 08 2008 at 18:28

Если у вас есть базовый класс 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-объект (все особое поведение класса Bтеряется).
42 geh Aug 23 2014 at 01:33

Все это хорошие ответы. Я просто хотел бы добавить пример выполнения при передаче объектов по значению vs по ссылке:

#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'
30 TheArchetypalPaul Nov 08 2008 at 18:14

Третье совпадение в Google для "C ++ нарезки" дает мне эту статью в Википедии. http://en.wikipedia.org/wiki/Object_slicing и это (горячо, но первые несколько сообщений определяют проблему): http://bytes.com/forum/thread163565.html

Так происходит, когда вы назначаете объект подкласса суперклассу. Суперкласс ничего не знает о дополнительной информации в подклассе, и у него нет места для ее хранения, поэтому дополнительная информация «отсекается».

Если эти ссылки не дают достаточно информации для «хорошего ответа», отредактируйте свой вопрос, чтобы сообщить нам, что еще вы ищете.

29 WalterBright Nov 08 2008 at 18:56

Проблема нарезки серьезна, потому что она может привести к повреждению памяти, и очень трудно гарантировать, что программа не пострадает от этого. Чтобы спроектировать его вне языка, классы, поддерживающие наследование, должны быть доступны только по ссылке (а не по значению). Этим свойством обладает язык программирования D.

Рассмотрим класс A и класс B, производный от A. Повреждение памяти может произойти, если часть A имеет указатель p, а экземпляр B, который указывает p на дополнительные данные B. Затем, когда дополнительные данные будут отрезаны, p указывает на мусор.

11 KartikMaheshwari Mar 07 2018 at 16:35

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

Нарезка объекта происходит, когда объект производного класса назначается объекту базового класса, дополнительные атрибуты объекта производного класса отсекаются, чтобы сформировать объект базового класса.

7 SteveSteiner Nov 09 2008 at 00:38

Итак ... Почему потеря полученной информации - это плохо? ... потому что автор производного класса мог изменить представление таким образом, что отсечение дополнительной информации изменяет значение, представляемое объектом. Это может произойти, если производный класс используется для кэширования представления, которое более эффективно для определенных операций, но дорого для преобразования обратно в базовое представление.

Также подумал, что кто-то также должен упомянуть, что вам следует делать, чтобы избежать нарезки ... Получите копию Стандартов кодирования C ++, руководящих принципов 101 правила и лучших практик. Работа с нарезкой - # 54.

Он предлагает несколько сложный шаблон для полного решения проблемы: иметь конструктор защищенной копии, защищенный чистый виртуальный DoClone и общедоступный клон с утверждением, которое сообщит вам, не удалось ли (дополнительному) производному классу правильно реализовать DoClone. (Метод Clone делает правильную глубокую копию полиморфного объекта.)

Вы также можете пометить конструктор копирования на базе явным образом, что позволяет при желании явное нарезание.

7 ididak Nov 09 2008 at 07:31

Проблема нарезки в C ++ возникает из-за семантики значений его объектов, которая осталась в основном из-за совместимости со структурами C. Вам необходимо использовать явную ссылку или синтаксис указателя для достижения «нормального» поведения объекта, характерного для большинства других языков, в которых используются объекты, т. Е. Объекты всегда передаются по ссылке.

Короткие ответы заключаются в том, что вы нарезаете объект, присваивая производный объект базовому объекту по значению , т.е. оставшийся объект является только частью производного объекта. Чтобы сохранить семантику значений, нарезка является разумным поведением и имеет относительно редкое применение, которого нет в большинстве других языков. Некоторые люди считают это особенностью C ++, в то время как многие считают это одной из причуд / недостатков C ++.

6 haberdar Jan 29 2012 at 01:00

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 (объект класса-потомка), не будет потерян. Кроме того, если вам нужно использовать функции, функция должна быть виртуальной функцией.

4 Minok Jul 25 2009 at 02:45

Мне кажется, что нарезка - это не такая уж большая проблема, кроме случаев, когда ваши собственные классы и программа плохо спроектированы / спроектированы.

Если я передаю объект подкласса в качестве параметра методу, который принимает параметр типа суперкласс, я обязательно должен знать об этом и знать внутренне, вызываемый метод будет работать только с объектом суперкласса (он же базовый класс).

Мне кажется, только необоснованное ожидание того, что предоставление подкласса, в котором запрашивается базовый класс, каким-то образом приведет к результатам, специфичным для подкласса, вызовет проблему нарезки. Либо плохой дизайн использования метода, либо плохая реализация подкласса. Я предполагаю, что это обычно результат принесения в жертву хорошего дизайна ООП в пользу целесообразности или увеличения производительности.

3 Dude Oct 18 2012 at 10:22

Хорошо, я попробую после прочтения множества сообщений, в которых объясняется нарезка объектов, но не о том, как это становится проблематичным.

Порочный сценарий, который может привести к повреждению памяти, выглядит следующим образом:

  • Класс обеспечивает (случайно, возможно, созданное компилятором) назначение полиморфному базовому классу.
  • Клиент копирует и нарезает экземпляр производного класса.
  • Клиент вызывает виртуальную функцию-член, которая получает доступ к отключенному состоянию.
3 Santosh Mar 13 2014 at 01:08

Нарезка означает, что данные, добавленные подклассом, отбрасываются, когда объект подкласса передается или возвращается по значению или от функции, ожидающей объект базового класса.

Объяснение: Рассмотрим следующее объявление класса:

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

Поскольку функции копирования базового класса ничего не знают о производном, копируется только базовая часть производного. Это обычно называется нарезкой.

1 quidkid Nov 29 2012 at 19:32
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 Sorush Sep 12 2020 at 18:27

Я вижу, что во всех ответах упоминается, когда нарезка объекта происходит при нарезке элементов данных. Здесь я привожу пример того, что методы не переопределяются:

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, как и следовало ожидать, вызывают свою функцию-член. Но с точки зрения полиморфизма мы не ожидаем, что a2, присвоенное b, не будет отменено. По сути, a2 сохраняет только часть класса A класса b, а именно нарезку объекта в C ++.

Для решения этой проблемы следует использовать ссылку или указатель.

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

или же

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

Подробнее см. Мой пост