Что такое нарезка объектов? - PullRequest
667 голосов
/ 08 ноября 2008

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

Ответы [ 17 ]

562 голосов
/ 08 ноября 2008

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

Например,

class A {
   int foo;
};

class B : public A {
   int bar;
};

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

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

B b;

A a = b;

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

429 голосов
/ 22 января 2013

Большинство ответов здесь не в состоянии объяснить, в чем проблема нарезки. Они объясняют только доброкачественные случаи нарезки, а не предательские. Предположим, как и другие ответы, что вы имеете дело с двумя классами A и B, где 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. Это связано с тем, что для не виртуальных функций объявленный тип * (который A&) определяет, какая функция вызывается, в отличие от типа фактического (который будет B). , поскольку a_ref ссылается на экземпляр B). Теперь оператор присваивания A, очевидно, знает только о членах, объявленных в A, поэтому он будет копировать только тех, которые будут добавлены в B, без изменений.

Решение

Назначение только частям объекта обычно не имеет смысла, но, к сожалению, в C ++ нет встроенного способа запретить это. Вы можете, однако, свернуть свое собственное. Первый шаг - сделать оператор присваивания virtual . Это гарантирует, что всегда вызывается оператор присваивания типа фактического , а не тип объявленного . Второй шаг - использовать dynamic_cast, чтобы убедиться, что назначенный объект имеет совместимый тип. Третий шаг - выполнить фактическое назначение (защищенного!) Члена assign(), поскольку B assign(), возможно, захочет использовать A * 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
  }
};

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

145 голосов
/ 08 ноября 2008

Если у вас есть базовый класс 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 потеряно).
35 голосов
/ 22 августа 2014

Это все хорошие ответы. Я просто хотел бы добавить пример выполнения при передаче объектов по значению 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'
31 голосов
/ 08 ноября 2008

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

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

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

28 голосов
/ 08 ноября 2008

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

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

9 голосов
/ 07 марта 2018

В 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 голосов
/ 08 ноября 2008

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

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

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

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

7 голосов
/ 09 ноября 2008

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

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

7 голосов
/ 28 января 2012

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. КАК ИСПРАВИТЬ ПРОБЛЕМУ СЛОЖЕНИЯ

Чтобы победить проблему, мы используем указатели на динамические переменные.

* ** 1 022 тысяча двадцать-один * Пример
Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;         
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed; 

В этом случае ни один из элементов данных или функций-членов динамической переменной на который указывает ptrD (объект класса-потомка), он будет потерян. Кроме того, если вам нужно использовать функции, эта функция должна быть виртуальной.

...