Урок №170. Обрезка объектов

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 30416

 ǀ   9 

На этом уроке мы рассмотрим, что такое обрезка объектов в языке С++, как она используется и какие есть нюансы.

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

Вернемся к примеру с классами 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 (163 оценок, среднее: 4,90 из 5)
Загрузка...

Комментариев: 9

  1. kmish:

    Сомнительная польза от этого std::reference_wrapper…

    Вот пример с указателями без выделения памяти. Проще намного:

    Результат:

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

    1. r4dish:

      Ну, если ты собираешься делать всё в пределах одной функции, то возможно, хотя все равно код выглядит опасно.

      Дело в том, что переменные «p» и «ch» у тебя локальные, и будут автоматически уничтожены при выходе из области видимости функции. А адреса их ты сохранил в вектор. В итоге в векторе у тебя мусорные указатели.

  2. Мгер:

    Не понял, зачем использовать reference_wrapper вместо Parent*, если все равно создаются не анонимные переменные с автоматической временем жизни.

    1. Мгер:

      Можно прочитать "Урок №193. std::unique_ptr" и использовать умные ссылки для работы с указателями в векторе, и не париться на счет утечки памяти.

  3. Мгер:

    Пример с reference_wrapper, не понятно для чего его использовать, если у наших объектов так и так есть имена.
    Более информативно, если создание объектов и добавление в вектор происходило во вложенном блоке:

    Я для себя добавил статическое поле cnt.

    1. Сергей:

      я что-то не понимаю почему после выхода из локальной области видимости (после скобок) — выполняются деструкцторы объектов Child и Parent. Соответственно в цыкле выводится содержимое вектора, хотя объекта уже нет. Почему выводятся данные, а не мусор?

    2. Sergey:

      А вот если создание объектов происходит в конструкторе класса например, то reference wrapper уже не помогает, так что честно говоря в таком применении профит от него не понял, буду благодарен если кто пояснит <3

  4. dshadov:

    Добрый день!
    Похоже у Вас во втором примере с вектором опечатка. Вместо

    имелось ввиду

    1. Фото аватара Юрий:

      Привет. Спасибо, исправил.

Добавить комментарий для Мгер Отменить ответ

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