Namespaces
Variants

virtual function specifier

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Virtual function
override specifier (C++11)
final specifier (C++11)
Special member functions
Templates
Miscellaneous

Указывает, что нестатическая функция-член является виртуальной и поддерживает динамическое связывание. Может появляться только в decl-specifier-seq первоначального объявления нестатической функции-члена (то есть при объявлении в определении класса).

Содержание

Объяснение

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

#include <iostream>
struct Base
{
    virtual void f()
    {
        std::cout << "base\n";
    }
};
struct Derived : Base
{
    void f() override // 'override' является опциональным
    {
        std::cout << "derived\n";
    }
};
int main()
{
    Base b;
    Derived d;
    // вызов виртуальной функции через ссылку
    Base& br = b; // тип br - Base&
    Base& dr = d; // тип dr также Base&
    br.f(); // выводит "base"
    dr.f(); // выводит "derived"
    // вызов виртуальной функции через указатель
    Base* bp = &b; // тип bp - Base*
    Base* dp = &d; // тип dp также Base*
    bp->f(); // выводит "base"
    dp->f(); // выводит "derived"
    // вызов невиртуальной функции
    br.Base::f(); // выводит "base"
    dr.Base::f(); // выводит "base"
}

Подробно

Если некоторая функция-член vf объявлена как virtual в классе Base , и некоторый класс Derived , который наследуется, прямо или косвенно, от Base , имеет объявление для функции-члена с тем же

  • имя
  • список типов параметров (но не возвращаемый тип)
  • cv-квалификаторы
  • ref-квалификаторы

Тогда эта функция в классе Derived также является виртуальной (независимо от того, используется ли ключевое слово virtual в её объявлении) и переопределяет Base::vf (независимо от того, используется ли спецификатор override в её объявлении).

Base::vf не обязательно должен быть доступен или виден для переопределения. ( Base::vf может быть объявлен приватным, или Base может быть унаследован с использованием приватного наследования. Любые члены с таким же именем в базовом классе Derived , который наследует Base , не имеют значения для определения переопределения, даже если они скрывают Base::vf при поиске имени.)

class B
{
    virtual void 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 { virtual void f(); };     // A::f является виртуальной
struct B : A { void f(); };         // B::f переопределяет A::f в B
struct 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
{
    virtual void 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
{
    virtual void 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)
}

Если функция объявлена со спецификатором override , но не переопределяет виртуальную функцию, программа является некорректной:

struct B
{
    virtual void f(int);
};
struct D : B
{
    virtual void f(int) override;  // OK, D::f(int) переопределяет B::f(int)
    virtual void f(long) override; // Ошибка: f(long) не переопределяет B::f(int)
};

Если функция объявлена со спецификатором final , и другая функция пытается её переопределить, программа является некорректной:

struct B
{
    virtual void f() const final;
};
struct D : B
{
    void f() const; // Ошибка: D::f пытается переопределить final B::f
};
(since C++11)

Функции, не являющиеся членами класса, и статические функции-члены не могут быть виртуальными.

Шаблоны функций не могут быть объявлены virtual . Это относится только к функциям, которые сами являются шаблонами - обычная функция-член шаблона класса может быть объявлена виртуальной.

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

struct A
{
    virtual void f() requires true; // Error: constrained virtual function
};

Виртуальная функция consteval не должна переопределять или быть переопределенной не- consteval виртуальной функцией.

(начиная с 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
{
    virtual void vf1();
    virtual void vf2();
    virtual void vf3();
    virtual B* vf4();
    virtual B* vf5();
};
class D : private B
{
    friend struct 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 (since C++20) .

Полезное правило заключается в том, что деструктор любого базового класса должен быть public and virtual or protected and non-virtual , когда используются выражения delete , например, при неявном использовании в std::unique_ptr (since C++11) .

Во время конструирования и деструкции

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

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

struct V
{
    virtual void f();
    virtual void g();
};
struct A : virtual V
{
    virtual void f(); // A::f является финальным переопределителем V::f в A
};
struct B : virtual V
{
    virtual void g(); // B::g является финальным переопределителем V::g в B
    B(V*, A*);
};
struct D : A, B
{
    virtual void f(); // D::f является финальным переопределителем V::f в D
    virtual void 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)
}

Ключевые слова

virtual

Отчёты о дефектах

Следующие отчеты об изменениях поведения, влияющие на дефекты, были применены задним числом к ранее опубликованным стандартам C++.

DR Применяется к Поведение в опубликованной версии Корректное поведение
CWG 258 C++98 неконстантная функция-член производного класса может становиться
виртуальной из-за константной виртуальной функции-члена его базового класса
виртуальность также требует одинаковых
cv-квалификаций
CWG 477 C++98 объявление друга может содержать virtual спецификатор запрещено
CWG 1516 C++98 определения терминов "вызов виртуальной функции"
и "виртуальный вызов" не были предоставлены
предоставлены

Смотрите также

производные классы и режимы наследования
override спецификатор (C++11) явно объявляет, что метод переопределяет другой метод
final спецификатор (C++11) объявляет, что метод не может быть переопределен или класс не может быть унаследован