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

619

Кто-то упомянул об этом в IRC, но у google нет хорошего ответа.

Теги:
inheritance
object-slicing
c++-faq

17 ответов

500

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

Например,

class A {
   int foo;
};

class B : public A {
   int bar;
};

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

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

B b;

A a = b;

Затем информация в B о члене bar теряется в a.

  • 65
    Очень информативно, но см. Stackoverflow.com/questions/274626#274636 для примера того, как нарезка происходит во время вызовов метода (что подчеркивает опасность немного лучше, чем простой пример назначения).
  • 49
    Интересно. Я программирую на C ++ в течение 15 лет, и эта проблема мне никогда не приходила в голову, поскольку я всегда передавал объекты по ссылке в целях эффективности и личного стиля. Идет, чтобы показать, как хорошие привычки могут помочь вам.
Показать ещё 12 комментариев
421

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

В этой ситуации С++ позволяет передать экземпляр оператора присваивания B в A (а также в конструктор копирования). Это работает, потому что экземпляр B может быть преобразован в 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). Ой!

Что случилось? Ну, С++ по умолчанию не обрабатывает операторы присваивания как virtual. Таким образом, строка a_ref = b1 вызовет оператор присваивания A, а не символ B. Это связано с тем, что для не виртуальных функций тип объявленный (который A&) определяет, какая функция вызывается, а не тип фактический (который будет B, так как a_ref ссылается на экземпляр B). Теперь оператор назначения A явно знает только о членах, объявленных в A, поэтому он будет копировать только те, оставляя члены, добавленные в B неизменными.

Решение

Назначение только частям объекта обычно имеет мало смысла, но, к сожалению, С++ не предусматривает встроенного способа запретить это. Вы можете, однако, бросить свои собственные. Первый шаг - сделать оператор присваивания виртуальным. Это гарантирует, что он всегда является оператором присваивания типа фактический, который вызывается, а не тип объявленный. Второй шаг - использовать 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 assign() copy members of A from b to this
    // copy members of B from b to this
  }
};

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

  • 1
    Тогда некоторые операции над объектом A не допускаются, когда объект имеет тип B
  • 9
    ИМХО, проблема заключается в том, что существует два различных вида заменяемости, которые могут подразумеваться наследованием: либо любое derived значение может быть дано коду, ожидающему base значение, либо любая производная ссылка может использоваться в качестве базовой ссылки. Я хотел бы видеть язык с системой типов, которая рассматривает обе концепции отдельно. Во многих случаях производная ссылка должна заменять базовую ссылку, но производные экземпляры не должны заменять базовые; Есть также много случаев, когда экземпляры должны быть конвертируемыми, но ссылки не должны заменять.
Показать ещё 25 комментариев
139

Если у вас есть базовый класс 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 теряются).
  • 35
    C ++ - это не Java! Если wantAnA (как следует из названия!) Хочет получить A , то это то, что он получает. И экземпляр A будет вести себя как A Как это удивительно?
  • 66
    @fgp: Это удивительно, потому что вы не передаете A в функцию.
Показать ещё 8 комментариев
31

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

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

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

27

Все это хорошие ответы. Я просто хотел бы добавить пример выполнения при передаче объектов по значению 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'
  • 1
    Это самое простое решение, позволяющее избежать нарезки объектов, +1.
  • 0
    Отличный пример +1.
Показать ещё 1 комментарий
24

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

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

  • 3
    Пожалуйста, объясните, как может происходить повреждение памяти.
  • 0
    я ему не доверяю
Показать ещё 10 комментариев
8

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, теряет поле своей породы. Это называется проблемой slicing.

2. КАК УСТАНОВИТЬ ПРОБЛЕМУ СКОРОСТИ

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

