Наследование. Виртуальные функции / FAQ C++

Добавлено 10 декабря 2020 в 05:33

Что такое «виртуальная функция-член»?

Виртуальные функции-члены – это ключ к объектно-ориентированной парадигме, например, упрощение вызова нового кода старым кодом.

Виртуальная функция позволяет производным классам заменять реализацию, предоставляемую базовым классом. Компилятор гарантирует, что замена всегда вызывается всякий раз, когда рассматриваемый объект действительно принадлежит производному классу, даже если к объекту обращается указатель базового класса, а не указатель производного класса. Это позволяет заменять алгоритмы базового класса в производном классе, даже если пользователи не знают о производном классе.

Производный класс может либо полностью заменить («переопределить») функцию-член базового класса, либо частично заменить («расширить») функцию-член базового класса. Последнее достигается за счет того, что функция-член производного класса при необходимости вызывает функцию-член базового класса.


Почему функции-члены не являются виртуальными по умолчанию?

Потому что многие классы не предназначены для использования в качестве базовых классов. Например, смотрите класс complex.

Кроме того, объекты класса с виртуальной функцией требуют пространства, необходимого для механизма вызова виртуальной функции – обычно одно слово на объект. Эти накладные расходы могут быть значительными и могут мешать совместимости компоновки с данными из других языков (например, C и Fortran).

Смотрите «Дизайн и эволюция C++» для более подробного обоснования.


Как в C++ добиться динамической привязки и статической типизации?

Когда у вас есть указатель на объект, этот объект может фактически относиться к классу, производному от класса указателя (например, Vehicle* (транспортное средство), который фактически указывает на объект Car (автомобиль); это называется «полиморфизмом»). Таким образом, существует два типа: (статический) тип указателя (в данном случае Vehicle) и (динамический) тип объекта, на который тот указывает (в данном случае Car).

Статическая типизация означает, что законность вызова функции-члена проверяется в самый ранний возможный момент: компилятором во время компиляции. Компилятор использует статический тип указателя, чтобы определить, корректен ли вызов функции-члена. Если тип указателя может обрабатывать функцию-член, то ее, разумеется, может обрабатывать и объект, на который указывает этот указатель. Например, если Vehicle имеет определенную функцию-член, то Car также имеет эту функцию-член, поскольку Car является разновидностью Vehicle.

Динамическое связывание означает, что адрес кода при вызове функции-члена определяется в последний возможный момент: на основе динамического типа объекта во время выполнения. Это называется «динамической привязкой», потому что привязка к фактически вызываемому коду выполняется динамически (во время выполнения). Динамическая привязка – это результат использования виртуальных функций.


Что такое чисто виртуальная функция?

Чисто виртуальная функция – это функция, которая должна быть переопределена в производном классе и должна быть не определена в базовом классе. Виртуальная функция объявляется «чистой» с помощью необычного синтаксиса =0. Например:

class Base 
{
public:
  void f1();              // не виртуальная
  virtual void f2();      // виртуальная, но не чисто
  virtual void f3() = 0;  // чисто виртуальная
};

Base b; // ошибка: чисто виртуальная f3 не переопределена

Здесь Base является абстрактным классом (поскольку он имеет чисто виртуальную функцию), поэтому объекты класса Base не могут быть созданы напрямую: Base (явно) предназначен для использования в качестве базового класса. Например:

class Derived : public Base 
{
  // нет f1: хорошо
  // нет f2: хорошо, мы наследуем Base::f2
  void f3();
};

Derived d;  // ok: Derived::f3 переопределяет Base::f3

Абстрактные классы чрезвычайно полезны для определения интерфейсов. На самом деле, класс без данных, в котором все функции являются чисто виртуальными функциями, часто называют интерфейсом.

Вы можете дать определение чисто виртуальной функции:

Base::f3() { /* ... */ }

