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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
791
0
198 059
18

Ответы 18

Третье совпадение в 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.

Очень информативно, но см. stackoverflow.com/questions/274626#274636 для примера того, как нарезка происходит во время вызовов методов (что подчеркивает опасность немного лучше, чем пример простого присваивания).

Blair Conrad 08.11.2008 16:53

Интересно. Я программировал на C++ 15 лет, и эта проблема никогда не приходила мне в голову, поскольку я всегда передавал объекты по ссылке из соображений эффективности и личного стиля. Показывает, как полезные привычки могут вам помочь.

Karl Bielefeldt 02.02.2011 06:48

@ Дэвид: не могли бы вы подробно объяснить свою строчку Then the information in b about member bar is lost in a.. повреждаются ли данные foo после назначения? но почему?

user72424 21.07.2011 12:51

@Hades: данные не повреждаются. Просто невозможно сказать a.bar, потому что компилятор считает, что a относится к типу A. Если вы вернете его к типу B, тогда вы сможете получить все скрытые («отрезанные») поля.

Felix Dombek 12.08.2011 12:24

@Felix Спасибо, но я не думаю, что обратное приведение (поскольку не арифметика указателя) будет работать, A a = b;a теперь является объектом типа A, который имеет копию B::foo. Думаю, сейчас будет ошибкой отбрасывать его обратно.

user72424 12.08.2011 16:27

@Hades: Я понимаю, что вы имеете в виду, я думал об указателях. Вы правы, присваивание - это, конечно, не приведение - на самом деле в стеке размещается новый объект. Тогда bar в b вообще не поврежден, просто не копируется оператором присваивания, созданным компилятором, поэтому a теперь является полностью новым объектом типа A с членом a.foo, установленным на то же значение, что и b.foo.

Felix Dombek 12.08.2011 20:22

@ Карл: не помогает в случае переуступки. B& b = xxx; b = someDerivedClass(); еще провоцирует нарезку. Просто обычно проблема остается незамеченной.

Matthieu M. 08.09.2011 21:12

Другой ответ, который объясняет нарезку, но не проблему.

Dude 18.10.2012 07:08

Это не «нарезка» или, по крайней мере, ее мягкий вариант. Настоящая проблема возникает, если вы делаете B b1; B b2; A& b2_ref = b2; b2 = b1. Вы могли подумать, что скопировали b1 на b2, но это не так! Вы скопировали частьb1 в b2 (часть b1, которую B унаследовал от A), а остальные части b2 оставили без изменений. b2 - теперь франкенштейновское существо, состоящее из нескольких битов b1, за которыми следуют несколько фрагментов b2. Фу! Голосование против, потому что я думаю, что ответ вводит в заблуждение.

fgp 22.01.2013 18:07

@fgp Ваш комментарий должен быть следующим: B b1; B b2; A& b2_ref = b2; b2_ref = b1 "Настоящая проблема возникает, если вы" ... унаследован от класса с невиртуальным оператором присваивания. A вообще предназначен для производства? В нем нет виртуальных функций. Если вы производите от типа, вы должны иметь дело с тем фактом, что его функции-члены могут быть вызваны!

curiousguy 29.06.2013 18:54

связь с парой примеров того, как избежать проблемы нарезки с помощью указателей или ссылок

pablo_worker 28.03.2017 17:50

@fgp, поэтому вы не помещаете B в имя ссылки на A.

Euri Pinhollow 26.10.2017 10:43

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

user7860670 12.03.2018 13:12

Какова сигнатура неявно сгенерированного конструктора копирования, используемого в объявлении A? т.е. в A a = b;?

user2023370 20.01.2021 14:44

Это A(const A&).

user2023370 20.01.2021 15:31

Если у Вас есть базовый класс 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 22.01.2013 20:39

@fgp: Это удивительно, потому что вы не сдать пятёрку к функции.

Black 03.03.2013 11:03

@Black: Но wantAnA сказал, что это хочет an A, так что это то, что он получает. Это то же самое, что объявить функцию, которая принимает int, передать 0,1, а затем пожаловаться, что функция получает 0 ...

fgp 20.03.2013 17:44

@fgp: поведение аналогично. Однако для среднего программиста на C++ это может быть менее очевидным. Насколько я понял вопрос, никто не «жалуется». Это просто о том, как компилятор справляется с ситуацией. Имхо, лучше вообще избегать нарезки, передавая (константные) ссылки.

Black 07.04.2013 16:13

@Black: Это, ИМХО, опасный совет. Если значение позже копируется, передача по ссылке предотвращает включение семантики перемещения, потому что даже если значение изначально было rvalue, внутри функции будет lvalue.

fgp 12.04.2013 04:33

Значит, вы бы отказались от наследования в пользу семантики «перемещения»? Честно говоря, я был бы удивлен, если бы это считалось общим «дизайнерским предпочтением».

Thomas W 07.05.2013 14:01

@ThomasW Нет, я бы не выбрасывал наследование, а использовал ссылки. Если сигнатура wantAnA была бы void wantAnA (const A & myA), значит, нарезки не было. Вместо этого передается доступная только для чтения ссылка на объект вызывающего.

Black 28.05.2013 11:44

проблема в основном связана с автоматическим преобразованием, которое компилятор выполняет от derived к типу A. Неявное приведение типов всегда является источником неожиданного поведения в C++, потому что часто бывает трудно понять, глядя на локальный код, что произошло приведение.

pqnet 07.08.2014 03:15

@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 17.05.2015 08:35

@ user1902689 Чтобы функция была перегружена в производном классе, вы должны сделать ее «виртуальной», например virtual void print () const {....}

Black 18.05.2015 23:12

@pqnet это не актерский состав. A создается на месте вызова через вызов A::A(const A &).

Caleth 23.04.2019 13:36

@Caleth Я имел в виду автоматическое преобразование const derived& в const A&, которое происходит неявно, когда вы копируете инициализируете экземпляр A. Мой юридический язык не соответствовал стандарту 5 лет назад, поэтому я использовал "приведение" для идентификации всех преобразований, а не только явного выражения приведения.

pqnet 23.04.2019 19:02

@pqnet нет B&, нет преобразования, ссылка (аргумент) (конструктора копирования) напрямую связана с подобъектом AB.

Caleth 23.04.2019 19:16

@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.

pqnet 23.04.2019 19:46

@fgp Я не ожидаю этого в ООП.

M.kazem Akhgary 28.09.2019 11:14

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

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

Объясните, пожалуйста, как может произойти повреждение памяти.

foraidt 08.11.2008 15:48

Дано: class A {virtual void foo () {}}; класс B: A {int * p; void foo () {* p = 3; }}; Теперь нарежьте B, когда он назначен A, вызовите foo (), который вызывает B :: foo (), и вуаля! Присвоение повреждения памяти из-за мусора значения для B :: p.

Walter Bright 08.11.2008 22:28

Ага, Уолтер смешивает присвоение указателя (где A * указывает на объект B, поэтому B :: p НЕ теряется) и назначение объекта (после которого вызывается A :: foo).

MSalters 10.11.2008 17:20

Я забыл, что копирующий ctor сбросит vptr, моя ошибка. Но вы все равно можете получить повреждение, если у A есть указатель, а B устанавливает его так, чтобы он указывал на раздел B, который отсекается.

Walter Bright 11.11.2008 05:21

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

Weeble 11.02.2009 14:54

@Weeble - вот почему вы переопределяете деструктор по умолчанию, оператор присваивания и конструктор копирования в этих случаях.

Bjarke Freund-Hansen 24.07.2009 23:55

Для полиморфизма требуется виртуальная функция или CRTP (шаблоны, чтобы это произошло во время компиляции) ...

Dude 18.10.2012 06:32

@Weeble: Что делает нарезку объекта хуже, чем общие исправления указателя, так это то, что чтобы быть уверенным, что вы предотвратили нарезку, базовый класс должен предоставлять конструкторы преобразования для каждого производного класса. (Почему? Любые производные классы, которые пропущены, могут быть подхвачены ctor копирования базового класса, поскольку Derived неявно конвертируется в Base.) Это, очевидно, противоречит принципу открытого-закрытого и требует больших затрат на обслуживание.

j_random_hacker 24.10.2012 16:30

