Что такое нарезка объекта?
Кто-то упомянул об этом в IRC как о проблеме нарезки.
Ответы
«Нарезка» - это когда вы назначаете объект производного класса экземпляру базового класса, тем самым теряя часть информации - часть ее «нарезается».
Например,
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
оператора присваивания «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
}
};
Обратите внимание, что для чистого удобства 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 указывает на мусор.
В 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. КАК УСТРАНИТЬ ПРОБЛЕМУ РАЗРЕЗАНИЯ
Чтобы решить эту проблему, мы используем указатели на динамические переменные.
ПРИМЕР
Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed;
В этом случае ни один из элементов данных или функций-членов динамической переменной, на которую указывает ptrD (объект класса-потомка), не будет потерян. Кроме того, если вам нужно использовать функции, функция должна быть виртуальной функцией.
Мне кажется, что нарезка - это не такая уж большая проблема, кроме случаев, когда ваши собственные классы и программа плохо спроектированы / спроектированы.
Если я передаю объект подкласса в качестве параметра методу, который принимает параметр типа суперкласс, я обязательно должен знать об этом и знать внутренне, вызываемый метод будет работать только с объектом суперкласса (он же базовый класс).
Мне кажется, только необоснованное ожидание того, что предоставление подкласса, в котором запрашивается базовый класс, каким-то образом приведет к результатам, специфичным для подкласса, вызовет проблему нарезки. Либо плохой дизайн использования метода, либо плохая реализация подкласса. Я предполагаю, что это обычно результат принесения в жертву хорошего дизайна ООП в пользу целесообразности или увеличения производительности.
Хорошо, я попробую после прочтения множества сообщений, в которых объясняется нарезка объектов, но не о том, как это становится проблематичным.
Порочный сценарий, который может привести к повреждению памяти, выглядит следующим образом:
- Класс обеспечивает (случайно, возможно, созданное компилятором) назначение полиморфному базовому классу.
- Клиент копирует и нарезает экземпляр производного класса.
- Клиент вызывает виртуальную функцию-член, которая получает доступ к отключенному состоянию.
Нарезка означает, что данные, добавленные подклассом, отбрасываются, когда объект подкласса передается или возвращается по значению или от функции, ожидающей объект базового класса.
Объяснение: Рассмотрим следующее объявление класса:
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 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
Подробнее см. Мой пост