Иногда это бывает полезно (чтобы предоставить некоторые простые общие детали реализации для производных классов), но Base::f3() всё же необходимо переопределить в каком-то производном классе. Если вы не переопределите чисто виртуальную функцию в производном классе, этот производный класс станет абстрактным:

class D2 : public Base 
{
  // нет f1: хорошо
  // нет f2: хорошо, мы наследуем Base::f2
  // нет f3: хорошо, но D2, следовательно, всё еще будет абстрактным
};

D2 d;   // ошибка: чисто виртуальная Base::f3 не переопределена

В чем разница между вызовом виртуальных и невиртуальных функций-членов?

Вызов невиртуальных функций-членов решается статически. То есть функция-член выбирается статически (во время компиляции) на основе типа указателя (или ссылки) на объект.

Вызов виртуальных функций-членов, напротив, решается динамически (во время выполнения). То есть функция-член выбирается динамически (во время выполнения) в зависимости от типа объекта, а не типа указателя/ссылки на этот объект. Это называется «динамическая привязка». Большинство компиляторов используют тот или иной вариант следующей техники: если объект имеет одну или несколько виртуальных функций, компилятор помещает скрытый указатель в объект, называемый «виртуальным указателем» или «vpointer». Этот виртуальный указатель указывает на глобальную таблицу, называемую «виртуальная таблица» или «vtable».

Компилятор создает виртуальную таблицу для каждого класса, который имеет хотя бы одну виртуальную функцию. Например, если класс Circle имеет виртуальные функции для draw(), move() и resize(), с классом Circle будет связана ровно одна виртуальная таблица, даже если бы существовало множество объектов Circle, и виртуальный указатель каждого из этих объектов Circle будет указывать на виртуальную таблицу Circle. Сама vtable содержит указатели на каждую из виртуальных функций в классе. Например, виртуальная таблица Circle будет содержать три указателя: указатель на Circle::draw(), указатель на Circle::move() и указатель на Circle::resize().

Во время вызова виртуальной функции система времени выполнения следует за виртуальным указателем объекта на vtable класса, затем следует за соответствующим слотом в виртуальной таблице к коду метода.

Накладные расходы на пространство, связанные с описанной выше техникой, являются условными: дополнительный указатель на объект (но только для объектов, которым необходимо выполнять динамическое связывание), плюс дополнительный указатель на метод (но только для виртуальных методов). Накладные затраты на время также довольно условны: по сравнению с обычным вызовом функции вызов виртуальной функции требует двух дополнительных выборок (одна для получения значения указателя vpointer, вторая для получения адреса метода). Ни одно из этих действий времени выполнения не происходит с невиртуальными функциями, поскольку компилятор решает вызов невиртуальных функций исключительно во время компиляции на основе типа указателя.

Примечание: приведенное выше обсуждение значительно упрощено, поскольку оно не учитывает дополнительные структурные вещи, такие как множественное наследование, виртуальное наследование, RTTI и т.д., а также не учитывает проблемы с пространством/скоростью, такие как сбои страниц, вызов функции через указатель на функцию и т.д.


Что происходит на аппаратном уровне, когда я вызываю виртуальную функцию? Сколько будет уровней косвенного обращения? Сколько будет накладных расходов?

Это подробное описание предыдущего ответа FAQ. Ответ полностью зависит от компилятора, поэтому ваш опыт может отличаться, но большинство компиляторов C++ используют схему, аналогичную представленной здесь.

Приведем пример. Предположим, что в классе Base есть 5 виртуальных функций: от virt0() до virt4().

// Ваш исходный код на C++

class Base 
{
public:
  virtual arbitrary_return_type virt0( /*...произвольные параметры...*/ );
  virtual arbitrary_return_type virt1( /*...произвольные параметры...*/ );
  virtual arbitrary_return_type virt2( /*...произвольные параметры...*/ );
  virtual arbitrary_return_type virt3( /*...произвольные параметры...*/ );
  virtual arbitrary_return_type virt4( /*...произвольные параметры...*/ );
  // ...
};

Шаг № 1: компилятор создает статическую таблицу, содержащую 5 указателей на функции, закапывая эту таблицу где-нибудь в статической памяти. Многие (не все) компиляторы определяют эту таблицу при компиляции файла .cpp, который определяет первую не встраиваемую виртуальную функцию Base. Мы называем эту таблицу vtable; представим, что ее техническое название – Base::__vtable. Если указатель на функцию помещается в одно машинное слово на целевой аппаратной платформе, Base::__ vtable в конечном итоге занимает 5 скрытых слов памяти. Не 5 на экземпляр класса, не 5 на функцию; а всего 5. Это может выглядеть примерно как следующий псевдокод:

// Псевдокод (не C++, не C) для статической таблицы, определенной в файле Base.cpp

// Представьте, что FunctionPtr - это универсальный указатель на универсальную функцию-член
// (Помните: это псевдокод, а не код C++)
FunctionPtr Base::__vtable[5] = {
  &Base::virt0, &Base::virt1, &Base::virt2, &Base::virt3, &Base::virt4
};

Шаг № 2: компилятор добавляет скрытый указатель (обычно также машинное слово) к каждому объекту класса Base. Он называется vpointer. Думайте об этом скрытом указателе как о скрытом элементе данных, как если бы компилятор переписал ваш класс примерно так:

// Ваш исходный код на C++

class Base {
public:
  // ...
  FunctionPtr* __vptr;  // обеспечивается компилятором, скрыто от программиста
  // ...
};

Шаг № 3: компилятор инициализирует this->__vptr в каждом конструкторе. Идея состоит в том, чтобы заставить vpointer каждого объекта указывать на vtable его класса, как если бы он добавлял следующую инструкцию в каждый список инициализации конструктора:

Base::Base( /*...произвольные параметры...*/ )
  : __vptr(&Base::__vtable[0])  // обеспечивается компилятором, скрыто от программиста
  // ...
{
  // ...
}

Теперь займемся производным классом. Предположим, ваш код на C++ определяет класс Der, который наследуется от класса Base. Компилятор повторяет шаги №1 и №3 (но не №2). На шаге № 1 компилятор создает скрытую vtable, сохраняя те же указатели на функции, что и в Base::__vtable, но заменяя те слоты, которые соответствуют переопределениям. Например, если Der переопределяет функции с virt0() по virt2() и наследует остальные как есть, виртуальная таблица Der может выглядеть примерно так (притворимся, что Der не добавляет никаких новых виртуальных функций):

// Псевдокод (не C++, не C) для статической таблицы, определенной в файле Der.cpp

// Представьте, что FunctionPtr - это универсальный указатель на универсальную функцию-член
// (Помните: это псевдокод, а не код C ++)
FunctionPtr Der::__vtable[5] = {
  &Der::virt0, &Der::virt1, &Der::virt2, &Base::virt3, &Base::virt4
                                          ↑↑↑↑          ↑↑↑↑ // наследуются "как есть"
};

На шаге № 3 компилятор добавляет аналогичное присваивание указателя в начало каждого конструктора Der. Идея состоит в том, чтобы изменить vpointer каждого объекта Der, чтобы он указывал на vtable своего класса. (Это не второй виртуальный указатель; это тот же виртуальный указатель, который был определен в базовом классе Base; помните, компилятор не повторяет шаг 2 в классе Der.)

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

// Ваш исходный код на C++

void mycode(Base* p)
{
  p->virt3();
}

Компилятор не знает, вызывает ли он Base::virt3() или Der::virt3() или, возможно, метод virt3() другого производного класса, который еще даже не существует. Он только точно знает, что вы вызываете virt3(), которая является функцией в слоте № 3 виртуальной таблицы. Он переписывает этот вызов примерно так:

// Псевдокод, который компилятор генерирует из вашего кода на C++

void mycode(Base* p)
{
  p->__vptr[3](p);
}

