На цьому уроці ми розглянемо, що таке обрізка об’єктів в мові С++, як вона використовується і які є нюанси.
Обрізка об’єктів
Повернемося до прикладу з класами Parent і Child:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#include <iostream> class Parent { protected: int m_value; public: Parent(int value) : m_value(value) { } virtual const char* getName() const { return "Parent"; } int getValue() const { return m_value; } }; class Child: public Parent { public: Child(int value) : Parent(value) { } virtual const char* getName() const { return "Child"; } }; int main() { Child child(7); std::cout << "child is a " << child.getName() << " and has value " << child.getValue() << '\n'; Parent &ref = child; std::cout << "ref is a " << ref.getName() << " and has value " << ref.getValue() << '\n'; Parent *ptr = &child; std::cout << "ptr is a " << ptr->getName() << " and has value " << ptr->getValue() << '\n'; return 0; } |
Тут посилання ref і вказівник ptr посилаються/вказують на об’єкт child, який має як частину Parent, так і частину Child. Оскільки ref і ptr є типу Parent, то вони можуть бачити частину Parent об’єкта child. Частина Child об’єкта child існує протягом усього часу життя об’єкта, але доступ до неї для ref або ptr — закритий. Однак, використовуючи віртуальні функції, ми отримаємо доступ до “найдочірнішого” методу.
Відповідно, результат виконання програми:
child is a Child and has value 7
ref is a Child and has value 7
ptr is a Child and has value 7
Але що відбулося б, якби ми, замість створення посилання або вказівника класу Parent на об’єкт класу Child, просто присвоїли об’єкт класу Child об’єкту класу Parent?
|
1 2 3 4 5 6 7 8 |
int main() { Child child(7); Parent parent = child; // що відбудеться тут? std::cout << "parent is a " << parent.getName() << " and has value " << parent.getValue() << '\n'; return 0; } |
Пам’ятаєте, що child має як частину Parent, так і частину Child? Коли ми присвоюємо об’єкт класу Child об’єкту класу Parent, то копіюється лише частина Parent, частина Child не копіюється. У прикладі, наведеному вище, parent отримує копію частини Parent об’єкта child, а частина Child об’єкта child «обрізається». Це називається обрізкою об’єктів.
Оскільки змінна parent не має частини Child, то parent.getName() викликає Parent::getName().
Результат:
parent is a Parent and has value 7
Обрізка об’єктів і функції
Зараз ви можете подумати, що вищенаведений приклад безглуздий. Зрештою, навіщо нам присвоювати об’єкт child об’єкту parent таким чином? Повторювати це, швидше за все, ви не будете. Однак обрізка об’єктів досить-таки нерідко трапляється з функціями. Наприклад:
|
1 2 3 4 |
void printName(const Parent parent) // примітка: Передача по значенню { std::cout << "I am a " << parent.getName() << '\n'; } |
Це проста функція з константним об’єктом parent в якості параметру, який передається по значенню. Якщо ми викликатимемо цю функцію наступним чином:
|
1 2 3 4 5 6 7 |
int main() { Child ch(7); printName(ch); // упс, передача по значенню return 0; } |
То отримаємо:
I am a Parent
Ви, напевно, не помітили, що parent є параметром-значенням, а не параметром-посиланням. При виконанні printName(ch), ви, напевно, очікували, що parent.getName() викличе перевизначення getName(), яке виведе I am a Child, але це не так. Замість цього об’єкт ch класу Child обрізається, і тільки частина Parent копіюється в переданий параметр parent. При виконанні parent.getName(), незважаючи на те, що функція getName() є віртуальною, для неї не існує частини Child. Отже, отримали те, що отримали.
У цьому випадку все досить очевидно з того, що виводиться на екран. Але якщо у вас є функції, які нічого не виводять на екран, то відстежити таку помилку буде вже проблематично.
Звичайно, обрізки тут можна було б легко уникнути, використовуючи передачу по посиланню замість передачі по значенню (ось ще одна причина, по якій передача класів по посиланню замість передачі по значенню є хорошою ідеєю):
|
1 2 3 4 5 6 7 8 9 10 11 12 |
void printName(const Parent &parent) // примітка: Передача по посиланню { std::cout << "I am a " << parent.getName() << '\n'; } int main() { Child ch(7); printName(ch); return 0; } |
Результат:
Обрізка векторів
Ще одна помилка, з якою стикаються початківці при роботі з обрізкою, полягає в спробі реалізувати поліморфізм, використовуючи std::vector. Додамо до нашої програми наступний код:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <vector> int main() { std::vector<Parent> v; v.push_back(Parent(7)); // додаємо об'єкт класу Parent в наш вектор v.push_back(Child(8)); // додаємо об'єкт класу Child в наш вектор // Виводимо всі елементи нашого вектора for (int count = 0; count < v.size(); ++count) std::cout << "I am a " << v[count].getName() << " with value " << v[count].getValue() << "\n"; return 0; } |
Результат виконання програми:
I am a Parent with value 7
I am a Parent with value 8
Оскільки std::vector був оголошений як вектор класу Parent, то при доданні до нього Child(8) виконалася обрізка об’єкта.
Виправити це трохи складніше. Початківці намагаються зробити вектор з посиланнями на об’єкти, наприклад:
|
1 |
std::vector<Parent&> v; |
На жаль, це не спрацює. Елементи std::vector повинні бути об’єктами, яким можна переприсвоювати значення, тоді як посилання можуть бути ініціалізовані лише раз і переприсвоювати їм значення заборонено.
Одним із способів вирішення цієї проблеми є створення вектора з вказівниками на об’єкти:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <vector> int main() { std::vector<Parent*> v; v.push_back(new Parent(7)); // додаємо об'єкт класу Parent в наш вектор v.push_back(new Child(8)); // додаємо об'єкт класу Child в наш вектор // Виводимо всі елементи нашого вектора for (int count = 0; count < v.size(); ++count) std::cout << "I am a " << v[count]->getName() << " with value " << v[count]->getValue() << "\n"; for (int count = 0; count < v.size(); ++count) delete v[count]; return 0; } |
Результат виконання програми:
I am a Parent with value 7
I am a Child with value 8
Працює! Але це біль, тому що тепер нам доведеться мати справу з динамічним виділенням пам’яті.
На щастя, є ще один спосіб вирішення цієї проблеми. Стандартна бібліотека C++ надає клас std::reference_wrapper. По суті, std::reference_wrapper — це клас, який працює як посилання, але дозволяє виконувати операції присвоювання і копіювання і сумісний з std::vector.
Гарна новина полягає в тому, що вам не потрібно знати, як він реалізований для того, щоб його використовувати. Все, що вам потрібно знати:
std::reference_wrapper знаходиться в заголовковому файлі functional.
При створенні об’єкта класу std::reference_wrapper, цей об’єкт не може бути анонімним (оскільки анонімні об’єкти мають область видимості виразу, що може призвести до виникнення висячого посилання).
Для отримання об’єкта з std::reference_wrapper використовується метод get().
Перепишемо наш код, додавши std::reference_wrapper:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <vector> #include <functional> // для std::reference_wrapper int main() { std::vector<std::reference_wrapper<Parent> > v; Parent p(7); // p і ch не можуть бути анонімними об'єктами Child ch(8); v.push_back(p); // додаємо об'єкт класу Parent в наш вектор v.push_back(ch); // додаємо об'єкт класу Child в наш вектор // Виводимо всі елементи нашого вектора for (int count = 0; count < v.size(); ++count) std::cout << "I am a " << v[count].get().getName() << " with value " << v[count].get().getValue() << "\n"; // використовуємо .get() для отримання елементів з std::reference_wrapper return 0; } |
Результат виконання програми:
I am a Parent with value 7
I am a Child with value 8
Все чудово, і нам не потрібно морочитися з динамічним виділенням пам’яті.
Висновки
Хоча мова C++ підтримує присвоювання об’єктів дочірніх класів об’єктам батьківського класу за допомогою обрізки об’єктів, це приносить більше болю, ніж користі, тому рекомендується уникати випадків з виконанням обрізки об’єктів. Коли справа доходить до роботи з дочірніми класами, завжди переперевіряйте параметри своїх функцій, щоб ні в якому разі не виконувалася передача по значенню.
