Что такое нарезка объектов?
Кто-то упомянул это в IRC как проблему нарезки.
17 ответов
"Вырезание" - это то, где вы назначаете объект производного класса экземпляру базового класса, тем самым теряя часть информации - часть ее "отбрасывается".
Например,
class A {
int foo;
};
class B : public A {
int bar;
};
Итак, объект типа B
имеет два члена данных, foo
а также bar
,
Тогда, если вы должны были написать это:
B b;
A a = b;
Тогда информация в b
о члене bar
потерян в a
,
Большинство ответов здесь не в состоянии объяснить, в чем проблема нарезки. Они объясняют только доброкачественные случаи нарезки, а не предательские. Предположим, как и другие ответы, что вы имеете дело с двумя классами 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++ нет встроенного способа запретить это. Вы можете, однако, свернуть свое собственное. Первый шаг - сделать оператор присваивания виртуальным. Это гарантирует, что всегда вызывается оператор присваивания фактического типа, а не объявленный тип. Второй шаг заключается в использовании dynamic_cast
чтобы убедиться, что назначенный объект имеет совместимый тип. Третий шаг - выполнить фактическое назначение (защищенного!) Члена. assign()
, поскольку B
"s 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
}
};
Обратите внимание, что для удобства B
"s operator=
ковариантно переопределяет тип возвращаемого значения, поскольку он знает, что возвращает экземпляр B
,
Если у вас есть базовый класс 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
потерян).
Это все хорошие ответы. Я просто хотел бы добавить пример выполнения при передаче объектов по значению 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'
Третье совпадение в Google для "нарезки на C++" дает мне эту статью в Википедии http://en.wikipedia.org/wiki/Object_slicing и вот это (горячий, но первые несколько постов определяют проблему): http://bytes.com/forum/thread163565.html
Так что, когда вы назначаете объект подкласса суперклассу. Суперкласс ничего не знает о дополнительной информации в подклассе и не имеет места для ее хранения, поэтому дополнительная информация "обрезается".
Если эти ссылки не дают достаточно информации для "хорошего ответа", пожалуйста, отредактируйте свой вопрос, чтобы сообщить нам, что еще вы ищете.
Проблема нарезки является серьезной, поскольку она может привести к повреждению памяти, и очень трудно гарантировать, что программа не пострадает от этого. Чтобы создать его вне языка, классы, которые поддерживают наследование, должны быть доступны только по ссылке (не по значению). Язык программирования D обладает этим свойством.
Рассмотрим класс A и класс B, производные от A. Повреждение памяти может произойти, если часть A имеет указатель p и экземпляр B, который указывает p на дополнительные данные B. Затем, когда дополнительные данные удаляются, p указывает на мусор.
Я вижу, что во всех ответах упоминается, когда нарезка объекта происходит при нарезке элементов данных. Здесь я привожу пример того, что методы не переопределяются:
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
Подробнее см. Мой пост
В 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++ возникает из семантики значений его объектов, которая осталась в основном из-за совместимости со структурами C. Вам необходимо использовать явный синтаксис ссылки или указателя для достижения "нормального" поведения объекта, встречающегося в большинстве других языков, которые делают объекты, то есть объекты всегда передаются по ссылке.
Короткие ответы: вы нарезаете объект, назначая производный объект базовому объекту по значению, то есть оставшийся объект является только частью производного объекта. Чтобы сохранить семантику значений, нарезка является разумным поведением и имеет относительно редкое применение, которого нет в большинстве других языков. Некоторые люди считают, что это особенность C++, в то время как многие считают, что это одна из странностей / недостатков C++.
Итак... Почему потеря производной информации плохо? ... потому что автор производного класса, возможно, изменил представление так, что отсечение дополнительной информации изменяет значение, представляемое объектом. Это может произойти, если производный класс используется для кэширования представления, которое более эффективно для определенных операций, но дорогостоящее для преобразования обратно в базовое представление.
Также подумал, что кто-то должен также упомянуть, что вы должны делать, чтобы избежать нарезки... Получите копию стандартов кодирования C++, 101 правил и рекомендаций. Работа с нарезкой #54.
Он предлагает несколько сложный шаблон для полного решения проблемы: иметь конструктор защищенных копий, защищенный чистый виртуальный DoClone и общедоступный клон с assert, который сообщит вам, если (в дальнейшем) производный класс не смог правильно реализовать DoClone. (Метод Clone делает правильную глубокую копию полиморфного объекта.)
Вы также можете пометить конструктор копирования на явном основании, который позволяет явно разрезать, если это необходимо.
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 (объект класса-потомка), не будет потерян. Кроме того, если вам нужно использовать функции, функция должна быть виртуальной функцией.
Мне кажется, что нарезка не является большой проблемой, за исключением случаев, когда ваши собственные классы и программы плохо спроектированы / спроектированы.
Если я передам объект подкласса в качестве параметра методу, который принимает параметр типа суперкласс, я, безусловно, должен знать об этом и знать внутренне, что вызываемый метод будет работать только с объектом суперкласса (он же базовый класс).
Мне кажется, только необоснованное ожидание, что предоставление подкласса, где запрашивается базовый класс, каким-то образом приведет к конкретным результатам для подкласса, вызовет проблемы с нарезкой. Это либо плохой дизайн при использовании метода, либо плохая реализация подкласса. Я предполагаю, что это обычно является результатом того, что жертвует хорошим дизайном ООП в пользу целесообразности или увеличения производительности.
Хорошо, я попробую после прочтения многих постов, объясняющих нарезку объектов, но не то, как это становится проблематичным.
Порочный сценарий, который может привести к повреждению памяти, следующий:
- Класс обеспечивает (случайно, возможно, сгенерированный компилятором) присваивание для полиморфного базового класса.
- Клиент копирует и разрезает экземпляр производного класса.
- Клиент вызывает виртуальную функцию-член, которая обращается к отключенному состоянию.
Найти похожие ответы здесь: http://sickprogrammersarea.blogspot.in/2014/03/technical-interview-questions-on-c_6.html
Разделение означает, что данные, добавленные подклассом, отбрасываются, когда объект подкласса передается или возвращается по значению или из функции, ожидающей объект базового класса.
Объяснение: Рассмотрим следующее объявление класса:
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
Поскольку функции копирования базового класса ничего не знают о производном, копируется только базовая часть производного. Это обычно называют нарезкой.
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;
}
Когда объект производного класса назначается объекту базового класса, дополнительные атрибуты объекта производного класса вырезаются (отбрасываются) из объекта базового класса.
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
}
Когда производный класс Object назначается базовому классу 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'
Я просто наткнулся на проблему нарезки и тут же приземлился здесь. Итак, позвольте мне добавить мои два цента к этому.
Давайте возьмем пример из "производственного кода" (или что-то вроде этого):
Допустим, у нас есть что-то, что отправляет действия. Пользовательский интерфейс центра управления, например.
Этот пользовательский интерфейс должен получить список вещей, которые в настоящее время могут быть отправлены. Таким образом, мы определяем класс, который содержит диспетчерскую информацию. Давайте назовем это Action
, Так что Action
имеет некоторые переменные-члены. Для простоты у нас просто есть 2, будучи std::string name
и std::function<void()> f
, Тогда у него есть void activate()
который просто выполняет f
член.
Таким образом, пользовательский интерфейс получает std::vector<Action>
в комплект поставки. Представьте себе некоторые функции, такие как:
void push_back(Action toAdd);
Теперь мы установили, как это выглядит с точки зрения пользовательского интерфейса. Пока проблем нет. Но какой-то другой парень, который работает над этим проектом, внезапно решает, что есть специальные действия, которым нужно больше информации в Action
объект. По какой причине. Это также можно решить с помощью лямбда-захвата. Этот пример не взят 1-1 из кода.
Так что парень происходит от Action
добавить свой собственный аромат.
Он передает экземпляр своего домашнего заваренного класса в push_back
но тогда программа выходит из строя.
Так что же случилось?
Как вы уже догадались: объект был нарезан.
Дополнительная информация из экземпляра была потеряна, и f
теперь склонен к неопределенному поведению.
Я надеюсь, что этот пример проливает свет на тех людей, которые не могут себе представить, когда говорят о A
с и B
получаются в некотором роде.