На типовом аппаратном обеспечении машинный код представляет собой две «загрузки» плюс сам вызов:

  1. Первая загрузка получает vpointer, сохраняя его в регистре, скажем, r1.
  2. Вторая загрузка получает слово, расположенное по адресу r1 + 3*4 (предполагаемые указатели на функции имеют длину 4 байта, поэтому r1 + 12 является указателем на функцию virt3() класса). Представьте, что он помещает это слово в регистр r2 (или r1, если на то пошло).
  3. Третья инструкция вызывает код по адресу, находящемуся в r2.

Выводы:

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

Предупреждение: я намеренно проигнорировал множественное наследование, виртуальное наследование и RTTI. В зависимости от компилятора это может немного усложнить задачу.

Предупреждение: всё в этом ответе FAQ зависит от компилятора. У вас действия при запуске могут отличаться.


Как функция-член в моем производном классе может вызывать ту же функцию из его базового класса?

Используйте Base::f();

Начнем с простого случая. Когда вы вызываете невиртуальную функцию, компилятор явно не использует механизм виртуальных функций. Вместо этого он вызывает функцию по имени, используя полное имя функции-члена. Например, следующий код C++…

void mycode(Fred* p)
{
  p->goBowling();  // Притворимся, что Fred::goBowling() не виртуальная функция
}

… может быть скомпилирован во что-то вроде этого C-подобного кода (параметр p становится объектом this внутри функции-члена):

void mycode(Fred* p)
{
  __Fred__goBowling(p);  // просто псевдокод; ненастоящий
}

Фактическая схема изменения имен более сложна, чем простая, подразумеваемая выше, но вы поняли идею. Дело в том, что в этом конкретном случае нет ничего странного – он преобразуется в обычную функцию, более или менее похожую на printf().

Теперь для случая, рассматриваемого в вопросе выше: когда вы вызываете виртуальную функцию, используя ее полное имя (имя класса, за которым следует «::»), компилятор не использует механизм виртуального вызова, а вместо этого использует тот же механизм, как если бы вы вызывали невиртуальную функцию. Другими словами, он вызывает функцию по имени, а не по номеру слота. Поэтому, если вы хотите, чтобы код в производном классе Der вызывал Base::f(), то есть версию f(), определенную в его базовом классе Base, вы должны написать:

void Der::f()
{
  Base::f();  // или, если хотите, this->Base::f();
}

Компилятор превратит это во что-то отдаленно похожее на следующее (опять же, используя чрезмерно упрощенную схему изменения имен):

void __Der__f(Der* this)  // просто псевдокод; ненастоящий
{
  __Base__f(this);        // просто псевдокод; ненастоящий
}

У меня есть разнородный список объектов, и мой код должен делать с объектами специфичные для классов вещи. Похоже, здесь нужно использовать динамическое связывание, но я не могу понять как. Что я должен делать?

На удивление это легко.

Предположим, что существует базовый класс Vehicle (транспортное средство) с производными классами Car (легковой автомобиль) и Truck (грузовик). Код просматривает список объектов Vehicle и делает разные вещи в зависимости от типа Vehicle. Например, он может взвешивать объекты Truck (чтобы убедиться, что они не несут слишком тяжелый груз), но он может делать что-то другое с объектом Car – например, проверять регистрацию.

Первоначальное решение для этого, по крайней мере, для большинства людей, – использовать оператор if. Например, «если объект является Truck, сделайте это, иначе, если это Car, сделайте то, иначе сделайте третье действие»:

typedef std::vector<Vehicle*>  VehicleList;

void myCode(VehicleList& v)
{
  for (VehicleList::iterator p = v.begin(); p != v.end(); ++p) 
  {
    Vehicle& v = **p;  // просто для краткости
    // универсальный код, который работает для любого транспортного средства...
    // ...

    // выполнить операцию "foo-bar".
    // примечание: подробности операции "foo-bar" зависят от того,
    // с чем мы работаем, с легковым автомобилем или с грузовиком.
    if (v is a Car) 
    {
      // код, специфичный для легкового автомобиля, который выполняет "foo-bar"
      // с легковым автомобилем v
      // ...
    } 
    else if (v is a Truck) 
    {
      // код, специфичный для грузовика, который выполняет "foo-bar"
      // с грузовиком v
      // ...
    } 
    else 
    {
      // полууниверсальный код, который выполняет "foo-bar" с чем-то еще
      // ...
    }

    // универсальный код, который работает для любого транспортного средства...
    // ...
  }
}

Проблема заключается в том, что я называю «болезнью else-if-heimer». Приведенный выше код дает вам «болезнь else-if-heimer», потому что в конечном итоге вы забудете добавить else if при добавлении нового производного класса, и у вас, вероятно, будет ошибка, которая не будет обнаружена до времени выполнения, или хуже, когда продукт уже будет находиться в работе.

Решение состоит в том, чтобы использовать динамическое связывание, а не динамическую типизацию. Вместо того чтобы иметь (как я называю) метафору «мертвые данные живого кода» (где код жив, а объекты автомобиля/грузовика относительно мертвы), мы перемещаем код в данные. Это небольшая вариация Закона инверсии Бертрана Мейера.

Идея проста: используйте описание кода в блоках {...} каждого if (в данном случае это «операция foo-bar»; очевидно, ваше имя будет другим). Просто возьмите это описательное имя и используйте его как имя новой виртуальной функции-члена в базовом классе (в этом случае мы добавим функцию-член fooBar() к классу Vehicle).

class Vehicle 
{
public:
  // выполняет операцию "foo-bar"
  virtual void fooBar() = 0;
};

Затем вы удаляете весь блок if ... else if ... и заменяете его простым вызовом этой виртуальной функции:

typedef std::vector<Vehicle*>  VehicleList;

void myCode(VehicleList& v)
{
  for (VehicleList::iterator p = v.begin(); p != v.end(); ++p) 
  {
    Vehicle& v = **p;  // просто для краткости
    // универсальный код, который работает для любого транспортного средства...
    // ...

    // выполняет операцию "foo-bar"
    v.fooBar();

    // универсальный код, который работает для любого транспортного средства...
    // ...
  }
}

Наконец, вы перемещаете код, который раньше находился в блоке {...} каждого if, в функцию-член fooBar() соответствующего производного класса:

class Car : public Vehicle 
{
public:
  virtual void fooBar();
};

void Car::fooBar()
{
  // код, специфичный для легкового автомобиля, который выполняет "foo-bar" с 'this'
  // это код, который был в {...} выражения if (v is a Car)
}

class Truck : public Vehicle 
{
public:
  virtual void fooBar();
};

void Truck::fooBar()
{
  // код, специфичный для грузовика, который выполняет "foo-bar" с 'this'
  // это код, который был в {...} выражения if (v is a Truck)
}

Если у вас действительно есть блок else в исходной функции myCode() (смотрите выше «полууниверсальный код, который выполняет "foo-bar" с чем-то еще, кроме Car или Truck»), измените fooBar() класса Vehicle с чисто виртуальной в просто виртуальную и переместите этот код в эту функцию-член:

class Vehicle 
{
public:
  // выполняет операцию "foo-bar"
  virtual void fooBar();
};

void Vehicle::fooBar()
{
  // полууниверсальный код, который выполняет "foo-bar" с чем-то еще
  // этот код находился в блоке {...} выражения else
  // вы можете думать о нем, как о коде "по умолчанию"...
}

Вот и всё!

Дело, конечно, в том, что мы стараемся избегать логики принятия решений на основе типа производного класса, с которым вы имеете дело. Другими словами, вы пытаетесь избежать, «если объект является легковым автомобилем, выполнить xyz, иначе, если является грузовиком, выполнить pqr и т.д.», потому что это приводит к болезни else-if-heimer.