Это один из самых разочаровывающих ответов на C++ SO. Здесь нет примера, это действительно расплывчато и просто неверно: нарезка обычно не вызывает «повреждения памяти» (не могли бы вы уточнить термин?), Я не вижу случая, когда это повредит стек или что-либо, что может повлиять на выполнение. Однако это может быть ошибка программиста в его собственном приложении, но это не «повреждение». Наконец, большинство подобных назначений будет выполняться с помощью указателя, без нарезки, шаблон из OP никогда не обсуждается в самых популярных ссылках.

Croll 16.11.2018 23:18

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

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

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

Вы также можете пометить конструктор копирования на базе явным образом, что позволяет при желании явно нарезать фрагменты.

"Вы также можете пометить конструктор копирования на основе явного", который вообще помогает нет.

curiousguy 05.08.2012 02:25

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

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

««нормальное» поведение объекта», это не «нормальное поведение объекта», это справочная семантика. И это связывает никоим образом не с C struct, совместимостью или прочей ерундой, которую вам сказал любой случайный священник ООП.

curiousguy 27.11.2011 15:27

@curiousguy Аминь, брат. Печально видеть, как часто C++ терпит поражение из-за того, что он не является Java, когда семантика значений - одна из вещей, которые делают C++ таким безумно мощным.

fgp 22.01.2013 20:42

Это не особенность, не причуда / недостаток. Это нормальное поведение при копировании в стеке, поскольку вызов функции с аргументом или (такой же) переменной стека распределения типа Base должен занимать в памяти ровно sizeof(Base) байта с возможным выравниванием, возможно, поэтому «присваивание» (в стеке -copy) не будет копировать члены производного класса, их смещения находятся за пределами sizeof. Чтобы избежать «потери данных», просто используйте указатель, как и любой другой, поскольку память указателя фиксирована по месту и размеру, тогда как стек очень изменчив.

Croll 16.11.2018 23:32

Определенно недостаток C++. Назначение производного объекта базовому объекту должно быть запрещено, в то время как привязка производного объекта к ссылке или указателю базового класса должна быть в порядке.

John Z. Li 02.05.2019 12:11

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

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

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

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

Arafangion 22.12.2010 14:06

защищенная копия / присвоение базового класса и эта проблема решена.

Dude 18.10.2012 06:51

Ты прав. Хорошая практика - использовать абстрактные базовые классы или ограничивать доступ к копированию / назначению. Однако его не так просто обнаружить, когда он есть, и легко забыть о заботе. Вызов виртуальных методов с помощью split * может привести к загадочным вещам, если вы уйдете без нарушения прав доступа.

Dude 18.10.2012 07:06

Из моих курсов программирования на C++ в университете я вспоминаю, что существовали передовые практики, согласно которым для каждого создаваемого нами класса мы должны были писать конструкторы по умолчанию, конструкторы копирования и операторы присваивания, а также деструктор. Таким образом, при написании класса вы убедитесь, что построение копии и тому подобное происходит именно так, как вам нужно, а не позже, когда проявляется какое-то странное поведение.

Minok 25.07.2014 21:24

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. Нарезка - определенно "проблема", если она нежелательна, но здесь я этого не вижу.

curiousguy 18.02.2012 08:18

"((Dog *)ptrP)" Я предлагаю использовать static_cast<Dog*>(ptrP)

curiousguy 18.02.2012 08:20

Я предлагаю указать, что вы заставите строку 'порождать' в конечном итоге утечку памяти без виртуального деструктора (деструктор 'string' не будет вызываться) при удалении через 'ptrP' ... Почему то, что вы показываете, проблематично? Исправление в основном связано с правильным дизайном класса. Проблема в этом случае заключается в том, что запись конструкторов для управления видимостью при наследовании утомительна и легко забывается. Вы не попадете в опасную зону с вашим кодом, поскольку здесь нет никакого полиморфизма, или даже упомянутого (здесь нарезка усечет ваш объект, но не приведет к сбою вашей программы).

Dude 18.10.2012 06:58

-1 Это совершенно не объясняет сути проблемы. C++ имеет семантику значений, эталонную семантику нет, как и Java, так что этого вполне следовало ожидать. И «исправление» действительно является примером настоящего кода C++ какой ужас. «Устранение» несуществующих проблем, подобных этому типу нарезки, путем использования динамического распределения - это рецепт ошибочного кода, утечки памяти и ужасной производительности. Обратите внимание, что есть случаи являются, когда нарезка плохая, но этот ответ не может указать на них. Подсказка: беда начнется, если назначить через использованная литература.

fgp 22.01.2013 20:35

Вы хоть понимаете, что попытка доступа к члену неопределенного типа (Dog::breed) не является ОШИБКОЙ, связанной с РАЗРЕЗОМ?

Croll 16.11.2018 23:22

Должен дать -1, это ошибка времени компиляции, а не ошибка времени выполнения, Pet :: порода не существует.

Nicholas Pipitone 17.09.2020 01:20

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

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

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

Не могли бы вы дать дополнительные подробности? Чем ваш ответ отличается от уже опубликованных?

Alexis Pigeon 29.11.2012 16:55

Думаю, было бы неплохо дополнительное объяснение.

looper 29.11.2012 16:55

Большинство ответов здесь не объясняют, в чем заключается настоящая проблема с нарезкой. Они объясняют только безобидные случаи разрезания, но не предательские. Предположим, как и другие ответы, что вы имеете дело с двумя классами 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.

curiousguy 29.06.2013 19:05

IMHO, проблема в том, что существует два разных типа заменяемости, которые могут подразумеваться наследованием: либо любое значение derived может быть присвоено коду, ожидающему значение base, либо любая производная ссылка может использоваться в качестве базовой ссылки. Я хотел бы видеть язык с системой типов, в которой обе концепции рассматриваются по отдельности. Во многих случаях производная ссылка должна быть заменена базовой ссылкой, но производные экземпляры не должны быть заменены на базовые; есть также много случаев, когда экземпляры должны быть конвертируемыми, но ссылки не должны заменять.

supercat 12.08.2013 20:11

По идее, в .NET, если функция возвращает KeyValuePair<SiameseCat, ToyotaPrius>, следует иметь возможность сохранить этот результат в месте хранения типа KeyValuePair<Animal, Vehicle> не потому, что экземпляр первого является экземпляром второго, а, скорее, потому, что возвращаемое значение интерпретируется как KVP<A,V> фактически превратил бы его в единое целое. К сожалению, для этого потребуется отдельная иерархия от обычного наследования, поскольку упакованный экземпляр первого типа определенно не эквивалентен упакованному экземпляру последнего.

supercat 12.08.2013 20:19

Я не понимаю, что же такого плохого в вашем «предательском» деле. Вы заявили, что хотите: 1) получить ссылку на объект класса A и 2) преобразовать объект b1 в класс A и скопировать его содержимое в ссылку класса A. Что на самом деле неверно, так это правильная логика, лежащая в основе данный код. Другими словами, вы взяли небольшую рамку изображения (A), поместили ее поверх более крупного изображения (B) и нарисовали через эту рамку, жалуясь позже, что ваше большое изображение теперь выглядит некрасиво :) выглядит неплохо, как и хотел художник, правда? :)

Mladen B. 14.11.2013 17:05

Иными словами, проблема в том, что C++ по умолчанию предполагает очень сильный тип заменяемость - он требует, чтобы операции базового класса корректно работали с экземплярами подкласса. И это даже для операций, которые компилятор автоматически сгенерировал как присваивание. Так что недостаточно не испортить свои собственные операции в этом отношении, вы также должны явно отключить неправильные, сгенерированные компилятором. Или, конечно, держитесь подальше от публичного наследования, что обычно бывает хорошим советом ;-)

fgp 16.11.2013 20:31

@supercat Я немного не понимаю, какое различие вы проводите. Это 1) прозрачная передача производного в качестве замены базы vs 2) непрозрачная передача ссылки на производное, как если бы на самом деле это была ссылка на базу?

Praxeolitic 17.06.2014 07:30

В .NET с каждым типом структуры связан тип объекта кучи, в который он может быть неявно преобразован. Если тип структуры реализует интерфейс, методы этого интерфейса могут быть вызваны в структуре «на месте». Если у кого-то была структура FooStruct<T> с полем типа T, интерфейс реализации IFoo<T>, содержащий свойство Foo типа T, которое могло читать и записывать поле, то, если FooStruct<Cat> унаследован от FooStruct<Animal>, он должен реализовывать IFoo<Animal>, подразумевая, что можно хранить Animal к нему.

