Метод объявленный в базовом классе как виртуальный в дальнейшем во всех классах наследниках

Обновлено: 28.06.2024

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Компилятор создает виртуальную таблицу для каждого класса, который имеет хотя бы одну виртуальную функцию. Например, если класс 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() .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Сегодня пришла очередь виртуальных функций. И, во-первых, я сразу оговорюсь, что статья моя (в принципе, как и предыдущая) ни в коей мере не претендует на полноту изложения. А во-вторых, как и раньше, эта статья не для профессионалов. Она будет полезна тем, кто уже нормально разбирается в основах C++, но имеет недостаточно опыта, либо же тем, кто не любит читать книжек.

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

Виртуальные функции и ключевое слово virtual

К моему удивлению, я очень часто сталкивался и сталкиваюсь с людьми (да что там говорить, я и сам был таким же), которые считают, что ключевое слово virtual делает функцию виртуальной только на один уровень иерархии. Объясню, что имеется в виду, на примере:

Итак, имеем простую иерархию классов. В каждом классе определены 3 метода: , и . Рассмотрим неверную логику людей, которые находятся под действием мифа:
когда указатель pA указывает на объект типа B имеем вывод:

pA is B:
B::foo() // потому что в родительском классе A метод foo() помечен как virtual
B::bar() // потому что в родительском классе A метод bar() помечен как virtual
A::baz() // потому что в родительском классе A метод baz() не помечен как virtual

pA is C:
С::foo() // потому что в родительском классе B метод foo() помечен как virtual
B::bar() // потому что в родительском классе B метод bar() не помечен как virtual,
// но он помечен как virtual в классе A, указатель на который мы используем
A::baz() // потому что в классе A метод baz() не помечен как virtual

С невиртуальной функцией baz() всё и так ясно. А вот с логикой вызова виртуальных функций есть неувязочка. Думаю, не стоит говорить, что на самом деле вывод будет следующим:

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

Раннее и позднее связывание. Таблица виртуальных функций

Связывание — это сопоставление вызова функции с вызовом. В C++ все функции по умолчанию имеют раннее связывание, то есть компилятор и компоновщик решают, какая именно функция должна быть вызвана, до запуска программы. Виртуальные функции имеют позднее связывание, то есть при вызове функции нужное тело выбирается на этапе выполнения программы.

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

Вывод может отличаться в зависимости от платформы, но в моем случае (Win32, msvc2008) он был следующим:

Что представляет собой таблица виртуальных функций и для чего она нужна? Таблица виртуальных функций хранит в себе адреса всех виртуальных методов класса (по сути, это массив указателей), а также всех виртуальных методов базовых классов этого класса.

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

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

Думаю, на примере все станет понятнее. Рассмотрим следующую иерархию:


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

Base
0
Base::foo()
1 Base::bar()
2 Base::baz()

и
Inherited
0
Base::foo()
1 Inherited::bar()
2 Base::baz()
3 Inherited::qux()

Как видим, в таблице производного класса адрес второго метода был заменен на соответствующий переопределенный. Пруфкод:

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

Размышления и впечатления, которые не хочется держать в себе. О программировании в частности. Ну и о творчестве, и о жизни вообще.

пятница, 27 февраля 2015 г.

[prog.c++] Про наследование, виртуальные методы вообще и деструкторы в частности

Решил написать этот пост по следам двух недавних LOR-овских дискуссий. В первой поучаствовал сам, но там авторитетные анонимные эксперты дали разгромную оценку моим знаниями и способностям, на чем тема и затихла :) Вторая развивается прямо сейчас и по накалу идиотии может со временем стать достойным образчиком LOR-овских срачей, бессмысленных и беспощадных :)

Тем не менее, тема наследования, виртуальных методов и, особенно, виртуальных деструкторов (спасибо RSDN-у за этот мем), которая была там затронута, как я вижу, для многих является terra incognita, поэтому имеет смысл уделить ей некоторое внимание.

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

Так же бытует мнение, что если в классе нет виртуальных методов, то наследование от этого класса бессмысленно.

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

Прежде всего нужно вспомнить, что C++ -- это не объектно-ориентированный язык. Это мультипарадигменный язык, в котором ООП является всего лишь одной из возможностей. Может быть ключевой, но не это главное. Важнее то, что ООП в C++ несколько отличается от ООП в Eiffel, очень сильно отличается от ООП в Java. Не говоря уже о SmallTalk/Ruby или, прости Господи, Self или JavaScript :)