Когда деструктор должен быть виртуальным?

Когда кто-то будет удалять объект производного класса с помощью указателя базового класса.

В частности, вот когда вам нужно сделать деструктор виртуальным:

  • если кто-то будет наследовать ваш класса,
  • и если кто-то скажет new Derived, где Derived является производным от вашего класса,
  • и если кто-то скажет delete p, где фактический тип объекта – Derived, а тип указателя p – ваш класс.

Запутались? Вот упрощенное практическое правило, которое обычно защищает вас и обычно ничего вам не стоит: сделайте свой деструктор виртуальным, если в вашем классе есть какие-либо виртуальные функции. Обоснование:

  • это обычно защищает вас, потому что у большинства базовых классов есть хотя бы одна виртуальная функция;
  • обычно это вам ничего не стоит, потому что для второй или последующих виртуальных функций в вашем классе нет дополнительных затрат на пространство для каждого объекта. Другими словами, вы уже оплатили все затраты на пространство для каждого объекта, которые вы когда-либо заплатите после добавления первой виртуальной функции, поэтому виртуальный деструктор не добавляет никаких дополнительных затрат на пространство для каждого объекта. (Всё в этом пункте теоретически зависит от компилятора, но на практике это будет справедливо почти для всех компиляторов.)

Примечание: в производном классе, если ваш базовый класс имеет виртуальный деструктор, ваш собственный деструктор автоматически становится виртуальным. Вам может потребоваться явно определенный деструктор по другим причинам, но нет необходимости повторно объявлять деструктор просто для того, чтобы убедиться, что он виртуальный. Независимо от того, объявляете ли вы его с ключевым словом virtual, объявляете ли вы без ключевого слова virtual или вообще не объявляете, он всё равно остается виртуальным.

Кстати, если вам интересно, вот подробности того, зачем вам нужен виртуальный деструктор, когда кто-то говорит delete, используя указатель базового класса Base, указывающий на производный объект класса Derived. Когда вы говорите delete p, а класс p имеет виртуальный деструктор, вызывается деструктор, связанный с типом объекта *p, не обязательно связанный с типом указателя. Это хорошая вещь. Фактически, нарушение этого правила делает вашу программу неопределенной.


Почему деструкторы не виртуальные по умолчанию?

Потому что многие классы не предназначены для использования в качестве базовых классов. Виртуальные функции имеют смысл только в классах, предназначенных для работы в качестве интерфейсов для объектов производных классов (обычно выделяемых в куче и доступных через указатели или ссылки).

Итак, когда я должен объявить деструктор виртуальным? Каждый раз, когда в классе есть хотя бы одна виртуальная функция. Наличие виртуальных функций указывает на то, что класс предназначен для работы в качестве интерфейса для производных классов, и когда это так, объект производного класса может быть уничтожен с помощью указателя на базовый класс. Например:

class Base 
{
  // ...
  virtual ~Base();
};

class Derived : public Base 
{
  // ...
  ~Derived();
};

void f()
{
  Base* p = new Derived;
  delete p;   // виртуальный деструктор используется, чтобы убедиться, что вызывается ~Derived
}

Если бы деструктор Base не был виртуальным, деструктор Derived не был бы вызван – вероятно, с плохими последствиями, такими как неиспользование ресурсов, принадлежащих Derived.


Что такое «виртуальный конструктор»?

Идиома, позволяющая делать то, что C++ напрямую не поддерживает.

Вы можете получить эффект виртуального конструктора с помощью функции-члена virtual clone() (для создания копии) или функции-члена virtual create() (для конструктора по умолчанию).

class Shape 
{
public:
  virtual ~Shape() { }                 // виртуальный деструктор
  virtual void draw() = 0;             // чисто виртуальная функция
  virtual void move() = 0;
  // ...
  virtual Shape* clone()  const = 0;   // использует конструктор копирования
  virtual Shape* create() const = 0;   // использует конструктор по умолчанию
};

class Circle : public Shape 
{
public:
  Circle* clone()  const;   // ковариантные возвращаемые типы; смотрите ниже
  Circle* create() const;   // ковариантные возвращаемые типы; смотрите ниже
  // ...
};

Circle* Circle::clone()  const { return new Circle(*this); }
Circle* Circle::create() const { return new Circle();      }

В функции-члене clone() код new Circle(*this) вызывает конструктор копирования Circle, чтобы скопировать состояние this во вновь созданный объект Circle. (Примечание: если известно, что Circle не является final (он же leaf, «лист на дереве наследования»), вы можете уменьшить вероятность нарезки, сделав его конструктор копирования защищенным.) В функции-члене create() код new Circle() вызывает конструктор Circle по умолчанию.

Пользователи используют их, как если бы они были «виртуальными конструкторами»:

void userCode(Shape& s)
{
  Shape* s2 = s.clone();
  Shape* s3 = s.create();
  // ...
  delete s2;    // здесь вам нужен виртуальный деструктор
  delete s3;
}

Эта функция будет работать правильно независимо от того, является ли Shape (фигура) Circle (кругом), Square (квадратом) или какой-либо другой фигурой, которой еще даже не существует.

Примечание. Тип возвращаемого значения функции-члена clone() класса Circle намеренно отличается от типа возвращаемого значения функции-члена clone() класса Shape. Это называется ковариантными возвращаемыми типами, функция, которая изначально не была частью языка. Если ваш компилятор жалуется на объявление Circle* clone() const в классе Circle (например, говоря: «Тип возвращаемого значения отличается» или «Тип функции-члена отличается от виртуальной функции базового класса только по типу возвращаемого значения»), у вас старый компилятор, и вам придется изменить возвращаемый тип на Shape*.


Почему нет виртуальных конструкторов?

Виртуальный вызов – это механизм, позволяющий выполнить работу с учетом частично имеющейся информации. В частности, virtual позволяет нам вызывать функцию, зная только интерфейсы, а не точный тип объекта. Для создания объекта нужна полная информация. В частности, вам нужно точно знать, что вы хотите создать. Следовательно, «вызов конструктора» не может быть виртуальным.

Приемы использования косвенного обращения, когда вы просите создать объект, часто называют «виртуальными конструкторами». Например, смотрите TC++PL3 15.6.2.

Например, вот метод создания объекта соответствующего типа с использованием абстрактного класса:

struct F // интерфейс к функциям создания объектов
{  
  virtual A* make_an_A() const = 0;
  virtual B* make_a_B() const = 0;
};

void user(const F& fac)
{
  A* p = fac.make_an_A(); // создает A соответствующего типа
  B* q = fac.make_a_B();  // создает B соответствующего типа
  // ...
}

struct FX : F 
{
  A* make_an_A() const { return new AX(); } // AX наследуется от A
  B* make_a_B() const { return new BX();  } // BX наследуется от B
};

struct FY : F 
{
  A* make_an_A() const { return new AY(); } // AY наследуется от A
  B* make_a_B() const { return new BY();  } // BY наследуется от B
};

int main()
{
  FX x;
  FY y;
  user(x);    // этот пользователь создает объекты AX и BX
  user(y);    // этот пользователь создает объекты AY и BY

  user(FX()); // этот пользователь создает объекты AX и BX
  user(FY()); // этот пользователь создает объекты AY и BY
  // ...
}

Это вариант того, что часто называют «паттерном фабрика». Дело в том, что user() полностью изолирован от знаний о таких классах, как AX и AY.

Теги

C++ / CppFAQvpointervtableВиртуальная функцияВысокоуровневые языки программированияДеструктор / Destructor / dtor (программирование)Идиома виртуального конструктора / Virtual Constructor IdiomКонструктор / Constructor / ctor (программирование)НаследованиеПолиморфизмЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.