supercat 17.06.2014 07:43

Сохранение FooStruct<Cat> в переменной типа FooStruct<Animal> превратит его в FooStruct<Animal>, который может содержать Animal в своем поле, но приведение его к IFoo<Animal> превратит его в тип объекта кучи, связанный с FooStruct<Cat>, который должен был бы реализовать IFoo<Animal>, но не может принять Animal, не относящийся к кошке, хранящийся в его собственности.

supercat 17.06.2014 07:44

Другой распространенный подход - просто отключить оператор копирования и присваивания. Для классов в иерархии наследования обычно нет причин использовать значение вместо ссылки или указателя.

Siyuan Ren 22.08.2014 14:48

@supercat "если FooStruct <Cat> унаследован от FooStruct <Animal>". Эээ, что? Как структура может наследовать от самой себя в .Net? Вы пишете очень странные вещи, которые кажутся ненастоящими.

Ark-kun 04.11.2014 21:43

@ Арк-кун: Я пытался указать на проблему, которая может возникнуть если бы такое было разрешено. Если бы существовал способ объявления типа структуры, который не мог быть помещен в коробку «как его собственный тип» [он все еще мог быть «упакован», сохраняя его в одноэлементном массиве], это позволило бы .NET поддерживать сценариям наследования, которым в настоящее время препятствует тот факт, что для каждого типа структуры среда выполнения также эффективно определяет тип объекта кучи, который имеет те же отношения наследования. Любые отношения, которые не могут работать с объектами кучи, также не могут работать со структурами.

supercat 04.11.2014 22:30

@ Ark-kun Он не унаследовал бы от себя, FooStruct<Cat> и FooStruct<Animal>не одного и того же типа. Вы не можете этого сделать в Java, потому что два являются, что касается среды выполнения, одного типа. Но, например, C++, это два совершенно разных типа. Вы должны иметь возможность использовать специализации шаблонов, чтобы разрешить один специализированный тип (в данном случае FooStruct<Cat>) от другого (в данном случае FooStruct<Animal>). Однако не уверен в .NET - он делает имеет специализацию шаблонов AFAIK, но, я думаю, не совсем такие общие, как у C++.

fgp 05.11.2014 04:15

Что за? Я понятия не имел, что операторы могут быть помечены как виртуальные

paulm 02.03.2015 18:23

«Вероломное» дело действительно коварное, и я его раньше не рассматривал. Но то, что вы называете «безобидным», вовсе не безобидно - почти в 100% случаев писать этот код является ошибкой, и язык должен был быть спроектирован так, чтобы он приводил к ошибке компиляции. LSP означает, что производный объект должен правильно вести себя при обработке как объект базового класса, но «поведение» означает только «ответ на вызовы общедоступного метода». LSP позволяет произвольно переопределить внутреннее состояние (то, что копирует operator=()) в B, поэтому копирование этого в экземпляр A может даже создать UB.

j_random_hacker 28.07.2015 17:00

@j_random_hacker Я не согласен. Поведение C++ очень разумно для языка, который имеет сложные типы по значению и наследование. Я согласен с тем, что это может быть удивительно, если вы пришли из языка, в котором объекты всегда имеют поведение по ссылке, например Java или .NET, но изучение подобных вещей - это всего лишь часть обучения эффективному программированию на C++. Я не понимаю, как A a = b мог вызвать UB, чего не могло случиться и с B b2 = b.

fgp 28.07.2015 17:41

@j_random_hacker В качестве более убедительного аргумента в пользу того, почему C++ должен предотвращать мягкий случай, рассмотрим сигнатуру оператора присваивания A - его A& operator=(const A&). Таким образом, единственный способ избежать того, чтобы этот оператор был с аргументом, являющимся экземпляром B, - это не приводить экземпляры B к const A& автоматически. Но этот бы мешает LSP ...

fgp 28.07.2015 19:47

Если один или несколько элементов данных 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 больше ни на что не указывает.

j_random_hacker 28.07.2015 20:53