Пример

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

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

  • 7
    Я понимаю часть «нарезки», но не понимаю «проблемы». Какова проблема, что некоторое состояние dog которая не является частью класса Pet (член данных breed ), не копируется в переменную pet ? Код интересует только данные членов Pet - по-видимому. Нарезка определенно является «проблемой», если она нежелательна, но я не вижу этого здесь.
  • 4
    " ((Dog *)ptrP) " Я предлагаю использовать static_cast<Dog*>(ptrP)
Показать ещё 3 комментария
8

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

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

  • 5
    " " нормальное "поведение объекта ", это не "нормальное поведение объекта", это ссылка на семантику . И это никоим образом не связано со struct C, совместимостью или другими несуразностями, которые вам сказал любой случайный священник ООП.
  • 3
    @curiousguy Аминь, брат. Грустно видеть, как часто C ++ теряется из-за того, что не является Java, когда семантика значений является одной из вещей, которые делают C ++ настолько безумно мощным.
Показать ещё 1 комментарий
6

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

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

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

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

  • 2
    « Вы также можете пометить конструктор копирования на явном основании », что совсем не помогает.
4

В С++ объект производного класса может быть назначен объекту базового класса, но другой способ невозможен.

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
}

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

4

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

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

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

  • 3
    Но помните, Минок, что вы НЕ передаете ссылку на этот объект. Вы передаете НОВУЮ копию этого объекта, но используете базовый класс для копирования в процессе.
  • 0
    защищенное копирование / назначение на базовый класс и эта проблема решена.
Показать ещё 2 комментария
2

Найти похожие ответы: http://sickprogrammersarea.blogspot.in/2014/03/technical-interview-questions-on-c_6.html

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

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

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

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

2

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

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

  • Класс предоставляет (случайно, возможно, сгенерированное компилятором) назначение в полиморфном базовом классе.
  • Клиент копирует и нарезает экземпляр производного класса.
  • Клиент вызывает функцию виртуального члена, которая обращается к состоянию среза.
1
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; 
}
  • 3
    Не могли бы вы дать некоторые дополнительные детали? Чем ваш ответ отличается от уже опубликованных?
  • 1
    Я думаю, что больше объяснений не будет плохим.
0

Когда классу класса Derived присваивается объект Base class Object, все члены объекта производного класса копируются в объект базового класса, за исключением членов, которых нет в базовом классе. Эти члены удаляются компилятором. Это называется разделением объектов.

Вот пример:

#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'
  • 0
    Проголосовал, потому что это не хороший пример. Это также не сработает, если вместо копирования d в b вы будете использовать указатель, в этом случае d и e все еще будут существовать, но в Base нет этих членов. Ваш пример показывает только то, что вы не можете получить доступ к членам, которых нет в классе.
-2

когда объект производного класса присваивается объекту базового класса, дополнительные атрибуты объекта производного класса вырезаются (отбрасываются) из объекта базового класса.

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
}
  • 0
    В вашем коде нет назначения .
-3

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

Возьмем пример из "производственного кода" (или что-то вроде близкого):


Скажем, у нас есть что-то, что рассылает действия. Например, пользовательский интерфейс центра управления. Этот пользовательский интерфейс должен получить список вещей, которые в настоящее время могут быть отправлены. Поэтому мы определяем класс, содержащий информацию о доставке. Позвольте называть его Action. Таким образом, Action имеет некоторые переменные-члены. Для простоты имеем просто 2, являющееся a std::string name и a std::function<void()> f. Затем он имеет void activate(), который просто выполняет элемент f.

Таким образом, пользовательский интерфейс получает std::vector<Action>. Представьте себе некоторые функции, такие как:

void push_back(Action toAdd);

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

Итак, парень получает от Action, чтобы добавить свой собственный вкус.
Он передает экземпляр своего домашнего класса в push_back, но затем программа переходит в haywire.

И что случилось?
Как вы могли догадаться: объект был нарезан.

Дополнительная информация из экземпляра потеряна, а f теперь подвержена действию undefined.


Надеюсь, что этот пример освещает те люди, которые не могут себе представить, когда говорят о A и B в некотором роде.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню