Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Не зная брода, не лезь в воду. Часть N4

Anna | 25.06.2014 | нет комментариев
В данный раз я хочу побеседовать о виртуальном наследовании в языке Си , и отчего его следует применять дюже осмотрительно. Предыдущие статьи: часть N1N2N3.
Статья написана по мотивам заметки “Грабли 2: Виртуальное наследование“. Статья отличная, но, на мой взор, несколько размыта, и новичок может не до конца уловить суть опасностей. Я решил предложить свой вариант изложения задач связанных с виртуальным наследованиям.

О инициализации виртуальных базовых классов

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

class Base { ... };
class X : public Base { ... };
class Y : public Base { ... };
class XY : public X, public Y { ... };

Тут всё легко. Члены невиртуального базового класса ‘Base’ размещаются как примитивные данные-члены производного класса. В итоге внутри объекта ‘XY’ мы имеем два самостоятельных подобъекта ‘Base’. Схематически это дозволено изобразить так:
Рисунок 1. Невиртуальное множественное наследование.
Рисунок 1. Невиртуальное множественное наследование.

Объект виртуального базового класса входит в объект производного класса только один раз. Устройство объекта ‘XY’ для приведенного ниже кода отображена на рисунке 2.

class Base { ... };
class X : public virtual Base { ... };
class Y : public virtual Base { ... };
class XY : public X, public Y { ... };

Рисунок 2. Виртуальное множественное наследование.
Рисунок 2. Виртуальное множественное наследование.

Память для разделяемого подобъекта ‘Base’, скорее каждого, будет выделена в конце объекта ‘XY’. Как именно будет устроен класс, зависит от компилятора. Скажем, в классах ‘X’ и ‘Y’ могут храниться указатели на всеобщий объект ‘Base’. Но как я понимаю, такой способ вышел из обихода. Почаще ссылка на разделяемый подобъект реализуется в виде смещения либо информации, которая хранится в таблице виртуальных функций.

Только «самый производный» класс ‘XY’ верно знает, где должна находиться память для подобъекта виртуального базового класса ‘Base’. Следственно инициализировать все подобъекты виртуальных базовых классов поручается самому производному классу.

Конструкторы ‘XY’ инициализируют подобъект ‘Base’ и указатели на данный объект в ‘X’ и ‘Y’. После этого инициализируются остальные члены классов ‘X’, ‘Y’, ‘XY’.

Позже того как подобъект ‘Base’ инициализируется в конструкторе ‘XY’, он не будет ещё раз инициализироваться конструктором ‘X’ либо ‘Y’. Как это будет сделано, зависит от компилятора. Скажем, компилятор может передавать особый добавочный довод в конструкторы ‘X’ и ‘Y’, тот, что будет указывать не инициализировать класс ‘Base’.

А сейчас самое увлекательное, приводящее ко многим непониманиям и ошибкам. Разглядим вот такие конструкторы:

X::X(int A) : Base(A) {}
Y::Y(int A) : Base(A) {}
XY::XY() : X(3), Y(6) {}

Какое число примет конструктор базового класса в качестве довода? Число 3 либо 6? Ни одно из них.

Конструктор ‘XY’ инициализирует воображаемый подобъект ‘Base’, но делает это неявно. Вызывается конструктор ‘Base’ по умолчанию.

Когда конструктор ‘XY’ вызывает конструктор ‘X’ либо ‘Y’, он не инициализирует ‘Base’ снова. Следственно очевидного обращения к ‘Base’ с каким-то доводом не происходит.

На этом приключения с виртуальными базовыми классами не заканчиваются. Помимо конструкторов существуют операторы присваивания. Если я не заблуждаюсь, эталон говорит, что генерируемый компилятором оператор присваивания может неоднократно исполнять присваивание подобъекту виртуального базового класса. А может только один раз. Так что не вестимо, сколько раз будет протекать копирование объекта ‘Base’.

Если вы реализуете свой оператор присваивания, то вы обязаны самосильно позаботься об однократном копировании объекта ‘Base’. Разглядим неверный код:

XY &XY::operator =(const XY &src)
{
  if (this != &src)
  {
    X::operator =(*this);
    Y::operator =(*this);
    ....
  }
  return *this;
}

Это код приведёт к двойному копированию объекта ‘Base’. Дабы этого избежать, в классах ‘X’ и ‘Y’ нужно реализовать функции, которые не будут копировать члены класса ‘Base’. Содержимое класса ‘Base’ копируется однократно тут же. Поправленный код:

XY &XY::operator =(const XY &src)
{
  if (this != &src)
  {
    Base::operator =(*this);
    X::PartialAssign(*this);
    Y::PartialAssign(*this);
    ....
  }
  return *this;
}

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

Виртуальные базовые классы и приведение типов

Из-за особенностей размещения виртуальных базовых классов в памяти, невозможно исполнить вот такие приведения типов:

Base *b = Get();
XY *q = static_cast<XY *>(b); // Оплошность компиляции
XY *w = (XY *)(b); // Оплошность компиляции

Впрочем, напористый программист может всё-таки привести тип, воспользовавшись оператором ‘reinterpret_cast’:

XY *e = reinterpret_cast<XY *>(b);

Впрочем скорее каждого это даст непригодный для применения итог. Адрес начала объекта ‘Base’ будет интерпретирован, как предисловие объект ‘XY’. А это вовсе не то, что нужно. Смотри объясняющий рисунок 3.

Исключительный метод исполнить приведение типа, воспользоваться оператором dynamic_cast. Впрочем код, где регулярно применяется dynamic_cast, нехорошо пахнет.
Рисунок 3. Приведение типов.
Рисунок 3. Приведение типов.

Отказываться ли от виртуального наследования?

Я согласен с суждением многих авторов, что следует различно чураться виртуального наследования. И от простого множественного наследования тоже отменнее уходить.

Виртуальное наследование порождает задачи при инициализации и копировании объектов. Инициализацией и копирование должен заниматься «самый производный» класс. А это значит, он должен знать интимные подробности об устройстве базовых классов. Образуется лишняя связанность между классами, которая усложняет конструкцию плана и принуждает делать добавочные правки в различных классах при рефакторинге. Всё это содействует ошибкам и усложняет осознавание плана новыми разработчиками.

Трудности приведений типов также содействуют происхождению ошибок. Отчасти задачи решаются при применении оператора dynamic_cast. Впрочем, это дюже неторопливый оператор. И если он начинает массово возникать в программе, то, скорее каждого, это свидетельствует о дрянный архитектуре плана. Примерно неизменно, дозволено реализовать конструкцию плана, не прибегая к множественному наследованию. Собственно, во многих языках вообще нет таких изысков. И это не мешает реализовывать крупные планы.

Нелепо настаивать отказаться от виртуального наследования. Изредка оно пригодно и комфортно. Впрочем стоит отлично подумать, раньше нагородить трудные классы. Неизменно отменнее вырастить лес маленьких классов с неглубокой иерархией, чем трудиться с несколькими больших деревьями. Скажем, Зачастую взамен множественного наследования дозволено воспользоваться а

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB