Хтось згадував це в 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
, uh, буде поводитися як A
. Як це дивно?
derived
до типу A
. Неявне кастинг - це завжди джерело несподіваної поведінки в C ++, тому що його часто важко зрозуміти, дивлячись на місцевий код, що відбувся акторський склад.
Це все хороші відповіді. Я просто хотів би додати приклад виконання при передачі об'єктів за значенням посиланням:
#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/ форум / тема163565.html
Тож це коли ви присвоюєте об’єкт підкласу суперкласу. Суперклас нічого не знає про додаткову інформацію в підкласі і не має місця для його зберігання, тому додаткова інформація стає "відрізаною".
Якщо ці посилання не дають достатньо інформації для "хорошої відповіді", відредагуйте своє запитання, щоб повідомити, що ще ви шукаєте.
Проблема нарізки є серйозною, оскільки це може призвести до пошкодження пам’яті, і дуже важко гарантувати, що програма не страждає від неї. Щоб розробити його з мови, класи, які підтримують успадкування, повинні бути доступними лише для посилання (не за значенням). Мова програмування D має цю властивість.
Розглянемо клас A і клас B, похідний від A. Пошкодження пам'яті може статися, якщо частина A має вказівник p, а екземпляр B, який вказує p на додаткові дані B. Потім, коли додаткові дані скидаються, p вказує на сміття.
Derived
це неявно конвертоване Base
.) Це, очевидно, суперечить принципу відкритого закриття та великому тягару з обслуговування.
У 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 ++.
struct
, сумісністю чи іншим безглуздим, як вам сказав будь-який випадковий священик ООП.
Base
повинен приймати рівно sizeof(Base)
байти в пам'яті з можливим вирівнюванням, можливо, саме тому "призначення" (on-stack-copy ) не буде копіювати похідних членів класу, їхні компенсації мають розмір поза. Щоб уникнути "втрати даних", просто використовуйте вказівник, як ніхто інший, оскільки пам'ять покажчика закріплена на місці та розмірі, тоді як стек дуже вольовий
Отже ... Чому втрачається отримана інформація погана? ... тому що автор похідного класу, можливо, змінив подання таким чином, що відсічення додаткової інформації змінює значення, яке представляє об'єкт. Це може статися, якщо похідний клас використовується для кешування представлення, яке є більш ефективним для певних операцій, але дорогим для перетворення назад у базове подання.
Також думав, що хтось також повинен згадати, що ви повинні зробити, щоб уникнути нарізки ... Отримайте копію стандартів кодування C ++, 101 настанови правил та найкращі практики. Справа з нарізкою - №54.
Він пропонує дещо складну схему, щоб повністю вирішити цю проблему: мати захищений конструктор копій, захищений чистий віртуальний DoClone та публічний клон з утвердженням, який підкаже, чи (подальший) похідний клас не зміг правильно виконати DoClone. (Метод Clone робить належну глибоку копію поліморфного об'єкта.)
Ви також можете позначити конструктор копій на базовій явній, що дозволяє для явного нарізання, якщо воно бажане.
1. ВИЗНАЧЕННЯ ПРОБЛЕМИ СЛІГУВАННЯ
Якщо D є похідним класом базового класу B, то ви можете призначити об'єкт типу Похідне змінної (або параметру) типу 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
Хоча вищезазначене призначення дозволене, значення, яке присвоюється змінному вихованцеві, втрачає поле своєї породи. Це називається проблемою нарізки .
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
), жодним чином не ПОМИЛКА, пов’язана зі СЛІЗУВАННЯм?
Мені здається, що нарізка - це не стільки інша проблема, як те, коли ваші власні класи та програма погано побудовані / розроблені.
Якщо я передаю об'єкт підкласу як параметр методу, який приймає параметр типу суперкласу, я, безумовно, повинен знати про це і знати внутрішньо, називаний метод буде працювати лише з об'єктом надкласу (він же базовим класом).
Мені здається лише необґрунтованим сподіванням, що надання підкласу, де вимагається базовий клас, якимось чином призведе до конкретних результатів підкласу, спричинить зрізання як проблему. Його або погана конструкція при використанні методу, або погана реалізація підкласу. Я здогадуюсь, що це, як правило, результат пожертвування хорошого дизайну OOP на користь доцільності чи підвищення продуктивності.
Гаразд, я спробую, прочитавши багато публікацій, що пояснюють нарізку об'єктів, але не те, як це стає проблематичним.
Порочний сценарій, який може призвести до пошкодження пам'яті, такий:
Нарізка означає, що дані, додані підкласом, відкидаються, коли об'єкт підкласу передається або повертається за значенням або від функції, що очікує об'єкта базового класу.
Пояснення: Розгляньте таку заяву класу:
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
}
Коли похідному класу "Об'єкт" присвоюється "Об'єкт базового класу", всі члени об'єкта похідного класу копіюються в об'єкт базового класу, крім членів, яких немає в базовому класі. Цих членів компілятор відрізає. Це називається "Обрізання об'єктів".
Ось приклад:
#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
, B
що вони робляться певним чином.