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





Третье совпадение в Google для "нарезки C++" дает мне эту статью в Википедии http://en.wikipedia.org/wiki/Object_slicing и эту (горячую, но первые несколько сообщений определяют проблему): http://bytes.com/forum/thread163565.html
Так происходит, когда вы назначаете объект подкласса суперклассу. Суперкласс ничего не знает о дополнительной информации в подклассе, и у него нет места для ее хранения, поэтому дополнительная информация «отсекается».
Если эти ссылки не дают достаточно информации для «хорошего ответа», отредактируйте свой вопрос, чтобы сообщить нам, что еще вы ищете.
«Нарезка» - это когда вы назначаете объект производного класса экземпляру базового класса, тем самым теряя часть информации - часть ее «нарезается».
Например,
class A {
int foo;
};
class B : public A {
int bar;
};
Итак, объект типа B имеет два члена данных, foo и bar.
Тогда, если вы напишете это:
B b;
A a = b;
Тогда информация в b о члене bar теряется в a.
Интересно. Я программировал на C++ 15 лет, и эта проблема никогда не приходила мне в голову, поскольку я всегда передавал объекты по ссылке из соображений эффективности и личного стиля. Показывает, как полезные привычки могут вам помочь.
@ Дэвид: не могли бы вы подробно объяснить свою строчку Then the information in b about member bar is lost in a.. повреждаются ли данные foo после назначения? но почему?
@Hades: данные не повреждаются. Просто невозможно сказать a.bar, потому что компилятор считает, что a относится к типу A. Если вы вернете его к типу B, тогда вы сможете получить все скрытые («отрезанные») поля.
@Felix Спасибо, но я не думаю, что обратное приведение (поскольку не арифметика указателя) будет работать, A a = b;a теперь является объектом типа A, который имеет копию B::foo. Думаю, сейчас будет ошибкой отбрасывать его обратно.
@Hades: Я понимаю, что вы имеете в виду, я думал об указателях. Вы правы, присваивание - это, конечно, не приведение - на самом деле в стеке размещается новый объект. Тогда bar в b вообще не поврежден, просто не копируется оператором присваивания, созданным компилятором, поэтому a теперь является полностью новым объектом типа A с членом a.foo, установленным на то же значение, что и b.foo.
@ Карл: не помогает в случае переуступки. B& b = xxx; b = someDerivedClass(); еще провоцирует нарезку. Просто обычно проблема остается незамеченной.
Другой ответ, который объясняет нарезку, но не проблему.
Это не «нарезка» или, по крайней мере, ее мягкий вариант. Настоящая проблема возникает, если вы делаете B b1; B b2; A& b2_ref = b2; b2 = b1. Вы могли подумать, что скопировали b1 на b2, но это не так! Вы скопировали частьb1 в b2 (часть b1, которую B унаследовал от A), а остальные части b2 оставили без изменений. b2 - теперь франкенштейновское существо, состоящее из нескольких битов b1, за которыми следуют несколько фрагментов b2. Фу! Голосование против, потому что я думаю, что ответ вводит в заблуждение.
@fgp Ваш комментарий должен быть следующим: B b1; B b2; A& b2_ref = b2; b2_ref = b1 "Настоящая проблема возникает, если вы" ... унаследован от класса с невиртуальным оператором присваивания. A вообще предназначен для производства? В нем нет виртуальных функций. Если вы производите от типа, вы должны иметь дело с тем фактом, что его функции-члены могут быть вызваны!
связь с парой примеров того, как избежать проблемы нарезки с помощью указателей или ссылок
@fgp, поэтому вы не помещаете B в имя ссылки на A.
Это неверный ответ. Никакая информация здесь не теряется. a может содержать только одно поле и получает копию этого поля от b. Он никогда не должен иметь дело с любыми другими полями, которые могут быть у b.
Какова сигнатура неявно сгенерированного конструктора копирования, используемого в объявлении A? т.е. в A a = b;?
Это A(const A&).
Если у Вас есть базовый класс 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).C++ - это нет Java! Если wantAnA (как следует из названия!) Хочет A, то он его и получает. А экземпляр A будет вести себя как A. Что в этом удивительного?
@fgp: Это удивительно, потому что вы не сдать пятёрку к функции.
@Black: Но wantAnA сказал, что это хочет an A, так что это то, что он получает. Это то же самое, что объявить функцию, которая принимает int, передать 0,1, а затем пожаловаться, что функция получает 0 ...
@fgp: поведение аналогично. Однако для среднего программиста на C++ это может быть менее очевидным. Насколько я понял вопрос, никто не «жалуется». Это просто о том, как компилятор справляется с ситуацией. Имхо, лучше вообще избегать нарезки, передавая (константные) ссылки.
@Black: Это, ИМХО, опасный совет. Если значение позже копируется, передача по ссылке предотвращает включение семантики перемещения, потому что даже если значение изначально было rvalue, внутри функции будет lvalue.
Значит, вы бы отказались от наследования в пользу семантики «перемещения»? Честно говоря, я был бы удивлен, если бы это считалось общим «дизайнерским предпочтением».
@ThomasW Нет, я бы не выбрасывал наследование, а использовал ссылки. Если сигнатура wantAnA была бы void wantAnA (const A & myA), значит, нарезки не было. Вместо этого передается доступная только для чтения ссылка на объект вызывающего.
проблема в основном связана с автоматическим преобразованием, которое компилятор выполняет от derived к типу A. Неявное приведение типов всегда является источником неожиданного поведения в C++, потому что часто бывает трудно понять, глядя на локальный код, что произошло приведение.
@Black, вы имели в виду «Имхо, лучше вообще избегать нарезки, передавая (константные) указатели», а не «ссылки»? GCC & clang дважды печатает baseClass ... #include <iostream> using namespace std; struct baseClass {void print () const {cout << "baseClass"; }}; структура производный класс: общедоступный базовый класс {void print () const {cout << "производный класс"; }}; void myFunction (const baseClass & theClass) {theClass.print (); } int main () {baseClass base; myFunction (база); производный класс производный; myFunction (производный); }
@ user1902689 Чтобы функция была перегружена в производном классе, вы должны сделать ее «виртуальной», например virtual void print () const {....}
@pqnet это не актерский состав. A создается на месте вызова через вызов A::A(const A &).
@Caleth Я имел в виду автоматическое преобразование const derived& в const A&, которое происходит неявно, когда вы копируете инициализируете экземпляр A. Мой юридический язык не соответствовал стандарту 5 лет назад, поэтому я использовал "приведение" для идентификации всех преобразований, а не только явного выражения приведения.
@pqnet нет B&, нет преобразования, ссылка (аргумент) (конструктора копирования) напрямую связана с подобъектом AB.
@Caleth выражение derived в выражении wantAnA(derived) представляет собой ссылку lvalue, набранную как B&, потому что derived - это идентификатор, соответствующий неконстантной переменной типа B. Язык автоматически преобразует ссылку из B& в const A&, как в const_cast<const A&>(static_cast<A&>(derived)), что затем позволяет ему вызывать конструктор копирования A(A const &) класса A. Если такой конструктор копирования знает о классе B и пытается вывести фактический тип своего аргумента (то есть, пытаясь получить dynamic_cast<const B&> его аргумент), он может получить доступ ко всему объекту B.
@fgp Я не ожидаю этого в ООП.
Проблема нарезки серьезна, потому что она может привести к повреждению памяти, и очень трудно гарантировать, что программа не пострадает от этого. Чтобы разработать его вне языка, классы, поддерживающие наследование, должны быть доступны только по ссылке (а не по значению). Этим свойством обладает язык программирования D.
Рассмотрим класс A и класс B, производный от A. Повреждение памяти может произойти, если часть A имеет указатель p, а экземпляр B, который указывает p на дополнительные данные B. Затем, когда дополнительные данные будут отрезаны, p указывает на мусор.
Объясните, пожалуйста, как может произойти повреждение памяти.
Дано: class A {virtual void foo () {}}; класс B: A {int * p; void foo () {* p = 3; }}; Теперь нарежьте B, когда он назначен A, вызовите foo (), который вызывает B :: foo (), и вуаля! Присвоение повреждения памяти из-за мусора значения для B :: p.
Ага, Уолтер смешивает присвоение указателя (где A * указывает на объект B, поэтому B :: p НЕ теряется) и назначение объекта (после которого вызывается A :: foo).
Я забыл, что копирующий ctor сбросит vptr, моя ошибка. Но вы все равно можете получить повреждение, если у A есть указатель, а B устанавливает его так, чтобы он указывал на раздел B, который отсекается.
Эта проблема не ограничивается только нарезкой. Любые классы, содержащие указатели, будут иметь сомнительное поведение с оператором присваивания по умолчанию и конструктором копирования.
@Weeble - вот почему вы переопределяете деструктор по умолчанию, оператор присваивания и конструктор копирования в этих случаях.
Для полиморфизма требуется виртуальная функция или CRTP (шаблоны, чтобы это произошло во время компиляции) ...
@Weeble: Что делает нарезку объекта хуже, чем общие исправления указателя, так это то, что чтобы быть уверенным, что вы предотвратили нарезку, базовый класс должен предоставлять конструкторы преобразования для каждого производного класса. (Почему? Любые производные классы, которые пропущены, могут быть подхвачены ctor копирования базового класса, поскольку Derived неявно конвертируется в Base.) Это, очевидно, противоречит принципу открытого-закрытого и требует больших затрат на обслуживание.
Это один из самых разочаровывающих ответов на C++ SO. Здесь нет примера, это действительно расплывчато и просто неверно: нарезка обычно не вызывает «повреждения памяти» (не могли бы вы уточнить термин?), Я не вижу случая, когда это повредит стек или что-либо, что может повлиять на выполнение. Однако это может быть ошибка программиста в его собственном приложении, но это не «повреждение». Наконец, большинство подобных назначений будет выполняться с помощью указателя, без нарезки, шаблон из OP никогда не обсуждается в самых популярных ссылках.
Итак ... Почему потеря полученной информации - это плохо? ... потому что автор производного класса мог изменить представление таким образом, что отсечение дополнительной информации изменяет значение, представляемое объектом. Это может произойти, если производный класс используется для кэширования представления, которое более эффективно для определенных операций, но дорого для преобразования обратно в базовое представление.
Также подумал, что кто-то также должен упомянуть, что вы должны делать, чтобы избежать нарезки ... Получите копию стандартов C++ Coding Standards, 101 правил и передовых практик. Работа с нарезкой - # 54.
Он предлагает несколько изощренный шаблон для полного решения проблемы: иметь конструктор защищенной копии, защищенный чистый виртуальный DoClone и общедоступный клон с утверждением, которое сообщит вам, не удалось ли (дополнительному) производному классу правильно реализовать DoClone. (Метод Clone делает правильную глубокую копию полиморфного объекта.)
Вы также можете пометить конструктор копирования на базе явным образом, что позволяет при желании явно нарезать фрагменты.
"Вы также можете пометить конструктор копирования на основе явного", который вообще помогает нет.
Проблема нарезки в C++ возникает из-за семантики значений его объектов, которая осталась в основном из-за совместимости со структурами C. Вам необходимо использовать явную ссылку или синтаксис указателя, чтобы добиться «нормального» поведения объекта, характерного для большинства других языков, в которых используются объекты, т.е. объекты всегда передаются по ссылке.
Короткие ответы заключаются в том, что вы нарезаете объект, присваивая производный объект базовому объекту по стоимости, то есть оставшийся объект является только частью производного объекта. Чтобы сохранить семантику значений, нарезка является разумным поведением и имеет относительно редкое применение, которого нет в большинстве других языков. Некоторые люди считают это особенностью C++, в то время как многие считают это одной из причуд / недостатков C++.
««нормальное» поведение объекта», это не «нормальное поведение объекта», это справочная семантика. И это связывает никоим образом не с C struct, совместимостью или прочей ерундой, которую вам сказал любой случайный священник ООП.
@curiousguy Аминь, брат. Печально видеть, как часто C++ терпит поражение из-за того, что он не является Java, когда семантика значений - одна из вещей, которые делают C++ таким безумно мощным.
Это не особенность, не причуда / недостаток. Это нормальное поведение при копировании в стеке, поскольку вызов функции с аргументом или (такой же) переменной стека распределения типа Base должен занимать в памяти ровно sizeof(Base) байта с возможным выравниванием, возможно, поэтому «присваивание» (в стеке -copy) не будет копировать члены производного класса, их смещения находятся за пределами sizeof. Чтобы избежать «потери данных», просто используйте указатель, как и любой другой, поскольку память указателя фиксирована по месту и размеру, тогда как стек очень изменчив.
Определенно недостаток C++. Назначение производного объекта базовому объекту должно быть запрещено, в то время как привязка производного объекта к ссылке или указателю базового класса должна быть в порядке.
Мне кажется, что нарезка - это не такая уж большая проблема, кроме случаев, когда ваши собственные классы и программа плохо спроектированы / спроектированы.
Если я передаю объект подкласса в качестве параметра методу, который принимает параметр типа суперкласс, я обязательно должен знать об этом и знать внутренне, вызываемый метод будет работать только с объектом суперкласса (он же базовый класс).
Мне кажется, только необоснованное ожидание того, что предоставление подкласса, в котором запрашивается базовый класс, каким-то образом приведет к результатам, специфичным для подкласса, вызовет проблему нарезки. Либо плохой дизайн использования метода, либо плохая реализация подкласса. Я предполагаю, что это обычно результат принесения в жертву хорошего ООП-дизайна в пользу целесообразности или увеличения производительности.
Но помните, Минок, что вы НЕ передаете ссылку на этот объект. Вы передаете НОВУЮ копию этого объекта, но используете базовый класс для его копирования в процессе.
защищенная копия / присвоение базового класса и эта проблема решена.
Ты прав. Хорошая практика - использовать абстрактные базовые классы или ограничивать доступ к копированию / назначению. Однако его не так просто обнаружить, когда он есть, и легко забыть о заботе. Вызов виртуальных методов с помощью split * может привести к загадочным вещам, если вы уйдете без нарушения прав доступа.
Из моих курсов программирования на 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 (объект класса-потомка), будет утерян. Кроме того, если вам нужно использовать функции, функция должна быть виртуальной функцией.
Я понимаю "нарезку", но не понимаю "проблему". Как это проблема, что некоторое состояние dog, не являющееся частью класса Pet (член данных breed), не копируется в переменную pet? Код, по-видимому, интересует только элементы данных Pet. Нарезка - определенно "проблема", если она нежелательна, но здесь я этого не вижу.
"((Dog *)ptrP)" Я предлагаю использовать static_cast<Dog*>(ptrP)
Я предлагаю указать, что вы заставите строку 'порождать' в конечном итоге утечку памяти без виртуального деструктора (деструктор 'string' не будет вызываться) при удалении через 'ptrP' ... Почему то, что вы показываете, проблематично? Исправление в основном связано с правильным дизайном класса. Проблема в этом случае заключается в том, что запись конструкторов для управления видимостью при наследовании утомительна и легко забывается. Вы не попадете в опасную зону с вашим кодом, поскольку здесь нет никакого полиморфизма, или даже упомянутого (здесь нарезка усечет ваш объект, но не приведет к сбою вашей программы).
-1 Это совершенно не объясняет сути проблемы. C++ имеет семантику значений, эталонную семантику нет, как и Java, так что этого вполне следовало ожидать. И «исправление» действительно является примером настоящего кода C++ какой ужас. «Устранение» несуществующих проблем, подобных этому типу нарезки, путем использования динамического распределения - это рецепт ошибочного кода, утечки памяти и ужасной производительности. Обратите внимание, что есть случаи являются, когда нарезка плохая, но этот ответ не может указать на них. Подсказка: беда начнется, если назначить через использованная литература.
Вы хоть понимаете, что попытка доступа к члену неопределенного типа (Dog::breed) не является ОШИБКОЙ, связанной с РАЗРЕЗОМ?
Должен дать -1, это ошибка времени компиляции, а не ошибка времени выполнения, Pet :: порода не существует.
Хорошо, я попробую после прочтения множества сообщений, в которых объясняется нарезка объектов, но не то, как это становится проблематичным.
Порочный сценарий, который может привести к повреждению памяти, выглядит следующим образом:
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;
}
Не могли бы вы дать дополнительные подробности? Чем ваш ответ отличается от уже опубликованных?
Думаю, было бы неплохо дополнительное объяснение.
Большинство ответов здесь не объясняют, в чем заключается настоящая проблема с нарезкой. Они объясняют только безобидные случаи разрезания, но не предательские. Предположим, как и другие ответы, что вы имеете дело с двумя классами 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(), поскольку Bassign(), вероятно, захочет использовать Aassign() для копирования членов 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
}
};
Обратите внимание, что для чистого удобства Boperator= ковариантно переопределяет возвращаемый тип, поскольку знает означает, что он возвращает экземпляр B.
Тогда некоторые операции с объектом A не разрешены, если объект имеет тип B.
IMHO, проблема в том, что существует два разных типа заменяемости, которые могут подразумеваться наследованием: либо любое значение derived может быть присвоено коду, ожидающему значение base, либо любая производная ссылка может использоваться в качестве базовой ссылки. Я хотел бы видеть язык с системой типов, в которой обе концепции рассматриваются по отдельности. Во многих случаях производная ссылка должна быть заменена базовой ссылкой, но производные экземпляры не должны быть заменены на базовые; есть также много случаев, когда экземпляры должны быть конвертируемыми, но ссылки не должны заменять.
По идее, в .NET, если функция возвращает KeyValuePair<SiameseCat, ToyotaPrius>, следует иметь возможность сохранить этот результат в месте хранения типа KeyValuePair<Animal, Vehicle> не потому, что экземпляр первого является экземпляром второго, а, скорее, потому, что возвращаемое значение интерпретируется как KVP<A,V> фактически превратил бы его в единое целое. К сожалению, для этого потребуется отдельная иерархия от обычного наследования, поскольку упакованный экземпляр первого типа определенно не эквивалентен упакованному экземпляру последнего.
Я не понимаю, что же такого плохого в вашем «предательском» деле. Вы заявили, что хотите: 1) получить ссылку на объект класса A и 2) преобразовать объект b1 в класс A и скопировать его содержимое в ссылку класса A. Что на самом деле неверно, так это правильная логика, лежащая в основе данный код. Другими словами, вы взяли небольшую рамку изображения (A), поместили ее поверх более крупного изображения (B) и нарисовали через эту рамку, жалуясь позже, что ваше большое изображение теперь выглядит некрасиво :) выглядит неплохо, как и хотел художник, правда? :)
Иными словами, проблема в том, что C++ по умолчанию предполагает очень сильный тип заменяемость - он требует, чтобы операции базового класса корректно работали с экземплярами подкласса. И это даже для операций, которые компилятор автоматически сгенерировал как присваивание. Так что недостаточно не испортить свои собственные операции в этом отношении, вы также должны явно отключить неправильные, сгенерированные компилятором. Или, конечно, держитесь подальше от публичного наследования, что обычно бывает хорошим советом ;-)
@supercat Я немного не понимаю, какое различие вы проводите. Это 1) прозрачная передача производного в качестве замены базы vs 2) непрозрачная передача ссылки на производное, как если бы на самом деле это была ссылка на базу?
В .NET с каждым типом структуры связан тип объекта кучи, в который он может быть неявно преобразован. Если тип структуры реализует интерфейс, методы этого интерфейса могут быть вызваны в структуре «на месте». Если у кого-то была структура FooStruct<T> с полем типа T, интерфейс реализации IFoo<T>, содержащий свойство Foo типа T, которое могло читать и записывать поле, то, если FooStruct<Cat> унаследован от FooStruct<Animal>, он должен реализовывать IFoo<Animal>, подразумевая, что можно хранить Animal к нему.
Сохранение FooStruct<Cat> в переменной типа FooStruct<Animal> превратит его в FooStruct<Animal>, который может содержать Animal в своем поле, но приведение его к IFoo<Animal> превратит его в тип объекта кучи, связанный с FooStruct<Cat>, который должен был бы реализовать IFoo<Animal>, но не может принять Animal, не относящийся к кошке, хранящийся в его собственности.
Другой распространенный подход - просто отключить оператор копирования и присваивания. Для классов в иерархии наследования обычно нет причин использовать значение вместо ссылки или указателя.
@supercat "если FooStruct <Cat> унаследован от FooStruct <Animal>". Эээ, что? Как структура может наследовать от самой себя в .Net? Вы пишете очень странные вещи, которые кажутся ненастоящими.
@ Арк-кун: Я пытался указать на проблему, которая может возникнуть если бы такое было разрешено. Если бы существовал способ объявления типа структуры, который не мог быть помещен в коробку «как его собственный тип» [он все еще мог быть «упакован», сохраняя его в одноэлементном массиве], это позволило бы .NET поддерживать сценариям наследования, которым в настоящее время препятствует тот факт, что для каждого типа структуры среда выполнения также эффективно определяет тип объекта кучи, который имеет те же отношения наследования. Любые отношения, которые не могут работать с объектами кучи, также не могут работать со структурами.
@ Ark-kun Он не унаследовал бы от себя, FooStruct<Cat> и FooStruct<Animal>не одного и того же типа. Вы не можете этого сделать в Java, потому что два являются, что касается среды выполнения, одного типа. Но, например, C++, это два совершенно разных типа. Вы должны иметь возможность использовать специализации шаблонов, чтобы разрешить один специализированный тип (в данном случае FooStruct<Cat>) от другого (в данном случае FooStruct<Animal>). Однако не уверен в .NET - он делает имеет специализацию шаблонов AFAIK, но, я думаю, не совсем такие общие, как у C++.
Что за? Я понятия не имел, что операторы могут быть помечены как виртуальные
«Вероломное» дело действительно коварное, и я его раньше не рассматривал. Но то, что вы называете «безобидным», вовсе не безобидно - почти в 100% случаев писать этот код является ошибкой, и язык должен был быть спроектирован так, чтобы он приводил к ошибке компиляции. LSP означает, что производный объект должен правильно вести себя при обработке как объект базового класса, но «поведение» означает только «ответ на вызовы общедоступного метода». LSP позволяет произвольно переопределить внутреннее состояние (то, что копирует operator=()) в B, поэтому копирование этого в экземпляр A может даже создать UB.
@j_random_hacker Я не согласен. Поведение C++ очень разумно для языка, который имеет сложные типы по значению и наследование. Я согласен с тем, что это может быть удивительно, если вы пришли из языка, в котором объекты всегда имеют поведение по ссылке, например Java или .NET, но изучение подобных вещей - это всего лишь часть обучения эффективному программированию на C++. Я не понимаю, как A a = b мог вызвать UB, чего не могло случиться и с B b2 = b.
@j_random_hacker В качестве более убедительного аргумента в пользу того, почему C++ должен предотвращать мягкий случай, рассмотрим сигнатуру оператора присваивания A - его A& operator=(const A&). Таким образом, единственный способ избежать того, чтобы этот оператор был с аргументом, являющимся экземпляром B, - это не приводить экземпляры B к const A& автоматически. Но этот бы мешает LSP ...
Если один или несколько элементов данных A перепрофилированы B, UB может легко следовать из A a = b. (Можно сказать, что это плохой дизайн / реализация, но это не нарушает LSP, при условии, что общедоступные методы продолжают «вести себя должным образом» и часто могут быть полезны.) В качестве конкретного примера предположим, что A содержит два члена типа int x и y, int * p, устанавливает p = & x в своем ctor и требует, чтобы выражение * p было действительным всегда. (Возможно, другой код в A иногда делает p = & y.) B добавляет третий член z типа int, а в его ctor устанавливает p = & z. По истечении срока службы A a = b и b a.p больше ни на что не указывает.
Я согласен, что это сложная проблема. Я думаю, что правильным было бы провести различие на уровне типов между типами, которые предоставляют только открытый доступ к публичным методам объекта (и которые, следовательно, должны подчиняться LSP), и типами, которые предоставляют доступ к внутреннему состоянию. Большинству методов потребуется принимать только первые в качестве параметров (и, таким образом, их можно будет использовать со всей иерархией классов), в то время как операторы присваивания и операторы копирования потребуют последнего в качестве своего параметра (и не подчиняются LSP). Но я могу кое-что упустить.
Как переназначили ссылку?
@metamorphosis Если a является ссылкой, a = b не создает "a" объект ссылки "b" (как "a = & b", если бы "a" был указателем)! Он вызывает оператор присваивания объекта, на который ссылается «a», который (обычно) переходит к перезаписи указанного объекта содержимым объекта «b».
Ах, понятно, да, этот ответ тоже хорошо это объясняет: stackoverflow.com/a/9293732/3454889
Многие ответы здесь, в частности, этот, хорошо объясняют, как нарезка объекта какие. Но, учитывая популярность этих вопросов и ответов, было бы уместно также упомянуть, что можно вообще избежать этой проблемы, убедившись, что нелистовые базовые классы должны быть абстрактными (см., Например, Правило 33 в Скотт Мейерс _More Effective C++).
Я провел тест на MSVC 2015, и в конце a_ref в точности равно b1. Трудно сказать, является ли a_ref франкестинским объектом, потому что он выглядит как полностью законная ссылка на объект типа A с содержимым b1.
@ user2286810 Конечно, если вы сравните a_ref и b1 как экземпляры A, вы ничего не заметите, потому что их A-часть действительно идентична. Но если вы сравните b1 и b2, вы заметите, что их A-часть идентична, но все элементы, добавленные B, по-прежнему имеют свои исходные значения.
Недавно я хотел создать структуру статуса (StatusB), которая содержала бы статусы из структуры StatusA, а также некоторые дополнительные статусы. Я решил унаследовать StatusB от StatusA, но не смог придумать способ обновить StatusB данными из StatusA без написания собственных конструкторов копирования. Похоже, это отличное решение моей проблемы!
«это потому, что для невиртуальных функций объявленный тип (который является A &) определяет, какая функция вызывается» - но только для функций, которые переопределяют базовую виртуальную функцию, и оператор присваивания в форме T& op=(T&) не входит в их число.
@ j_random_hacker: Ваш пример плохой - вам нужно вручную реализовать конструктор копирования для такого a, и очевидная реализация, которая делает A a1 = a2 безопасным, также делает A a = b безопасным. глянь сюда
это ужасно. я должен вручную исправить "назначить" методы, если мой макет класса изменится.
@ M.kazemAkhgary Ну вот и жизнь. И это на самом деле не страшно, потому что вам нужно изменить метод свояassign класса только в том случае, если класс получает новый член. Так что он не хуже, чем любой другой класс с переопределенными операторами присваивания или сравнения, каждый из которых обычно должен полностью обрабатывать переменные-члены все.
Нарезка означает, что данные, добавленные подклассом, отбрасываются, когда объект подкласса передается или возвращается по значению или от функции, ожидающей объект базового класса.
Объяснение: Рассмотрим следующее объявление класса:
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
Поскольку функции копирования базового класса ничего не знают о производном, копируется только базовая часть производного. Это обычно называется нарезкой.
Все это хорошие ответы. Я просто хотел бы добавить пример выполнения при передаче объектов по значению 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'
Привет. Отличный ответ, но у меня есть один вопрос. Если я сделаю что-то вроде этого ** dev d; base * b = & d; ** Нарезка тоже происходит?
@Adrian Если вы вводите новые функции-члены или переменные-члены в производном классе, они не будут доступны напрямую из указателя базового класса. Однако вы все равно можете получить к ним доступ из перегруженных виртуальных функций базового класса. Смотрите это: godbolt.org/z/LABx33
когда объект производного класса назначается объекту базового класса, дополнительные атрибуты объекта производного класса отсекаются (отбрасываются) из объекта базового класса.
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
}
Когда объект производного класса назначается объекту базового класса, все члены объекта производного класса копируются в объект базового класса, кроме членов, которых нет в базовом классе. Эти члены отсекаются компилятором. Это называется нарезкой объекта.
Вот пример:
#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'
Проголосовали против, потому что это не очень хороший пример. Это также не сработало бы, если бы вместо копирования d в b вы использовали указатель, и в этом случае d и e все еще существовали бы, но у Base нет этих членов. Ваш пример показывает только то, что вы не можете получить доступ к членам, которых нет в классе.
Я только что столкнулся с проблемой нарезки и тут же приземлился. Так что позвольте мне добавить к этому свои два цента.
Приведем пример из "производственного кода" (или чего-то похожего):
Допустим, у нас есть что-то, что отправляет действия. Например, пользовательский интерфейс центра управления.
Этот пользовательский интерфейс должен получить список вещей, которые в настоящее время могут быть отправлены. Итак, мы определяем класс, который содержит информацию об отправке. Назовем его 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 каким-то образом происходят.
В 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
}
Нарезка объекта происходит, когда объект производного класса назначается объекту базового класса, дополнительные атрибуты объекта производного класса отсекаются, чтобы сформировать объект базового класса.
Я вижу, что во всех ответах упоминается, когда нарезка объекта происходит при нарезке элементов данных. Здесь я привожу пример того, что методы не переопределяются:
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
Подробнее см. мой пост.
Очень информативно, но см. stackoverflow.com/questions/274626#274636 для примера того, как нарезка происходит во время вызовов методов (что подчеркивает опасность немного лучше, чем пример простого присваивания).