Это сказано потому, что нельзя в C++ мыслить категориями, привычными для других языков программирования. Например, в Java есть какая сущность, как интерфейс. В C++ нет. При этом интерфейсы на C++ вполне себе можно описывать, но это будет лишь частным случаем класса, а не отдельной сущностью, как в Java. Поэтому, естественные для C++ вещи нужно и воспринимать это естественными и нормальными. А не с позиций "чистого ООП", "классического ООП в стиле SmallTalk" или "ООП из Java".

Так вот, если отталкиваться от нормальных для C++ вещей, то наследование в C++ можно условно разделить на наследование интерфейса и на наследование реализации.

Когда мы говорим о наследовании интерфейса, то мы говорим об одном из столбов ООП -- о полиморфизме. Т.е. базовый класс определяет набор методов, которые наследники обязаны реализовывать тем или иным способом.

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

Это не что иное, как интерфейс в терминологии Java. В приложении должны быть экземпляры классов-наследников desktop-а, которые реализуют определенные в desktop методы. Например, в десктопном приложении для Windows-платформы это может быть класс windows_desktop, который будет в методах width() и height() возвращать актуальные размеры десктопа на главном мониторе. В приложении для Android-а это может быть класс android_multipage_desktop, который в методе width() возвратит суммарную ширину всех видимых пользователю страничек десктопа (или как оно там называется). А для Web-приложения, где высота десктопа определяется длиной генерируемой Web-страницы, может потребоваться какой-нибудь web_page_desktop.

Так вот, когда речь идет о наследовании интерфейса, то без виртуальных методов, обычно, не обойтись. Но нужно ли при наследовании интерфейса иметь виртуальный деструктор?

Вообще говоря, нет :)

Виртуальный деструктор нужен для того, чтобы иметь возможность удалять объекты производных классов по указателю на базовый класс. Т.е. для ситуаций, когда пользователю отдают указатель на кем-то созданный экземпляр desktop и говорят, что пользователь должен удалить этот объект сам. Пользователь не знает, что это за объект, он знает только, что объект принадлежит к классу-наследнику desktop-а и все. Соответственно, пользователь будет удалять объект по указателю на desktop. А run-time должен понять, что там скрывается какой-нибудь web_page_desktop, занимающий в памяти столько-то места, имеющий такие-то атрибуты, нуждающиеся в вызове собственных деструкторов, и т.д.

Т.е. виртуальный деструктор нужен только в случае, когда производится удаление через базу. Да, такое происходит очень частно. Но не всегда.

Взять тот же desktop. Это пример такой сущности, которая вряд ли будет создаваться во множественном числе и вряд ли будет отдаваться под контроль пользователя. Скорее всего desktop-ы будут создаваться и уничтожаться где-то в дебрях GUI-библиотеки и пользователь об этом не будет даже знать. Если самой GUI-библиотеке потребуется размещать desktop-объекты в динамической памяти и удалять их через указатель на базу, то тогда у desktop-а будет виртуальный деструктор. Если не нужно -- не будет.

Так что интерфейс (базовый класс) с виртуальными методами, но без виртуального деструктора, вполне себе имеет и смысл, и право на существование.

Откуда же берется правило делать деструкторы виртуальными, если у класса появляется виртуальный метод?

От того, что лучше перебдеть, чем перебз. :) Сделать деструктор виртуальным сразу проще, чем потом объяснять пользователям, что класс не предназначался для того, чтобы через указатель на него кто-то удалял экземпляры наследников.

Хотя, если к нам в руки попадает нормальный C++ код и мы видим, что в базовом классе деструктор объявлен не виртуальным, то для нас это уже индикатор того, что подобное использование базового класса авторами не предусмотрено.

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

Тут нужно вспомнить о еще одном виде наследования, которое доступно нам в C++. Это наследование реализации. Т.е. базовый класс содержит в себе базовую функциональность, которая через наследование становится доступной всем наследникам. В ряде случаев наследование реализации может быть заменено агрегацией, но тут уже нужно рассматривать каждую ситуацию отдельно. На LOR-е я приводил следующий пример:

class locker synchronized_container * what_;
public :
locker( synchronized_container * what ) : what_(what) what_->lock();
>
~locker() what_->unlock();
>
>;

private :
std::mutex l_;
>;

template class T >
class thread_safe_list : private synchronized_container public :
void push_back(T&& o) locker l< this >;
.
>
void pop_front() locker l< this >;
.
>
.
>;

class my_ultra_fast_thread_safe_list : public thread_safe_list .
private :
virtual void lock() override < spin_.lock(); >
virtual void unlock() override

Здесь база synchronized_container служит поставщиком общей функциональности для группы классов-контейнеров, которые не имеют общего формального интерфейса. Например, synchronized_container может использоваться в качестве базы для реализации vector, deque, list, map, hash_table и т.д.

