Cпецификатор функции virtual
Спецификатор virtual указывает, что нестатические функции-элементы являются виртуальными и поддерживают динамическую диспетчеризацию. Оно может появляться только в последовательности-спецификаторов-объявления первоначального объявления нестатической функции-элемента (т.е. когда она объявлена в определении класса).
Содержание |
[править]Объяснение
Виртуальные функции это функции-элементы, поведение которых можно переопределить в производных классах. В отличие от невиртуальных функций переопределяющее поведение сохраняется, даже если нет информации о фактическом типе класса во время компиляции. Другими словами, если производный класс обрабатывается с помощью указателя или ссылки на базовый класс, вызов переопределённой виртуальной функции вызовет поведение, определённое в производном классе. Такой вызов функции известен как виртуальный вызов функции или виртуальный вызов. Вызов виртуальной функции подавляется, если функция выбирается с помощью поиска по полному имени (то есть, если имя функции отображается справа от оператора разрешения области видимости ::
).
#include <iostream> struct Base {virtualvoid f(){std::cout<<"базовый\n";}}; struct Derived : Base {void f() override // 'override' необязателен{std::cout<<"производный\n";}}; int main(){ Base b; Derived d; // вызов виртуальной функции по ссылке Base& br = b;// тип br является Base& Base& dr = d;// тип dr также Base& br.f();// печатает "базовый" dr.f();// печатает "производный" // вызов виртуальной функции через указатель Base* bp =&b;// тип bp является Base* Base* dp =&d;// тип dp также Base* bp->f();// печатает "базовый" dp->f();// печатает "производный" // не виртуальный вызов функции br.Base::f();// печатает "базовый" dr.Base::f();// печатает "базовый"}
[править]В деталях
Если некоторая функция-элемент vf
объявлена как virtual
в классе Base
, а некоторый класс Derived
является производным, прямо или косвенно, от Base
, имеет объявление для функции-элемента с тем же
- именем
- списком типов параметров (но не возвращаемый тип)
- cv-квалификаторами
- ссылочными-квалификаторами
Тогда эта функция в классе Derived
также является виртуальной (независимо от того, используется ли ключевое слово virtual
в её объявлении) и переопределяет Base::vf (используется или нет в его объявлении слово override
).
Base::vf
не обязательно должна быть доступна или видима для переопределения. (Base::vf
можно объявить закрытой или Base
можно наследовать с помощью закрытого наследования. Любые элементы с одинаковыми именами в базовом классе класса Derived
, которые унаследованы от Base
не имеют значения для определения переопределения, даже если они будут скрывать Base::vf
во время поиска имени.)
class B {virtualvoid do_f();// закрытый элементpublic:void f(){ do_f();}// открытый интерфейс}; struct D :public B {void do_f() override;// переопределяет B::do_f}; int main(){ D d; B* bp =&d; bp->f();// внутренне вызывает D::do_f();}
Для каждой виртуальной функции существует последний переопределяющий элемент, который выполняется при вызове виртуальной функции. Виртуальная функция-элемент vf
базового класса Base
является последним переопределением, если производный класс не объявляет или не наследует (посредством множественного наследования) другую функцию, которая переопределяет vf
.
struct A {virtualvoid f();};// A::f виртуальнаяstruct B : A {void f();};// B::f переопределяет A::f в Bstruct C :virtual B {void f();};// C::f переопределяет A::f в C struct D :virtual B {};// D не вводит переопределение, B::f является последней в D struct E : C, D // E не вводит переопределение, C::f является последней в E{using A::f;// не объявление функции, просто делает A::f видимой для поиска}; int main(){ E e; e.f();// виртуальный вызов вызывает C::f, последнее переопределение в e e.E::f();// невиртуальный вызов вызывает A::f, который виден в E}
Если функция имеет более одного последнего переопределения, программа некорректна:
struct A {virtualvoid f();}; struct VB1 :virtual A {void f();// переопределяет A::f}; struct VB2 :virtual A {void f();// переопределяет A::f}; // struct Error : VB1, VB2// {// // Ошибка: A::f имеет два последних переопределения в Error// }; struct Okay : VB1, VB2 {void f();// OK: это последнее переопределение для A::f}; struct VB1a :virtual A {};// не объявляет переопределение struct Da : VB1a, VB2 {// в Da последним переопределением A::f является VB2::f};
Функция с тем же именем, но другим списком параметров не переопределяет базовую функцию с тем же именем, а скрывает её: когда поиск по неполному имени проверяет область видимости производного класса, поиск находит объявление и не проверяет базовый класс.
struct B {virtualvoid f();}; struct D : B {void f(int);// D::f скрывает B::f (неверный список параметров)}; struct D2 : D {void f();// D2::f переопределяет B::f (неважно, что она не видна)}; int main(){ B b; B& b_as_b = b; D d; B& d_as_b = d; D& d_as_d = d; D2 d2; B& d2_as_b = d2; D& d2_as_d = d2; b_as_b.f();// вызов B::f() d_as_b.f();// вызов B::f() d2_as_b.f();// вызов D2::f() d_as_d.f();// Ошибка: поиск в D находит только f(int) d2_as_d.f();// Ошибка: поиск в D находит только f(int)}
Если функция объявлена со спецификатором struct B {virtualvoid f(int);}; struct D : B {virtualvoid f(int) override;// OK, D::f(int) переопределяет B::f(int)virtualvoid f(long) override;// Ошибка: f(long) не переопределяет B::f(int)}; Если функция объявлена со спецификатором struct B {virtualvoid f()const final;}; struct D : B {void f()const;// Ошибка: D::f пытается переопределить final B::f}; | (начиная с C++11) |
Функции, не являющиеся элементами, и статические функции-элементы не могут быть виртуальными.
Шаблоны функций нельзя объявлять virtual
. Это относится только к функциям, которые сами являются шаблонами — обычная функция-элемент шаблона класса может быть объявлена виртуальной.
Виртуальные функции (будь то объявленные виртуальными или переопределяющие) не могут иметь никаких связанных ограничений. struct A {virtualvoid f() requires true;// Ошибка: ограниченная виртуальная функция}; Виртуальная функция | (начиная с C++20) |
Аргументы по умолчанию для виртуальных функций заменяются во время компиляции.
[править]Ковариантные возвращаемые типы
Если функция Derived::f
переопределяет функцию Base::f
, их возвращаемые типы должны быть либо одинаковыми, либо быть ковариантными. Два типа являются ковариантными, если они соответствуют всем следующим требованиям:
- оба типа являются указателями или ссылками (lvalue или rvalue) на классы. Многоуровневые указатели или ссылки не допускаются.
- класс, на который ссылаются/указывают в возвращаемом типе
Base::f()
, должен быть однозначным и доступным прямым или косвенным базовым классом класса, на который ссылается/указывает возвращаемый типDerived::f()
. - возвращаемый тип
Derived::f()
должен быть равной или меньшей cv-квалификации, чем возвращаемый типBase::f()
.
Класс в возвращаемом типе Derived::f
должен быть либо сам Derived
, либо должен быть полным типом в точке объявления Derived::f
.
Когда выполняется вызов виртуальной функции, тип, возвращаемый последним переопределением, представляет собой неявно преобразованный в тип возвращаемого значения переопределённой функции, которая была вызвана:
class B {}; struct Base {virtualvoid vf1();virtualvoid vf2();virtualvoid vf3();virtual B* vf4();virtual B* vf5();}; class D :private B {friendstruct Derived;// в Derived B является доступным базовым классом для D}; class A;// предварительно объявленный класс является неполным типом struct Derived :public Base {void vf1();// виртуальная, переопределяет Base::vf1()void vf2(int);// не виртуальная, скрывает Base::vf2()// char vf3(); // Ошибка: переопределяет Base::vf3, но имеет// другой и нековариантный возвращаемый тип D* vf4();// переопределяет Base::vf4() и имеет ковариантный возвращаемый тип// A* vf5(); // Ошибка: A неполный тип}; int main(){ Derived d; Base& br = d; Derived& dr = d; br.vf1();// вызов Derived::vf1() br.vf2();// вызов Base::vf2()// dr.vf2(); // Ошибка: vf2(int) скрывает vf2() B* p = br.vf4();// вызывает Derived::vf4() и преобразует результат в B* D* q = dr.vf4();// вызывает Derived::vf4() и не преобразует результат в B*}
[править]Виртуальный деструктор
Несмотря на то, что деструкторы не наследуются, если базовый класс объявляет свой деструктор как virtual
, производный деструктор всегда переопределяет его. Это даёт возможность удалять динамически размещаемые объекты полиморфного типа через указатели на базовый класс.
class Base {public:virtual ~Base(){/* освобождает ресурсы Base */}}; class Derived :public Base { ~Derived(){/* освобождает ресурсы Derived */}}; int main(){ Base* b = new Derived; delete b;// Выполняет вызов виртуальной функции Base::~Base()// поскольку она виртуальная, она вызывает Derived::~Derived()// которая может освободить ресурсы производного класса,// а затем вызывает Base::~Base() в обычном порядке уничтожения}
Более того, если деструктор базового класса не является виртуальным, удаление объекта производного класса с помощью указателя на базовый класс является неопределённым поведением независимо от того, есть ли ресурсы, которые могут быть утеряны, если производный деструктор не будет вызван, если только выбранная функция освобождения не является operator delete(начиная с C++20).
Полезным правилом является то, что деструктор любого базового класса должен быть открытым и виртуальным или защищённым и не виртуальным, всякий раз, когда используются выражения удаления, например, при неявном использовании в std::unique_ptr(начиная с C++11).
[править]При конструировании и разрушении
Когда виртуальная функция вызывается прямо или косвенно из конструктора или из деструктора (в том числе во время построения или уничтожения нестатических элементов данных класса, например, в списке инициализаторов элементов), и объект, к которому применяется вызов, является объектом в процессе построения или уничтожения, вызываемая функция является последним переопределением в классе конструктора или деструктора, а не переопределением в более производном классе. Другими словами, во время создания или разрушения более производные классы не существуют.
При построении сложного класса с несколькими ветвями внутри конструктора, принадлежащего одной ветви, полиморфизм ограничивается этим классом и его базовыми: если он получает указатель или ссылку на базовый подобъект вне этой подиерархии и пытается вызвать виртуальную функцию (например, используя явный доступ к элементу), поведение не определено:
struct V {virtualvoid f();virtualvoid g();}; struct A :virtual V {virtualvoid f();// A::f является последним переопределением V::f в A}; struct B :virtual V {virtualvoid g();// B::g является последним переопределением V::g в B B(V*, A*);}; struct D : A, B {virtualvoid f();// D::f является последним переопределением V::f в Dvirtualvoid g();// D::g является последним переопределением V::g в D // примечание: A инициализируется перед B D(): B((A*) this, this){}}; // конструктор B, вызываемый из конструктора D B::B(V* v, A* a){ f();// виртуальный вызов V::f (хотя у D есть последний переопределяющий,// D не существует) g();// виртуальный вызов B::g, который является последним переопределением в B v->g();// тип v V является базовым для B, виртуальный вызов вызывает B::g,// как и раньше a->f();// тип a A не является базовым для B. Он принадлежит к другой// ветви иерархии. Попытка виртуального вызова через эту ветвь// приводит к неопределённому поведению, даже несмотря на то,// что в этом случае A уже был полностью сконструирован (он// был сконструирован до B, поскольку он появляется перед B в// списке базовых классов для D). На практике виртуальный вызов// A::f будет предпринят с использованием таблицы виртуальных функций-// элементов B, так как он активен во время построения B)}
[править]Отчёты о дефектах
Следующие изменения поведения были применены с обратной силой к ранее опубликованным стандартам C++:
Номер | Применён | Поведение в стандарте | Корректное поведение |
---|---|---|---|
CWG 258 | C++98 | неконстантная функция-элемент производного класса может стать виртуальной из-за константной виртуальной функции- элемента его базового класса | виртуальность также требует, чтобы cv-квалификация была одинаковой |
CWG 477 | C++98 | объявление дружественной функции может содержать спецификатор virtual | не позволяется |
CWG 1516 | C++98 | определение терминов "виртуальный вызов функции" и "виртуальный вызов" не дано | дано |
[править]Смотрите также
производные классы и режимы наследования | |
спецификатор override (C++11) | явно объявляет, что метод переопределяет другой метод |
спецификатор final (C++11) | объявляет, что метод не может быть переопределён |