Я согласен, что это сложная проблема. Я думаю, что правильным было бы провести различие на уровне типов между типами, которые предоставляют только открытый доступ к публичным методам объекта (и которые, следовательно, должны подчиняться LSP), и типами, которые предоставляют доступ к внутреннему состоянию. Большинству методов потребуется принимать только первые в качестве параметров (и, таким образом, их можно будет использовать со всей иерархией классов), в то время как операторы присваивания и операторы копирования потребуют последнего в качестве своего параметра (и не подчиняются LSP). Но я могу кое-что упустить.

j_random_hacker 28.07.2015 21:03

Как переназначили ссылку?

metamorphosis 24.05.2016 14:45

@metamorphosis Если a является ссылкой, a = b не создает "a" объект ссылки "b" (как "a = & b", если бы "a" был указателем)! Он вызывает оператор присваивания объекта, на который ссылается «a», который (обычно) переходит к перезаписи указанного объекта содержимым объекта «b».

fgp 24.05.2016 15:26

Ах, понятно, да, этот ответ тоже хорошо это объясняет: stackoverflow.com/a/9293732/3454889

metamorphosis 25.05.2016 04:16

Многие ответы здесь, в частности, этот, хорошо объясняют, как нарезка объекта какие. Но, учитывая популярность этих вопросов и ответов, было бы уместно также упомянуть, что можно вообще избежать этой проблемы, убедившись, что нелистовые базовые классы должны быть абстрактными (см., Например, Правило 33 в Скотт Мейерс _More Effective C++).

dfrib 08.07.2016 13:22

Я провел тест на MSVC 2015, и в конце a_ref в точности равно b1. Трудно сказать, является ли a_ref франкестинским объектом, потому что он выглядит как полностью законная ссылка на объект типа A с содержимым b1.

user2286810 11.02.2017 20:51

@ user2286810 Конечно, если вы сравните a_ref и b1 как экземпляры A, вы ничего не заметите, потому что их A-часть действительно идентична. Но если вы сравните b1 и b2, вы заметите, что их A-часть идентична, но все элементы, добавленные B, по-прежнему имеют свои исходные значения.

fgp 21.02.2017 17:33

Недавно я хотел создать структуру статуса (StatusB), которая содержала бы статусы из структуры StatusA, а также некоторые дополнительные статусы. Я решил унаследовать StatusB от StatusA, но не смог придумать способ обновить StatusB данными из StatusA без написания собственных конструкторов копирования. Похоже, это отличное решение моей проблемы!

Armandas 18.08.2017 12:24

«это потому, что для невиртуальных функций объявленный тип (который является A &) определяет, какая функция вызывается» - но только для функций, которые переопределяют базовую виртуальную функцию, и оператор присваивания в форме T& op=(T&) не входит в их число.

Euri Pinhollow 29.01.2018 02:04

@ j_random_hacker: Ваш пример плохой - вам нужно вручную реализовать конструктор копирования для такого a, и очевидная реализация, которая делает A a1 = a2 безопасным, также делает A a = b безопасным. глянь сюда

Eric 11.06.2019 11:09

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

M.kazem Akhgary 28.09.2019 11:22

@ M.kazemAkhgary Ну вот и жизнь. И это на самом деле не страшно, потому что вам нужно изменить метод свояassign класса только в том случае, если класс получает новый член. Так что он не хуже, чем любой другой класс с переопределенными операторами присваивания или сравнения, каждый из которых обычно должен полностью обрабатывать переменные-члены все.

fgp 16.02.2020 14:33

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

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

           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 10.07.2018 09:58

@Adrian Если вы вводите новые функции-члены или переменные-члены в производном классе, они не будут доступны напрямую из указателя базового класса. Однако вы все равно можете получить к ним доступ из перегруженных виртуальных функций базового класса. Смотрите это: godbolt.org/z/LABx33

Vishal Sharma 25.12.2019 17:20

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

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 нет этих членов. Ваш пример показывает только то, что вы не можете получить доступ к членам, которых нет в классе.

Stefan Fabian 29.03.2019 15:33

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

Приведем пример из "производственного кода" (или чего-то похожего):


Допустим, у нас есть что-то, что отправляет действия. Например, пользовательский интерфейс центра управления. Этот пользовательский интерфейс должен получить список вещей, которые в настоящее время могут быть отправлены. Итак, мы определяем класс, который содержит информацию об отправке. Назовем его 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

Подробнее см. мой пост.

Другие вопросы по теме