Нужен ли здесь synchronized_container-у виртуальный деструктор? Нет, не нужен. Т.к. если пользователь классов-наследников (вроде thread_safe_list или thread_safe_map) захочет почему-то оперировать ими через указатели на synchronized_container, то он собирается делает что-то, что разработчики класса synchronized_container не предполагали. И что, скорее всего, делать не следует.

Ну а теперь более интересный вопрос. А есть ли смысл в наследовании без виртуальных методов вообще?

И таки да :) Можно придумать и такие случаи.

Например, представим себе, что нам нужен класс, который будет реализовывать арену памяти (пул памяти), из которой можно брать блоки, а потом выбрасывать все содержимое арены сразу.

Вполне можно реализовать основные методы класса-арены без использования виртуальных методов. Может быть что-то вроде:

size_t free_bytes() const
return static_cast size_t >(end_ - current_);
>

size_t used_bytes() const
return static_cast size_t >(current_ - begin_);
>

На что указывают атрибуты memory_arena::begin_, end_ и current_?

А вот об этом должен позаботится производный класс ;)

Раз уж нам потребовалась собственная арена, а не штатный аллокатор динамической памяти, значит эффективность распределения памяти очень важна. И мы можем создать наследников, которые будут размещать арены в автоматической или статической памяти. Да еще и сделать так, чтобы наследник выделял столько памяти, сколько нам покажется достаточным. Так, если нам нужна арена для десятка мелких объектов по 20 байт каждый, то нам нужен блок на 200 байт. А если пару тысяч объектов по килобайту каждый, то несколько мегабайт. Что запросто делается, например, посредством шаблона:

Где метод setup_arena -- это protected-метод из memory_arena, который предназначен для вот такого использования производными классами.

В тех местах прикладного кода, где требуется абстрагирование от деталей реального размещения арены памяти, будет использоваться memory_arena. А там, где эти детали важны -- конкретные классы наследники:

void handle_request_two( const request_two & r, memory_arena & arena)
.
>

Однако, фокус в том, что последний пример с memory_arena можно так же использовать в качестве демонстрации того, что виртуальный деструктор может потребоваться и при наследовании, когда нет виртуальных методов, кроме самого виртуального деструктора! :)

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

В этом случае в memory_arena никаких прикладных виртуальных методов не будет. А вот виртуальный деструктор потребуется.

Вот как-то так оно, в C++ :)

Хотя на самом деле это еще не все :) Т.к. в C++ есть множественное наследование, то некоторые классы могут специально разрабатываться для того, чтобы быть подмешанными куда-то еще посредством наследования (т.н. mixin-классы). Поскольку эти mixin-классы могут использоваться и для наследования интерфейса, и для наследования реализации, и даже для наследования и того и другого сразу, то ситуация с виртуальностью деструкторов mixin-классов становится вообще веселой :) При том, что отдельного понятия mixin-а в C++ нет (в отличии, скажем, от trait-ов в Scala), и mixin-класс -- это обычный C++ный класс или интерфейс (которых в C++ так же нет, в отличии от Java).

Так что выбирай, но осторожно. Но выбирай. Но осторожно :)

