Урок №178. Обрізка об’єктів

  Юрій  | 

  Оновл. 27 Вер 2021  | 

 164

На цьому уроці ми розглянемо, що таке обрізка об’єктів в мові С++, як вона використовується і які є нюанси.

Обрізка об’єктів

Повернемося до прикладу з класами Parent і Child:

Тут посилання 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?

Пам’ятаєте, що 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 таким чином? Повторювати це, швидше за все, ви не будете. Однак обрізка об’єктів досить-таки нерідко трапляється з функціями. Наприклад:

Це проста функція з константним об’єктом parent в якості параметру, який передається по значенню. Якщо ми викликатимемо цю функцію наступним чином:

То отримаємо:

I am a Parent

Ви, напевно, не помітили, що parent є параметром-значенням, а не параметром-посиланням. При виконанні printName(ch), ви, напевно, очікували, що parent.getName() викличе перевизначення getName(), яке виведе I am a Child, але це не так. Замість цього об’єкт ch класу Child обрізається, і тільки частина Parent копіюється в переданий параметр parent. При виконанні parent.getName(), незважаючи на те, що функція getName() є віртуальною, для неї не існує частини Child. Отже, отримали те, що отримали.

У цьому випадку все досить очевидно з того, що виводиться на екран. Але якщо у вас є функції, які нічого не виводять на екран, то відстежити таку помилку буде вже проблематично.

Звичайно, обрізки тут можна було б легко уникнути, використовуючи передачу по посиланню замість передачі по значенню (ось ще одна причина, по якій передача класів по посиланню замість передачі по значенню є хорошою ідеєю):

Результат:

I am a Child

Обрізка векторів

Ще одна помилка, з якою стикаються початківці при роботі з обрізкою, полягає в спробі реалізувати поліморфізм, використовуючи std::vector. Додамо до нашої програми наступний код:

Результат виконання програми:

I am a Parent with value 7
I am a Parent with value 8

Оскільки std::vector був оголошений як вектор класу Parent, то при доданні до нього Child(8) виконалася обрізка об’єкта.

Виправити це трохи складніше. Початківці намагаються зробити вектор з посиланнями на об’єкти, наприклад:

На жаль, це не спрацює. Елементи std::vector повинні бути об’єктами, яким можна переприсвоювати значення, тоді як посилання можуть бути ініціалізовані лише раз і переприсвоювати їм значення заборонено.

Одним із способів вирішення цієї проблеми є створення вектора з вказівниками на об’єкти:

Результат виконання програми:

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:

Результат виконання програми:

I am a Parent with value 7
I am a Child with value 8

Все чудово, і нам не потрібно морочитися з динамічним виділенням пам’яті.

Висновки

Хоча мова C++ підтримує присвоювання об’єктів дочірніх класів об’єктам батьківського класу за допомогою обрізки об’єктів, це приносить більше болю, ніж користі, тому рекомендується уникати випадків з виконанням обрізки об’єктів. Коли справа доходить до роботи з дочірніми класами, завжди переперевіряйте параметри своїх функцій, щоб ні в якому разі не виконувалася передача по значенню.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (2 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *