Виртуальная функция — Virtual function
В объектно-ориентированном программировании, в таких языках, как C ++ и Object Pascal, виртуальная функция или виртуальный метод является наследуемой и переопределяемой функцией или методом для которых динамическая отправка облегчается. Эта концепция является важной частью (времени выполнения) полиморфизма части объектно-ориентированного программирования (ООП). Короче говоря, виртуальная функция определяет целевую функцию, которая должна быть выполнена, но цель может быть неизвестна во время компиляции.
Большинство языков программирования, таких как Java, PHP и Python, по умолчанию обрабатывают все методы как виртуальные и не предоставляют модификатор для изменить это поведение. Однако некоторые из них предоставляют модификаторы, чтобы избежать переопределения методов производными классами (как последнее ключевое слово в Java и PHP ).
- 1 Цель
- 2 Пример
- 3 Абстрактные классы и чисто виртуальные функции
- 4 Поведение во время построения и разрушения
- 5 Виртуальные деструкторы
- 6 См. Также
- 7 Ссылки
Цель
Концепция виртуальной функции решает следующую проблему:
В объектно-ориентированном программировании, когда производный класс наследуется от базового класса, объект производного класса может на него можно ссылаться через указатель или ссылку типа базового класса вместо типа производного класса. Если есть методы базового класса, переопределенные производным классом, метод, фактически вызываемый такой ссылкой или указателем, может быть привязан либо «рано» (компилятором), в соответствии с объявленным типом указателя или ссылки, либо «поздно» (т. е. исполняющей системой языка) в соответствии с действительным типом объекта, на который имеется ссылка.
Виртуальные функции разрешаются «поздно». Если рассматриваемая функция является «виртуальной» в базовом классе, реализация функции наиболее производного класса вызывается в соответствии с фактическим типом указанного объекта, независимо от объявленного типа указателя или ссылки. Если это не «виртуальный», метод разрешается «раньше», а вызываемая функция выбирается в соответствии с объявленным типом указателя или ссылки.
Виртуальные функции позволяют программе вызывать методы, которые не обязательно даже существуют в момент компиляции кода.
В C ++ виртуальные методы объявляются путем добавления ключевого слова virtual к объявлению функции в базовом классе. Этот модификатор наследуется всеми реализациями этого метода в производных классах, что означает, что они могут продолжать переопределять друг друга и иметь позднее связывание. И даже если методы, принадлежащие базовому классу, вызывают виртуальный метод, они вместо этого будут вызывать производный метод. Перегрузка возникает, когда два или более методов в одном классе имеют одно и то же имя метода, но разные параметры. Переопределение означает наличие двух методов с одним и тем же именем и параметрами. Перегрузка также называется сопоставлением функций, а переопределение — отображением динамических функций.
Пример
Схема классов животного
Например, базовый класс Animal может иметь виртуальную функцию Eat . Подкласс Llama будет реализовывать Eat иначе, чем подкласс Wolf , но можно вызвать Eat для любого экземпляра класса, называемого Animal, и получить поведение Eat конкретного подкласса.
class Animal virtual void Eat(void) = 0; >; // The class "Animal" may possess a definition for Eat if desired. class Llama : public Animal < public: // The non virtual function Move is inherited but not overridden. void Eat(void) override < std::cout >;
Это позволяет программисту обрабатывать список объектов класса Animal , сообщая каждому в повернуться, чтобы поесть (позвонив Ешь ), без необходимости знать, какое животное может быть в списке, как каждое животное ест или каков полный набор возможных типов животных.
Мы можем лучше увидеть, как работают виртуальные функции, реализовав приведенный выше пример в C
#include / * объект указывает на свой класс. * / struct Animal ; / * который содержит виртуальную функцию Animal.Eat * / struct AnimalClass ; / * Поскольку Animal.Move не является виртуальной функцией, его нет в приведенной выше структуре. * / void Move (struct Animal * self) каким-то образом перемещен \ n", (void *) self); > / * в отличие от Move, который выполняет Animal.Move напрямую, Eat не может знать, какую функцию (если есть) вызывать во время компиляции. Animal.Eat можно разрешить только во время выполнения, когда вызывается Eat. * / void Eat (struct Animal * self) Eat) class->Eat (self); // выполняем Animal.Eat else fprintf (stderr, "Есть не реализовано \ n"); > / * реализация Llama.Eat, это целевая функция, которая будет вызываться void Eat (struct Animal *). * / static void _Llama_eat (struct Animal * self) Лама ест траву! \ n", (void *) self); > / * инициализируем класс * / const struct AnimalClass Animal = <(void *) 0>; // базовый класс не реализует Animal.Eat const struct AnimalClass Llama = ; // но производный класс выполняет int main (void) * инициализирует объекты как экземпляр своего класса * / struct Animal animal = ; struct Animal llama = ; Перемещение (животное); // Animal.Move Move (llama); // Llama.Move Eat (animal); // не удается разрешить Animal.Eat, поэтому выведите «Не реализовано» в stderr Eat (llama); // разрешает Llama.Eat и выполняет>
Абстрактные классы и чистые виртуальные функции
A чистая виртуальная функция или чистый виртуальный метод — это виртуальная функция, которая должна быть реализована производным class, если производный класс не abstract. Классы, содержащие чистые виртуальные методы, называются «абстрактными», и они не могут быть созданы напрямую. Подкласс абстрактного класса может быть создан только напрямую, если все унаследованные чистые виртуальные методы были реализованы этим классом или родительским классом. Чистые виртуальные методы обычно имеют объявление (подпись ) и не имеют определения (реализация ).
В качестве примера абстрактный базовый класс MathSymbol может предоставлять чистую виртуальную функцию doOperation () и производные классы Plus и Минус реализовать doOperation () для обеспечения конкретных реализаций. Реализация doOperation () не имела бы смысла в классе MathSymbol , поскольку MathSymbol является абстрактной концепцией, поведение которой определяется исключительно для каждого данного вида (подкласса) MathSymbol . Точно так же данный подкласс MathSymbol не был бы полным без реализации doOperation () .
Хотя чистые виртуальные методы обычно не имеют реализации в классе, который их объявляет, чистые виртуальные методы в C ++ разрешено содержать реализацию в своем объявляющем классе, обеспечивая откат или поведение по умолчанию, которому производный класс может делегировать, если необходимо.
Чистые виртуальные функции также могут использоваться там, где объявления методов используются для определения interface — аналогично тому, что явно указывает ключевое слово interface в Java. При таком использовании производные классы будут предоставлять все реализации. В таком шаблоне проектирования абстрактный класс, который служит интерфейсом, будет содержать только чистые виртуальные функции, но не элементы данных или обычные методы. В C ++ использование таких чисто абстрактных классов в качестве интерфейсов работает, поскольку C ++ поддерживает множественное наследование. Однако, поскольку многие языки ООП не поддерживают множественное наследование, они часто предоставляют отдельный механизм интерфейса. Примером может служить язык программирования Java.
Поведение во время создания и разрушения
Языки различаются по своему поведению, в то время как конструктор или деструктор объекта являются Бег. По этой причине обычно не рекомендуется вызывать виртуальные функции в конструкторах.
В C ++ вызывается «базовая» функция. В частности, вызывается наиболее производная функция, которая не более производная, чем класс текущего конструктора. Если эта функция является чистой функцией, возникает неопределенное поведение.
В Java и C # вызывается производная реализация, но некоторые поля еще не инициализированы производным конструктором (хотя они инициализируются нулевыми значениями по умолчанию). Некоторые шаблоны проектирования , такие как шаблон абстрактной фабрики, активно продвигают это использование в языках, поддерживающих эту возможность.
Виртуальные деструкторы
Объектно-ориентированные языки обычно автоматически управляют выделением и освобождением памяти при создании и уничтожении объектов. Однако некоторые объектно-ориентированные языки позволяют при желании реализовать собственный метод деструктора. Если в рассматриваемом языке используется автоматическое управление памятью, вызываемый настраиваемый деструктор (обычно называемый финализатором в этом контексте) обязательно будет подходящим для рассматриваемого объекта. Например, если создается объект типа Wolf, который наследует Animal, и оба имеют настраиваемые деструкторы, вызываемый будет объявлен в Wolf.
В контексте ручного управления памятью ситуация может быть более сложной, особенно в отношении статической диспетчеризации. Если объект типа Wolf создается, но на него указывает указатель Animal, и именно этот тип указателя Animal удаляется, вызываемый деструктор может фактически быть тем, который определен для Animal, а не для Wolf, если только деструктор не является виртуальным.. Это особенно верно в случае C ++, где поведение является частым источником ошибок программирования, если деструкторы не виртуальные.
См. Также
- Абстрактный метод
- Наследование
- Суперкласс
- Виртуальное наследование
- Интерфейс (объектно-ориентированное программирование)
- Компонентная объектная модель
- Таблица виртуальных методов
Указатели и виртуальные функции в Java
В настоящее время в Интернете можно найти множество статей как о перспективности платформы Java, так и об её ограниченности. Многих программистов, только присматривающихся к Яве, могут отпугнуть частые заявления, типа: «низкое быстродействие», «отсутствие указателей» и т.д.
На быстродействии я останавливаться не буду. По этой теме можно найти множество статей, например эту. В этой статье я попытаюсь развеять мнение, что в Java отсутствуют указатели. Да, они там действительно отсутствуют в том виде, в котором их можно увидеть в C/C++. Но не даром же все сложные типы в Java называются ссылочными. Можно рассмотреть пример на C. Эта программа меняет местами значения переменных x и y. Могут быть и другие ситуации, где результат должен возвращаться через параметры функции. Но это самый простейший, и, по-моему, самый доходчивый пример.
#include void change(int *a, int *b) int c=*a; *a=*b; *b=c; > int main() int x=2, y=3; printf("%d %d\n", x, y); change(&x, &y); printf("%d %d\n", x, y); return 0; >
Результат работы программы будет выглядеть следующим образом:
2 3 3 2
В Java с простыми типами нельзя сделать это же самое. Но кто нам мешает написать класс-оболочку для простого типа? В общем, рассмотрим аналогичный пример на Java.
Сначала опишем класс-оболочку для типа int:
public class IntPtr public int value;
public IntPtr ( int a ) value = a;
>
>
Затем, используем этот класс в программе:
public class Main public void change ( IntPtr x, IntPtr y ) int z = x.value;
x.value = y.value;
y.value = z;
>
public static void main ( String [] args ) Main m = new Main () ;
IntPtr a = new IntPtr ( 2 ) ;
IntPtr b = new IntPtr ( 3 ) ;
System.out.print ( a.value ) ;
System.out.print ( ‘ ‘ ) ;
System.out.println ( b.value ) ;
m.change ( a, b ) ;
System.out.print ( a.value ) ;
System.out.print ( ‘ ‘ ) ;
System.out.println ( b.value ) ;
>
>
Результат будет таким же:
2 3 3 2
Недостатком использования такого вида «указателей» может быть только «лишняя писанина», которой нет в С. Но при наличии таких сред, как NetBeans или Eclipse (и некоторых коммерческих), с их развитыми суфлёрами кода, я думаю, наличие «лишнего» кода не является серьёзной проблемой.
Далее можно отметить наличие в Яве только виртуальных функций. При этом не нужно писать перед функцией слово virtual. Да его и нет в языке. Рассмотрим пример:
package virtualfunctest;
//суперкласс
public class Parent protected void f1 () System.out.println ( «Parent class» ) ;
>
public void f2 () f1 () ;
>
>
package virtualfunctest;
// производный класс
public class Child extends Parent protected void f1 () System.out.println ( «Child class» ) ;
>
>
// использование функций
package virtualfunctest;
public class Main public static void main ( String [] args ) Parent obj;
obj= new Parent () ;
obj.f2 () ;
obj= new Child () ;
obj.f2 () ;
>
>
Таким образом, можно увидеть, что язык Java всё же имеет указатели (об этом, кстати, разработчики из Sun Microsystems неоднократно говорили). Главное – это подход к ним. Я не думаю, что в этой статье я показал что-то новое людям, уже работавшим с этим языком. Но думаю, что для новичков эта статься будет хоть чем-то полезна.
А вообще, всё, чего не хватает в языке Ява можно реализовать с помощью JNI. Хотя в данный момент его вполне можно назвать самодостаточным языком.
A может Вас также заинтересует что-нибудь из этого:
- Разное → Теория и практика Java: Динамическая компиляция и измерение производительности
- Java Standard Edition → Блокировки
- Java сниппеты → Блоки статической и объектной инициализации
- Java Standard Edition → Производительность операций ввода/вывода в Java
- Java сниппеты → Методы для работы с переменным количеством аргументов
- Java сниппеты → Использование readResolve
Что такое виртуальная функция и используются ли они в java
При вызове функции программа должна определять, с какой именно реализацией функции соотносить этот вызов, то есть связать вызов функции с самой функцией. В С++ есть два типа связывания — статическое и динамическое.
Когда вызовы функций фиксируются до выполнения программы на этапе компиляции, это называется статическим связыванием (static binding), либо ранним связыванием (early binding). При этом вызов функции через указатель определяется исключительно типом указателя, а не объектом, на который он указывает. Например:
#include class Person < public: Person(std::string name): name < >void print() const < std::cout private: std::string name; // имя >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company < >void print() const < Person::print(); std::cout private: std::string company; // компания >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob >
В данном случае класс Employee наследуется от класса Person, но оба этих класса определяют функцию print() , которая выводит данные об объекте. В функции main создаем два объекта и поочередно присваиваем их указателю на тип Person и вызываем через этот указатель функцию print. Однако даже если этому указателю присваивается адрес объекта Employee, то все равно вызывает реализация функции из класса Person:
Employee bob ; person = &bob; person->print(); // Name: Bob
То есть выбор реализации функции определяется не типом объекта, а типом указателя. Консольный вывод программы:
Name: Tom Name: Bob
Динамическое связывание и виртуальные функции
Другой тип связывания представляет динамическое связывание (dynamic binding), еще называют поздним связыванием (late binding), которое позволяет на этапе выполнения решать, функцию какого типа вызвать. Для этого в языке С++ применяют виртуальные функции . Для определения виртуальной функции в базовом классе функция определяется с ключевым словом virtual . Причем данное ключевое слово можно применить к функции, если она определена внутри класса. А производный класс может переопределить ее поведение.
Итак, сделаем функцию print в базовом классе Person виртуальной:
#include class Person < public: Person(std::string name): name < >virtual void print() const // виртуальная функция < std::cout private: std::string name; >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company < >void print() const < Person::print(); std::cout private: std::string company; >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob // Works in Microsoft >
Таким образом, базовый класс Person определяет виртуальную функцию print, а производный класс Employee переопределяет ее. В первом же примере, где функция print не была виртуальной, класс Employee не переопределял, а скрывал ее. Теперь при вызове функции print для объекта Employee через указатель Person* будет вызываться реализация функции именно класса Employee. Соответственно тепепрь мы получим другой консольный вывод:
Name: Tom Name: Bob Works in Microsoft
В этом и состоит отличие переопределения виртуальных функций от скрытия.
Класс, который определяет или наследует виртуальную функцию, еще назвается полиморфным (polymorphic class). То есть в данном случае Person и Employee являются полиморфными классами.
Стоит отметить, что вызов виртуальной функции через имя объекта всегда разрешается статически.
Employee bob ; Person p = bob; p.print(); // Name: Bob - статическое связывание
Динамическое связывание возможно только через указатель или ссылку.
Employee bob ; Person &p ; // присвоение ссылке p.print(); // динамическое связывание Person *ptr ; // присвоение адреса указателю ptr->print(); // динамическое связывание
При определении вирутальных функций есть ряд ограничений. Чтобы функция попадала под динамическое связывание, в производном классе она должна иметь тот же самый набор параметров и возвращаемый тип, что и в базовом классе. Например, если в базовом классе виртуальная функция определена как константная, то в производном классе она тоже должна быть константной. Если же функция имеет разный набор параметров или несоответствие по константности, то мы будем иметь дело со скрытием функций, а не переопределением. И тогда будет применяться статическое связывание.
Также статические функции не могут быть виртуальными.
Ключевое слово override
Чтобы явным образом указать, что мы хотим переопредлить функцию, а не скрыть ее, в производном классе после списка параметров функции указывается слово override
#include class Person < public: Person(std::string name): name < >virtual void print() const // виртуальная функция < std::cout private: std::string name; >; class Employee: public Person < public: Employee(std::string name, std::string company): Person, company < >void print() const override // явным образом указываем, что функция переопределена < Person::print(); std::cout private: std::string company; >; int main() < Person tom ; Person* person ; person->print(); // Name: Tom Employee bob ; person = &bob; person->print(); // Name: Bob // Works in Microsoft >
То есть здесь выражение
void print() const override
указывает, что мы явным образом хотим переопределить функцию print. Однако может возникнуть вопрос: в предыдущем примере мы не указывали override для вирутальной функции, но переопределение все равно работало, зачем же тогда нужен override ? Дело в том, что override явным образом указывает компилятору, что это переопределяемая функция. И если она не соответствует виртуальной функции в базовом классе по списку параметров, возвращаемому типу, константности, или в базовом классе вообще нет функции с таким именем, то компилятор при компиляции сгенерирует ошибку. И по ошибке мы увидим, что с нашей переопределенной функцией что-то не так. Если же override не указать, то компилятор будет считать, что речь идет о скрытии функции, и никаких ошибок не будет генерировать, компиляция пройдет успешно. Поэтмоу при переопределении виртуальной функции в производном классе лучше указывать слово override
При этом стоит отметить, что виртуальную функцию можно переопределить по всей иерархии наследования в том числе не в прямых производных классах.
Принцип выполнения виртуальных функций
Стоит отметить, что виртальные функции имеют свою цены — объекты классов с виртуальными функциями требуют немного больше памяти и немного больше времени для выполнения. Поскольку при создании объекта полиморфного класса (который имеет виртуальные функции) в объекте создается специальный указатель. Этот указатель используется для вызова любой виртуальной функции в объекте. Специальный указатель указывает на таблицу указателей функций, которая создается для класса. Эта таблица, называемая виртуальной таблицей или vtable, содержит по одной записи для каждой виртуальной функции в классе.
Когда функция вызывается через указатель на объект базового класса, происходит следующая последовательность событий
- Указатель на vtable в объекте используется для поиска адреса vtable для класса.
- Затем в таблице идет поиск указателя на вызываемую виртуальную функцию.
- Через найденный указатель функции в vtable вызывается сама функция. В итоге вызов виртуальной функции происходит немного медленнее, чем прямой вызов невиртуальной функции, поэтому каждое объявление и вызов виртуальной функции несет некоторые накладные расходы.
Запрет переопределения
С помощью спецификатора final мы можем запретить определение в производных классах функций, которые имеют то же самое имя, возвращаемый тип и список параметров, что и виртуальная функция в базовом классе. Например:
class Person < public: virtual void print() const final < >>; class Employee : public Person < public: void print() const override // Ошибка. < >>;
Также можно переопределить функцию базового класса, но запретить ее переопределение в дальнейших производных классах:
class Person < public: virtual void print() const // переопределение разрешено < >>; class Employee : public Person < public: void print() const override final // в классах, производных от Employee переопределение запрещено < >>;
Дисклеймер.
Год от года темы этого раздела тасуются как попало. Я попытался их разбить логически (так, чтобы всё, имеющее отношение к виртуальным функциям, не было разделено на части, например), так что не удивляйтесь, если в вашем курсе темы будут в другом порядке разложены.
Наследование и виртуальные функции
- Запись лекции №1
- Запись лекции №2
- Запись лекции №3
- Запись лекции №4
В курсе предполагается, что что-то о наследовании вы знаете и примерно представляете, что это такое. Оно было в курсе Java, и это первая причина, почему мы не будем обсуждать всё в мельчайших деталях. Вторая — потому что в книжках оно обсасывается очень подробно.
Про наследование сложно говорить в том же ключе, в котором мы говорили/будем говорить об исключениях, шаблонах и т.п. Исключения и шаблоны решают конкретные задачи, а для наследования таковой нет. Поэтому на эту тему мы будем смотреть иначе: есть механизм, а на кой он нужен?
Lore.
Немного введения. Откуда идёт наследование? Если мы решаем задачу о моделировании дорожной сети, то первый порыв души — сопоставить объектам предметной области объекты нашей программы. Таким образом у нас очень естественно получается полиморфизм — у нас есть произвольные транспортные средства, которые очень похожи, а есть автобусы, трамваи и подобное, то есть более специализированные штуки.
Это не столько способ организации программы, сколько образ мысли. Причём полезный: мы можем начать декомпозировать задачу, даже не зная её решения. И, кстати, совсем необязательно объекту реальности сопоставлять объект программы, это может быть неэффективно — если мы решаем задачу о минимизации чего-то (за минимальные деньги перестроить дорогу так что. ), то совершенно необязательно у вас будут такие же объекты.
А ещё бывает ситуация, когда вы придумываете движок регулярных выражений — никакие структуры из внешнего мира не приходят, вы делаете что-то, не имеющее отношения к реальности. И реальные программы где-то посередине: часть из реального мира, часть к нему не имеет отношения.
Итак, наследование.
struct vehicle < std::string registration_number; >; struct bus : vehicle < // Наследуется от `vehicle`. int32_t route_number; std::string next_stop() const; >; struct truck : vehicle < double cargo_mass; >; int main() < bus b; vehicle& v = b; // Можно делать так. v.registration_number = "123"; // Обращение к полям базового класса. >
bus содержит и route_number , и registration_number . Когда вы обращаетесь к какому-то полю или методу, компилятор сначала ищет его внутри вашего класса, а если не находит — идёт в базовый. Поэтому если у базового и наследуемого класса есть поле с одним именем, обращение к нему вернём поле последнего. Если наследоваться от двух классов (в C++ есть множественное наследование), и в двух базовых есть поле/метод с одинаковым названием, а в наследнике нет, то будет ошибка компиляции при обращении от объекта наследника.
Если поля классов совпадают в названии, то обратиться к полю другого можно так:
struct base < int xyz; // 1. >; struct derived : base < int xyz; // 2. >; int main() < derived d; d.xyz = 123; // Изменяется 2. ((base&)d).xyz = 123; // Изменяется 1. d.base::xyz = 123; // Специальный синтаксис для изменения 1. >
Как можно видеть из примера, класс можно приводить к любому его базовому по ссылке. Указатель на наследуемый класс можно приводить к указателю на базовый.
С методами в наследовании работают точно так же, как и с полями:
struct base < void g() <>// 1. >; struct derived : base < void g() <>// 2. >; int main() < derived d; d.g(); // Запуск 2. d.base::g(); // Запуск 1. >
При создании объектов класса-наследника вызывается конструктор базового класса по умолчанию (в котором, если тот тоже от кого-то наследуется, вызывается конструктор его базы). Если нужно вызывать другой конструктор, используйте списки инициализации:
struct bus : vehicle < bus(std::string const& registration_number, std::string const& next_stop_name) : vehicle(registration_number), next_stop_name(next_stop_name)<>>;
При этом стандарт определяет следующий порядок инициализации (перевод с cppreference):
- Инициализируются виртуальные базовые классы (про них будет потом) в порядке обхода в глубину. Внутри одного класса инициализация баз происходит в том порядке, в котором вы их написали (слева направо).
- Инициализируются прямые базовые классы тоже слева направо.
То есть если вы написали class C : B, A , то сначала инициализируется B , а потом — A . - Инициализируются нестатические члены класса в порядке их объявления.
Worst practices.
Во-первых, не надо наследоваться, если вам нужно только расширить класс. Если вы хотите добавить новый функционал в std::string , не нужно от него наследоваться, а то ваш коллега тоже захочет по-другому её расширить, и вы получите два новых типа, а потом не сможете вызывать функции друг друга. Лучше создайте обычную функцию. Обычные функции — это хорошо, не надо писать всё классами.
Во-вторых, не надо создавать отдельный класс под одну операцию:
struct string_printer < private: std::string msg; public: string_printer(const std::string& msg) : msg(msg) < >void print() < std::cout >;
Помимо того, что этот код и так выглядит как странное дизайнерское решение, у него есть вторая проблема — вот сделали вы string_printer(«Hello, world»).print() . А что если вы не сделаете print , или сделаете дважды? Потому что напечатать-то вы можете и в конструкторе, зачем вам метод? Не интуитивно.
Мораль: не заводите класс, если вам нужно сделать действие. Исключение — какие-нибудь компараторы, которые в общем случае могут быть полноценными классами, но вообще могут являться и обёрткой вокруг функции. И вот тут ничего не поделаешь, std::map принимает класс, а не функцию.
Slicing.
struct base < virtual std::string get_name() const < return "base"; >>; struct derived : base < std::string get_name() const override < return "derived"; >>; int main() < derived d; base& b = d; b.get_name(); // -->"derived" >
Тут всё хорошо. А теперь представим, что в main ‘е забыли поставить & :
int main() < derived d; base b = d; b.get_name(); // -->"base" . >
Что это всё значит?
Заметим, что эти два примера совсем разные: в первом создаётся ровно один объект и этот объект имеет тип derived . Во втором же случае у нас два разных объекта. Один типа derived , а другой типа base . Причём у второго объекта base — это и статический, и динамический тип. Так что логично, что выводится «base».
А почему base b = d; — не ошибка компиляции?
На самом деле внутри base компилятор сгенерировал конструктор копирования. И на самом деле у нас написано такое:
Так что тут у нас просто скопировались мемберы base ‘a, а мемберы derived ‘а потерялись. Это называется Object slicing. Как избежать непреднамеренного slicing‘а?
Если для класса копирование не имеет смысла, то это стоит явно запретить:
base(base const& other) = delete; base& operator=(base const& other) = delete;
Тогда случайного slicing‘а не случится.
Кстати, у derived ‘а удалять эти функции уже не обязательно, они не сгенерятся, если у base ‘а удалены.
Вообще, часто копирование базового типа действительно не имеет смысла: в примере с vehicle , car и bus не совсем понятно, что мы хотим сделать, копируя vehicle . Мы скопировали какой-то абстрактый vehicle , зачем нам это?
Виртуальные функции.
Виртуальные функции — главное (если не единственное), для чего вам нужно наследование.
Мы уже рассматривали код
struct vehicle < void print() < std::cout >; struct bus : vehicle < void print() < std::cout >; struct truck : vehicle <>; int main() < vehicle v; v.print(); // "vehicle". bus b; b.print(); // "bus". truck t; t.print(); // "vehicle". >
При этом если мы напишем функцию
void foo(vehicle& v)
То даже если передать в неё bus , она выведет «vehicle» . Думаю, никому не надо объяснять, почему. Потому введём пару определений, а после поясним, как поправить код.
Статический тип — это тип объекта, который в программе написан. И у всего, что приходит в функцию foo статический тип vehicle (вообще vehicle& , но не суть).
Динамический тип — то, чем ваш объект был создан и по сути является. Так, ваш vehicle может являться bus ‘ом или truck ‘ом.
Так вот, виртуальные функции позволяют выбирать метод исходя из динамического типа, а не статического:
struct vehicle < virtual void print() < std::cout >; struct bus : vehicle < void print() < std::cout >; struct truck : vehicle <>; int main() < vehicle v; foo(v); // "vehicle" bus b; foo(b); // "bus" truck t; foo(t); // "vehicle" >
Кстати, если у базового класса, функция виртуальная, то у производных тоже virtual .
Когда вы подобным образом подменяете виртуальную функцию, говорят, что bus::print override‘ит vehicle::print .
Виртуальный деструктор.
int main() < bus* b = new bus(); vehicle* v = b; v->print(); delete v; >
Тут вызывается v->~vehicle . А если bus имеет какой-то нетривиальный деструктор, он не вызовется. Поэтому тут вам всё так же надо вызывать деструктор в зависимости от динамического типа, а не статического.
struct vehicle < virtual ~vehicle() <>>;
С точки зрения языка, вы не имеете права делать delete у базового класса, есть создали наследуемый и не пометили деструктор базового как virtual . Если будете так делать — UB. Даже если все деструкторы тривиальные.
Виртуальные функции и параметры по умолчанию.
Параметры по умолчанию являются частью декларации, поэтому соответствуют статическому типу, даже если указать другие в наследнике:
#include #include struct vehicle < virtual void print_name(std::string prefix = "Base: ")< std::cout >; struct bus : vehicle < void print_name(std::string prefix = "Derived: ")< std::cout >; void foo(vehicle& t) < t.print_name(); >int main() < bus b; b.print_name(); // Derived: bus foo(b); // Base: bus >
Как полиморфизм устроен изнутри.
Как бы мы сделали полиморфизм руками, если бы у нас его не было? Ну, через указатели на функции. По-другому не получится, потому что мы не знаем список всех наших наследников:
struct base < base(); void (*foo)(base*); void (*bar)(base*, int); void (*baz)(base*, double); >; void foo_base(base* self) < // . >void bar_base(base* self, int x) < // . >void baz_base(base* self, double y) < // . >base::base() : foo(foo_base), bar(bar_base), baz(baz_base) <> struct derived : base < derived(); >; void foo_derived(base* self) < derived* derived_self = static_cast(self); // . > void bar_derived(base* self, int x) < derived* derived_self = static_cast(self); // . > void baz_derived(base* self, double y) < derived* derived_self = static_cast(self); // . > derived::derived() : foo(foo_derived), bar(bar_derived), baz(baz_derived) <> int main()
Так в целом можно, но можно и чуть оптимальнее. У нас наши тройки функций не могут комбинироваться как им вздумается, всегда либо все из base , либо все из derived . И к тому же, нам не хочется с каждой новой функцией увеличивать размер структуры. Поэтому есть такая штука как таблица виртуальных функций. Это мы берём наши 3 указателя и выносим их в особый объект, указатель на который помещается в нашу структуру. А такие структуры создаем под каждый класс глобальными переменными. Это даёт нам ещё один indirection, но сокращает размер структур. И именно так это и работает во всех компиляторах. Во множественном наследовании у класса просто появляются две таблицы, под каждый базовый класс своя.
Абстрактные методы (и классы):
Вот создали вы, скажем, устройство вывода. И отнаследовались из него, создав устройство, которое пишет в одно место, в другое место и т.д. Возникает вопрос: а что должно делать базовое устройство? Ну, непонятно. Ничего путного. Для этого есть механизм чисто виртуальных (абстрактных) функций — пометить, что этой функции не существует:
struct output_device < virtual void write(void const* data, size_t size) = 0; >; struct speakers : output_device <>; struct twitch_stream : output_device <>; struct null_output : output_device <>;
Чисто виртуальную функцию нельзя вызвать. И нельзя создать экземпляр класса, содержащего чисто виртуальную функцию.
Но есть проблема:
struct base < base() < foo(); >void foo() < bar(); >virtual void bar() = 0; >; struct derived : base < void bar() <>; >;
Тут, когда вы создадите derived , вам на этапе исполнения скажут, что вы вызываете чисто виртуальную функцию. Почему?
А вот смотрите. Когда вы конструируете derived , сначала вызывается конструктор base , а только потом присваивается указатель на таблицу виртуальных функций. То есть когда вызывается конструктор объекта, он не сразу с правильным динамическим типом, а изменяется по чуть-чуть: сначала он базовый, а потом нормальный. Когда иерархия больше, динамический тип меняется большее количество раз. Поэтому в конструкторе base::base bar — это функция без тела.
А ещё этот пример не компилируется без прослойки вида foo . Почему так? А вот смотрите. Никто вам не мешает вызвать виртуальную функцию напрямую (обращения к таблице) так: base::foo . А если вы вызываете функцию в конструкторе и деструкторе, то вы точно знаете, ваш динамический тип. Поэтому, написав код
struct base < base() < bar(); >virtual void bar() = 0; >; struct derived : base < void bar() <>; >;
Вы получите ошибку компиляции, потому что в base::base происходит вызов base::bar , а не просто bar , а значит мы явно вызываем виртуальную функцию.
Множественное наследование.
Начнём с того, почему некорректно делать delete указателю на базовый класс, если все деструкторы тривиальные, но не виртуальные:
struct base1 < int x; >; struct base2 < int y; >; struct derived : base1, base2 <>; int main()
Потому что первый базовый класс лежит по тому же адресу, что и оригинальный класс, а второй — со смещением. Поэтому его удалить нельзя, вы освобождаете память не по тому указателю. А виртуальный деструктор вас спасёт.
Ещё про множественное наследование нужно сказать вот что:
struct base2; struct derived; base2& to_base2(derived& d) < return (base2&)d; >struct base1 < int x; base1(int x) : x(x) <>>; struct base2 < int y; base2(int y) : y(y) <>>; struct derived : base1, base2 < derived(int x, int y) : base1(x), base2(y) <>>; int main() < derived d(1, 2); std::cout
Почему? А вот почему. Когда мы пишем to_base2 , мы ещё не знаем, что один класс наследуется от другого, причём так, что ещё и указатели надо двигать. Он будет их двигать, если написать to_base2 после классов, а так нет. Поэтому в C++ не надо использовать каст из C, вместо него есть 4 новых.
Приведение типов (cast).
Пример выше правится так:
base2& to_base2(derived& d) < return static_cast(d); >
Если это будет написано в том же месте, то словим ошибку компиляции, а если после определения derived , то проблем не будет.
- static_cast — то, что нам нужно в $99%$ случаев. Кастует
- Числа друг в друга.
- Ссылки и указатели по иерархии наследования в любую сторону.
- void* в любой указатель и обратно.
При этом, понятно, кастовать void* куда-то корректно можно, если там изначально было то, куда вы кастуете.
Аналогично, вниз по иерархии (от базового к наследуемому) можно кастовать только тогда, когда совпадает динамический тип. Иначе UB.
RTTI. typeid .
В таблицах виртуальных функций может храниться нечто другое, не только указатели на функции. В частности, в них хранится такая штука как RTTI — runtime type information. Это какая-то информация, которую компилятор вставляет в таблицу, чтобы понимать динамический тип. И к ней даже можно доступ получить. Для этого есть ключевое слово typeid . Вы даёте ему объект, а он возвращает вам std::type_info const& , который по сути и является RTTI.
Кстати, в большинстве компиляторов можно выключать RTTI (в GCC — ключ -fno-rtti), чтобы не тратить место в бинарном файле. И в каких-то кодовых базах можно увидеть код без dynamic_cast 'ов и typeid .
Парочка полезных ключевых слов.
final .
final — нельзя наследовать. Либо нельзя наследовать класс, либо нельзя больше override'ить виртуальную функцию. Пишется так:
struct inderriveable final < // . >; struct error1 : inderriveable <> // A `final` class cannot be used as a base class. struct base < virtual void foo() <>>; struct derived : base < void foo() final <>>; struct error2 : derived < void foo() <>// Cannot override `final` function `derived::foo`. >
override .
Явно указать, что вы override'ите виртуальную функцию, а не пишете что-то своё. Очень советуется это писать. Если кто-то изменит базовый класс, вы хотите явно видеть, что все функции поломаются. Пишется в том же месте, где и final .
protected .
Представим, что мы пишем виджет на основе QT. Там есть базовый виджет, у которого есть операции, что делать в случае нажатия мышки, в случае перемещения колёсика и прочее подобное. Вам всё это нужно переопределить. В таком случае в базовом виджете используется ключевое слово protected . Оно для похожих случаев и было создано, лол. Это модификатор доступа, дающий доступ только дочерним классам и себе.
С ним, правда, есть вопрос. Если метод не ломает инвариант, почему он не public , а если ломает, то хотим ли мы давать доступ дочерним классам. Тем не менее, эти вопросы не риторические, если вы нашли на них ответ — делайте protected .
Ещё best practices.
Давайте дополним наш пример с виджетами выше. Вот есть у нас виджет, который знает, как его красить. И это виртуальная функция. Мы наследуемся, меняем функцию, всё хорошо. Но есть же второй вариант — создать отдельный класс, который отвечает за покраску, наследовать только его, и передавать этот объект в конструктор виджета. Это может быть очень полезно, если мы хотим, например, одинаково красить разные классы в разных местах. Более того, мы можем собирать наш виджет из кусочков. В QT, например, используется оба подхода. Реакция на мышку, на клавиатуру, перекраска и некоторые другие штуки обычно очень сильно связаны с самим классом, а какая-нибудь стилизация — уже что-то внешнее.
Однако надо понимать, что комбинируя кусочки, можно зайти так далеко, что вы будете складывать $2+2$, получая двойку из какого-то data_provider 'а, складывая каким-нибудь классом adder и подобное. Не надо плодить фабрики непонятных классов. Когда вы делаете точку настройки, вы делаете ставку на то, что будете менять. Тут надо сильно думать. Более того, если вы сделали какие-то точки настройки, а расширять надо в другую сторону, то ваши точки настройки будут вам во вред, потому что вам надо будет их с новыми согласовывать.
Мораль: когда вы делаете фабрики/точки настройки/всё остальное, думайте, для чего вы это делаете.
Мем про квадрат и прямоугольник.
Как правильно наследоваться: квадрат от прямоугольника или прямоугольника от квадрата? (Для более детального понимания проблемы — смотрите лекцию по Java.)
Это зависит от того, что требуется от интерфейса. Давайте посмотрим, что требуется от этих фигур:
Квадрат Прямоугольник Оба get_side set_width get_width set_height get_height set_side Если нам нужно всё из этого, то отнаследовать какую бы то ни было фигуру от другой не получится. Но если у нас нет set_* , то методы, специфичные для прямоугольника, резко пропадают, а значит его можно отнаследовать от квадрата.
Наследование против union 'а.
Мы же можем использовать наследование для той же цели, что и union / std::variant — выбирать из альтернативы. В случае с std::variant мы даже можем проверять корректность обращения. Что же лучше?
Преимущества наследования Преимущества union 'а Если альтернативы разного размера, то union жрёт много памяти. Наследование работает по указателю, а это даёт лишнюю индирекцию. Можно легко добавить новую альтернативу. Можно легко добавить новую операцию. Модификаторы доступа наследования.
Как и у полей, у базовых классов можно указывать такие штуки как class A : public B . Причём в случае приватного наследования, вы не только поля из B не будете видеть в A извне, у вас даже static_cast не сработает. То есть наследование с модификаторами доступа скрывает/показывает сам факт наследования.
С какой целью это можно использовать — смотрите далее.
Виртуальное наследование.
struct A < int x; >; struct B : A <>; struct C : A <>; struct D : B, C <>; int main() < D d; d.x = 7; // Не работает, x is ambiguous. d.B::x = 7; // Работает. d.C::x = 7; // Работает. >
Если две копии A (а, следовательно, x ) — это то, что вы хотите, то хорошо. А иначе есть виртуальное наследование:
struct A < int x; >; struct B : virtual A <>; struct C : virtual A <>; struct D : B, C <>; int main()
Если базовый класс помечен virtual , то это значит, что он шарится с другими такими же виртуальными классами в иерархии. Для иерархии все virtual базы склеиваются в один подобъект (subobject).
С методами это, кстати, работает точно также. Казалось бы, в чём проблема тут:
struct A < void foo(); >; struct B : A <>; struct C : A <>; struct D : B, C <>;
Ведь метод и вас по-любому один, зачем тут виртуальное наследование? А нифига, в этот метод надо передать this типа A* const , а таких у вас два, непонятно, какой брать. Поэтому тут тоже нужно виртуальное наследование.
Теперь посмотрим на вот такой пример:
struct A < virtual void foo() = 0; >; struct B : A < void foo() override <>>; struct C : A <>; struct D : B, C <>; int main()
Тут понятно, в чём проблема. У D есть два подобъекта типа A , в одном из которых никто не за'override'ил foo . Значит D виртуальный класс. Значит его нельзя создать.
Это правится так:
struct A < virtual void foo() = 0; >; struct B : A < void foo() override <>>; struct C : A < void foo() override <>>; struct D : B, C <>; int main()
И так тоже правится:
struct A < virtual void foo() = 0; >; struct B : virtual A < void foo() override <>>; struct C : virtual A <>; struct D : B, C <>;
Подобъект типа A у нас один, и его чисто виртуальная функция переопределена. Значит всё хорошо.
struct A < virtual void foo() = 0; >; struct B : virtual A < void foo() override <>>; struct C : virtual A < void foo() override <>>; struct D : B, C <>; int main()
А так, понятно, нельзя, потому что у одного подобъекта мы два раза за'override'или одну и ту же функцию. И фиг знает, что использовать. Только если вы за'override'иде эту же функцию прямо в D , то будет понятно, что использовать, и компилироваться исправно будет.
Описанным выше образом, кстати, работают вообще любые объекты одного имени. Т.е. переменные и обычные методы взаимодействуют с виртуальным наследованием также:
struct A < int x; >; struct B : virtual A < int x; >; struct C : virtual A <>; struct D : B, C <>;
У вас будет две переменных x , в D по умолчанию будет использоваться B::x .
Применение виртуального наследования.
С помощью виртуального наследования можно реализовывать интерфейсы по чуть-чуть. У нас есть абстрактный базовый класс, который умеет, там, рендериться, изменяться, что-то ещё делать, и мы можем override'ить одну его часть в одном классе, другую — в другом. А для сборки этих штук в одну придётся использовать виртуальное наследование:
struct game_object < virtual void render() = 0; virtual void update() = 0; >; struct billbord_object : virtual game_object < void render() override* . */> ; >; struct static_object : virtual game_object < void update() override* . */> ; >; struct static_billboard : billbord_object, static_object <>;
Но это ещё цветочки, на самом деле. Представьте, что у вас есть какой-то публичный базовый класс (например, widget_painter ), и вы создаёте несколько похожих его наследников. А потом видите, что наследники похожи, их можно обобщить, и получить какую-то такую иерархию:
// Somewhere.h struct widget_painter < virtual void paint()* . */> >;
// Your_file.h struct my_base_painter : widget_painter < void paint() override* . */> >; struct my_painter1 : my_base_painter <>; struct my_painter2 : my_base_painter <>;
Но мы не хотим, чтобы кто-то приводил наши классы my_painter* к my_base_painter , это деталь реализации. Поэтому хочется написать
struct my_painter1 : private my_base_painter <>;
Но это не сработает, потому что тогда никто снаружи и наследование от widget_painter видеть не будет. Поэтому вот как надо:
// Вот сюда смотреть: vvvvvvv struct my_base_painter : virtual widget_painter < void paint() override* . */> >; // И сюда смотреть: vvvvvvv vvvvvvv struct my_painter1 : private my_base_painter, virtual widget_painter <>; struct my_painter2 : private my_base_painter, virtual widget_painter <>;
Мораль: не надо бояться виртуального наследования и пренебрегать им.
Виртуальное наследование изнутри.
Во что бы мы оттранслировали виртуальное наследование? Давайте вместо того, чтобы внутри объекта B хранить объект A, хранить указатель на него. Но если у нас много виртуальных баз, то хочется табличку. А табличку указателей на базы нельзя, у всех объектов типа D эти указатели свои, в отличие от виртуальных методов.
Давайте хранить не указатель, а смещение до нужной виртуальной базы. И тогда в каждом объекте всё одинаковое, а значит можно объединить в табличку — табличку виртуальных баз.
А теперь пример мемы:
struct base <>; struct derived : virtual base <>; derived& test(base& b) < return static_cast(b); >
Так вот это не компилируется, потому что мы совершенно не шарим, откуда в объекте b взять смещение, которое хранится в derived . Но зато можно так:
struct base < virtual ~base() <>>; struct derived : virtual base <>; derived& test(base& b) < return dynamic_cast(b); >
Это, как ни странно, компилируется, потому что вы можете взять RTTI из b , понять, что это derived , и украсть смещение из таблицы виртуальных баз для него. Более того, с dynamic_cast есть ещё больший мем:
struct A < virtual void foo() <>>; struct B <>; B& test(A& a) < return dynamic_cast(a); >
Как ни странно, это компилируется, более того, даже может быть корректно, если вы создадите класс C и отнаследуете его и от A , и от B .