PS. Комментаторы, которые захотят мне объяснить, что приведенные выше примеры -- это говнокод, что так никто не пишет, что я не разбираюсь в ООП, в С++ и в программировании вообще, пусть сразу идут на LOR. Какой я разработчик, я и сам прекрасно знаю. Тем более, что уже года три, как не считаю себя программистом, хотя код пишу время от времени. Но главное не это. А то, что многие, к сожалению, пишут гораздо хуже :(

PPS. В качестве небольшого "бонуса" для дочитавших до этого места. Маленький этюд на тему memory_arena, который показывает, как посредством множественного наследования сделать так, чтобы у memory_arena не нужно было вручную вызывать setup_arena. Как результат, объект memory_arena сразу конструируется в корректном состоянии:

protected :
memory_arena( char * begin, char * end )
: begin_(begin), end_(end), current_(begin)
<>

public :
void * allocate( size_t bytes )
auto n = current_ + bytes;
if ( n > end_ )
throw bad_alloc();
auto r = current_;
current_ = n;

size_t free_bytes() const
return static_cast size_t >(end_ - current_);
>

size_t used_bytes() const
return static_cast size_t >(current_ - begin_);
>

private :
char * begin_;
char * end_;
char * current_;
>;

template size_t capacity >
class buffer_holder
char memory_[ capacity ];

Наследование и полиморфизм C ++ три: виртуальные функции, проблемы статического и динамического связывания, виртуальные деструкторы

Каталог статей

1. Виртуальная функция и статическая привязка, динамическая привязка

Виртуальная функция: Функция-член, объявленная как виртуальная в базовом классе и переопределенная в одном или нескольких производных классах.
нота:
1. Виртуальная функция определяется в классе, затем на этапе компиляции компилятору необходимо сгенерировать уникальную таблицу виртуальных функций vftable для этого типа класса. Основное содержимое, хранящееся в таблице виртуальных функций, - это указатель RTTI и адрес виртуальной функции. Когда программа запущена, каждая таблица виртуальных функций будет загружена в область .rodata (область данных только для чтения) памяти.
2. Виртуальная функция определена в классе, тогда объект, определенный этим классом, будет хранить указатель виртуальной функции vfptr в начале памяти, когда он выполняется, указывая на соответствующий тип таблицы виртуальных функций vftable. Все точки vfptr n объектов, определенных типом, представляют собой одну и ту же таблицу виртуальных функций.
3. Количество виртуальных функций в классе не влияет на размер памяти объекта, но влияет на размер таблицы виртуальных функций.
4. Если метод в производном классе и метод, унаследованный от базового класса, имеют одинаковое возвращаемое значение, имя функции, список параметров, а метод базового класса является виртуальной функцией, то этот метод производного класса автоматически преобразуется в виртуальную функцию. , То есть отношения покрытия.

Статическая привязка (вызов функции): Вызов функции во время компиляции привязан к вызову обычной функции.
Динамическое связывание (вызов функции): Вызов функции во время выполнения привязан к вызову виртуальной функции.

покрытие: Методы базового и производного классов, возвращаемые значения, функциональные модули и списки параметров совпадают, а методы базового класса являются виртуальными функциями. Затем методы производных классов автоматически преобразуются в виртуальные функции, и связь между ними называется покрытием.

Случай 1: Статическая привязка: давайте сначала рассмотрим простой пример.


Взглянем на инструкцию по сборке:


Во время компиляции исходный код высокого уровня компилируется в код сборки, и указываются вызовы Base :: show (01612DAh) и вызов Base :: show (01612B2h), а именноВызов функции указывается во время компиляции, что является статической привязкой.
Результат печати:

Случай 2: Динамическое связывание: мы добавляем ключевое слово virtual к методу члена в базовом классе.

Только во время выполнения вы знаете адрес регистра, определяете, какой адрес вызывает какую функцию, это статическая привязка. pb-> show (int) то же самое, это динамическое связывание.

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

Тип пб: Base-> Есть ли виртуальная функция
Если у Base нет виртуальной функции, * pb распознает тип во время компиляции. * pb - Базовый тип;
Если у Base есть виртуальная функция, * pb идентифицирует тип во время выполнения: тип RTTI, то есть тип извлечения;

Мы также можем использовать команду VS для просмотра:
cl -d1reportSingleClassLayout (вывод информации о макете памяти объекта)


Итак, мы понимаем виртуальные функции, какие функции не могут быть реализованы как виртуальные функции?
1. Чтобы стать виртуальной функцией, адрес функции должен быть записан в таблице виртуальных функций, то есть виртуальная функция может сгенерировать адрес функции и сохранить его в vftable.
2. Указатель vfptr должен зависеть от объекта, и объект должен существовать. Адрес виртуальной функции может быть найден, только когда найдена таблица виртуальных функций.Таблица виртуальных функций хранится в указателе виртуальной функции vfptr, а указатель виртуальной функции vfpte сохраняется в памяти объекта.

Конструктор: (Вызов любой функции статически привязан)
1. Конструктор нельзя назвать виртуальной функцией
2. Вызов виртуальных функций в конструкторе не вызывает статической привязки.

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

Два, виртуальный деструктор

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

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

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

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

Разберем проблему: Тип pb - это базовый тип, поэтому delete вызывает сначала деструктор, чтобы найти Base :: ~ Base () в Base. Вызов деструктора является статическим связыванием, и нет возможности вызвать деструктор производного класса. , И наконец происходит утечка памяти.
решение: Определите деструктор базового класса как виртуальный деструктор, и деструктор производного класса автоматически станет виртуальной функцией. Тип pb - это базовый тип.При вызове деструктора Base :: ~ Base оказывается виртуальной функцией и происходит динамическое связывание. В таблице виртуальных функций производного класса: & Derive :: ~ derive используйте деструктор производного класса, чтобы разрушить его часть, а затем вызовите деструктор базового класса для анализа и создания базового класса.


Результат исполнения: успешное уничтожение.

Три углубленных проблемы динамического связывания

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

нота:
1. Вызов виртуальных функций с самим объектом является статической привязкой.
2. Динамическое связывание: виртуальная функция должна вызываться указателем или ссылкой до того, как произойдет динамическое связывание: указатель базового класса указывает на объект базового класса, а указатель базового класса указывает на объект производного класса, оба из которых являются динамическими привязками.
3. Если виртуальная функция не вызывается по указателю или ссылке, это статическая привязка.

Случай 1. Давайте посмотрим на пример: происходит статическая привязка


Переходим к разборке: обнаруживается статическая привязка

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

Случай 2: указатель базового класса указывает на объект базового класса


Перейти к разборке: для динамической привязки

Случай 3: указатель базового класса указывает на объект производного класса


Перейти к разборке: для динамической привязки

Случай 4: указатель базового класса указывает на объект базового класса, а указатель базового класса относится к объекту производного класса

То же, что и указатели: все они динамически связаны.

Случай 5: указатель производного класса вызывает объект производного класса, а ссылка производного класса вызывает объект производного класса

Переходим в разборку: все динамическое связывание.

Случай 6: Принудительное преобразование типа


Динамическое связывание: но последний вызов - это Base :: show ();

Перед изучением данной темы рекомендуется ознакомиться со следующими темами:

Содержание

Поиск на других ресурсах:

1. Переопределение виртуальных методов в обобщенных классах. Особенности. Синтаксис

Виртуальные методы в обобщенных классах могут быть переопределены. Технология переопределения методов в обобщениях работает так же как в необобщенных классах. Более подробно о переопределения методов в классах описывается здесь .

Метод, который объявляется в базовом классе как виртуальный, должен быть объявлен с модификатором virtual . Одноименные методы в унаследованных классах объявляются с модификатором override .

В свою очередь, унаследованные классы могут быть базовыми для других классов. В этом случае, цепь модификаторов override продолжается.

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

Если два обобщенные классы, которые получают параметром тип T , образуют иерархию, то общая форма объявления виртуального метода в базовом классе следующая

Чтобы обеспечить механизм полиморфизма, в производном классе одноименный метод объявляется с ключевым словом override .

2. Пример, демонстрирующий переопределение методов в обобщенных классах, образующих иерархию. Классы получают параметром тип T

Данный пример демонстрирует способ переопределения одноименного метода в производном классе в случае с двумя классами.

Объявляется базовый обобщенный класс Base , который получает параметром тип T и имеет следующие составляющие:

  • поле value обобщенного типа T , определяет данные класса;
  • конструктор с одним параметром;
  • виртуальные методы доступа GetValue() , SetValue() . Эти методы объявляются с модификатором virtual ;
  • виртуальный метод PrintValue() , который выводит значение внутреннего поля value .

Также объявляется обобщенный класс Derived , который унаследованный от класса Base . Класс содержит следующие поля и методы:

  • внутреннее поле value обобщенного типа T ;
  • конструктор, вызывает конструктор базового класса;
  • виртуальные методы GetValue() и SetValue() , которые переопределяют одноименные методы класса Base . Эти методы объявлены с ключевым словом override ;
  • виртуальный метод PrintValue() , который переопределяет одноименный метод базового класса. Метод объявляется с модификатором virtual .

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

3. Пример переопределения виртуальных методов для базового и производного классов, получающих параметрами два типа T1 , T2

В примере продемонстрировано:

  • использование механизма наследования для объявления обобщенных классов, образующих иерархию;
  • объявление базового и производного обобщенных классов, которые получают параметрами два типа T1 , T2 ;
  • вызов конструктора базового класса из унаследованного класса в случае обобщений;
  • переопределение виртуальных методов базового класса с целью обеспечения полиморфизма;
  • обращение к полям базового класса из унаследованного класса с помощью средства base .

Объявляются два обобщенные класса, которые получают параметрами типы T1 , T2 . Классы образуют иерархию. Один из классов базовый, другой — производный (унаследованный).

Обобщенный базовый класс A содержит следующие элементы:

  • внутренние поля a1 , a2 , имеющие соответственно типы T1 , T2 ;
  • конструктор с двумя параметрами, который инициализирует значениями поля a1 , a2 ;
  • виртуальные методы чтения полей a1 , a2 с именами GetA1() и GetA2() . Эти методы объявлены с модификатором virtual ;
  • методы записи новых значений в поля a1 , a2 с именами SetA1() , SetA2() . Эти методы объявлены с модификатором virtual .

Из класса A унаследован класс B , который не содержит внутренних полей, а содержит следующие методы:

Читайте также: