Кто-то упомянул это в IRC как проблему нарезки.
Кто-то упомянул это в IRC как проблему нарезки.
Ответы:
«Разделение» - это то, где вы назначаете объект производного класса экземпляру базового класса, тем самым теряя часть информации - часть ее «удаляется».
Например,
class A {
int foo;
};
class B : public A {
int bar;
};
Таким образом, объект типа B
имеет два члена данных, foo
и bar
.
Тогда, если бы вы написали это:
B b;
A a = b;
Тогда информация b
о члене bar
теряется в a
.
A a = b;
a
теперь это объект типа, у A
которого есть копия B::foo
. Думаю, будет заброшено возвращать его обратно.
B b1; B b2; A& b2_ref = b2; b2 = b1
. Вы можете подумать , как вы скопировали b1
на b2
, но у вас нет! Вы скопировали часть из b1
к b2
(часть , b1
которая B
наследуется от A
), и оставили другие части b2
неизменными. b2
теперь это франкенштейновское существо, состоящее из нескольких b1
кусков, за которыми следуют несколько кусков b2
. Тьфу! Даунтинг, потому что я думаю, что ответ очень обманчив.
B b1; B b2; A& b2_ref = b2; b2_ref = b1
" Настоящая проблема возникает, если вы " ... происходят от класса с не виртуальным оператором присваивания. Есть A
даже предназначен для вывода? У него нет виртуальных функций. Если вы наследуете тип, вам приходится иметь дело с тем фактом, что его функции-члены могут быть вызваны!
Большинство ответов здесь не в состоянии объяснить, в чем проблема нарезки. Они объясняют только доброкачественные случаи нарезки, а не предательские. Предположим, как и другие ответы, что вы имеете дело с двумя классами A
и B
откуда B
происходят (публично) A
.
В этой ситуации, C ++ позволяет передавать экземпляр B
для A
оператора присваивания «s (а также конструктор копирования). Это работает, потому что экземпляр B
может быть преобразован в a 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
.
derived
значение может быть дано коду, ожидающему base
значение, либо любая производная ссылка может использоваться в качестве базовой ссылки. Я хотел бы видеть язык с системой типов, которая рассматривает обе концепции отдельно. Во многих случаях производная ссылка должна заменять базовую ссылку, но производные экземпляры не должны заменять базовые; Есть также много случаев, когда экземпляры должны быть конвертируемыми, но ссылки не должны заменять.
Если у вас есть базовый класс 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
потеряно).wantAnA
(как следует из названия!) Хочет A
, то это то, что он получает. И пример A
, будет вести себя как A
. Как это удивительно?
derived
тип A
. Неявное приведение всегда является источником неожиданного поведения в C ++, потому что зачастую трудно понять, просматривая код локально, что произошло приведение.
Это все хорошие ответы. Я просто хотел бы добавить пример выполнения при передаче объектов по значению 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/ форум / thread163565.html
Так что, когда вы назначаете объект подкласса суперклассу. Суперкласс ничего не знает о дополнительной информации в подклассе и не имеет места для ее хранения, поэтому дополнительная информация «обрезается».
Если эти ссылки не дают достаточно информации для «хорошего ответа», пожалуйста, отредактируйте свой вопрос, чтобы сообщить нам, что еще вы ищете.
Проблема нарезки является серьезной, поскольку она может привести к повреждению памяти, и очень трудно гарантировать, что программа не пострадает от этого. Чтобы создать его вне языка, классы, поддерживающие наследование, должны быть доступны только по ссылке (не по значению). Язык программирования D обладает этим свойством.
Рассмотрим класс A и класс B, производные от A. Повреждение памяти может произойти, если часть A имеет указатель p и экземпляр B, который указывает p на дополнительные данные B. Затем, когда дополнительные данные удаляются, p указывает на мусор.
Derived
неявно конвертируются в Base
.) Это явно противоречит принципу Open-Closed и является большой нагрузкой на обслуживание.
В 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 ++.
struct
, совместимостью или другим бессмысленным смыслом, который вам сказал любой случайный ООП-жрец.
Base
должен занимать ровно sizeof(Base)
байты в памяти с возможным выравниванием, может быть, поэтому «назначение» (on-stack-copy) ) не будет копировать производные члены класса, их смещения находятся за пределами sizeof. Для того, чтобы избежать «потери данных», просто указатель использования, как кто - либо другой, так как указатель памяти фиксируется на месте и размера, в то время как стек очень volitile
Итак ... Почему потеря производной информации плохо? ... потому что автор производного класса, возможно, изменил представление таким образом, что удаление дополнительной информации изменяет значение, представляемое объектом. Это может произойти, если производный класс используется для кэширования представления, которое более эффективно для определенных операций, но дорогостоящее для преобразования обратно в базовое представление.
Также подумал, что кто-то должен также упомянуть, что вы должны делать, чтобы избежать нарезки ... Получите копию стандартов кодирования 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 (объект класса-потомка), не будет потерян. Кроме того, если вам нужно использовать функции, функция должна быть виртуальной функцией.
dog
, которое не является частью класса Pet
( breed
член данных), не копируется в переменную pet
? Код интересует только Pet
данные членов - по-видимому. Нарезка - определенно «проблема», если она нежелательна, но я не вижу этого здесь.
((Dog *)ptrP)
Я предлагаю использоватьstatic_cast<Dog*>(ptrP)
Dog::breed
), никоим образом не является ОШИБКОЙ, связанной с SLICING?
Мне кажется, что нарезка не является большой проблемой, за исключением случаев, когда ваши собственные классы и программы плохо спроектированы / спроектированы.
Если я передам объект подкласса в качестве параметра методу, который принимает параметр типа суперкласс, я, безусловно, должен знать об этом и знать внутренне, что вызываемый метод будет работать только с объектом суперкласса (он же базовый класс).
Мне кажется только необоснованным ожиданием, что предоставление подкласса, в котором запрашивается базовый класс, каким-то образом приведет к конкретным результатам для подкласса, вызовет проблемы с нарезкой. Это либо плохой дизайн при использовании метода, либо плохая реализация подкласса. Я предполагаю, что это обычно является результатом того, что жертвует хорошим дизайном ООП в пользу целесообразности или увеличения производительности.
Хорошо, я попробую после прочтения многих постов, объясняющих нарезку объектов, но не то, как это становится проблематичным.
Порочный сценарий, который может привести к повреждению памяти, следующий:
Разделение означает, что данные, добавленные подклассом, отбрасываются, когда объект подкласса передается или возвращается по значению или из функции, ожидающей объект базового класса.
Объяснение: Рассмотрим следующее объявление класса:
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
. Таким образом, an 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
но затем программа выходит из строя.
Так что случилось?
Как вы , возможно , догадались: объект был кружочками.
Дополнительная информация из экземпляра была потеряна и f
теперь подвержена неопределенному поведению.
Я надеюсь, что этот пример проливает свет на тех людей, которые не могут по-настоящему представить, когда говорят о том, что A
s и B
s получены каким-либо образом.