Программирование ARM C++: ключевое слово virtual Fri, March 29 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

C++: ключевое слово virtual Печать
Добавил(а) microsin   

Ключевое слово virtual на языке C++ может использоваться либо как спецификатор функции, либо как спецификатор базового класса в процессе наследования (см. описание производных классов [2]). Здесь описывается первый вариант использования (перевод статьи [1]).

[virtual как спецификатор функции]

Спецификатор virtual при использовании с функцией класса задает, что эта не статическая функция является виртуальной, и поддерживает динамическую диспетчеризацию. Спецификатор virtual может появляться только при начальной декларации не статической функции класса (т. е. когда она декларируется в определении класса).

Виртуальные функции это такие функции, поведение которых может быть переопределено в унаследованных классах. В отличие от невиртуальных функций, переопределенное поведение сохраняется, даже если отсутствует информация времени компиляции о фактическом типе класса. Если производный класс обрабатывается с помощью указателя или ссылки на базовый класс, то вызов переопределенной virtual-функции вызовет поведение, заданное в производном классе. Это поведение подавляется, если функция выбрана с помощью поиска полного имени (qualified name lookup), т. е. если имя функции появляется справа от оператора разрешения области действия ::.

Пример:

#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;
 
   // Вызов virtual-функции по ссылке:
   Base& br = b;  // тип br это Base&
   Base& dr = d;  // тип dr также Base&
   br.f();        // напечатается "base"
   dr.f();        // напечатается "derived"
 
   // Вызов virtual-функции через указатель:
   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, и в функции vf такие же:

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

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

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

class B
{
   virtual void do_f();    // private-функция
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 или не унаследует (через множественное наследование) другую функцию, которая переопределяет 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 не предоставляет свое переопределение, так что в классе D
                           // конечным переопределителем будет B::f
struct E : C, D
{  // E не предоставляет свое переопределение, так что в классе E
   // конечным переопределителем будет C::f
   using A::f;    // это не декларация функции, а простое разрешение видимости A::f
};
 
int main()
{
   E e;
   e.f();      // virtual-вызов, запускающий C::f, конечный переопределитель в e
   e.E::f();   // non-virtual вызов, запускающий 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 два конечных переопределителя
//   void f();
//};
 
struct Okay : VB1, VB2
{
   void f(); // OK: это конечный переопределитель для A::f
};
 
struct VB1a : virtual A {}; // не декларируется переопределитель
 
struct Da : VB1a, VB2
{
   // в Da конечным переопределителем A::f будет VB2::f
};

Функция с таким же именем, но с другим списком параметров не переопределяет базовую функцию с тем же именем, однако скрывает её: когда система поиска по имени (unqualified name lookup) оценивает область видимости производного класса, будет найдена эта декларация, и базовый класс проверяться не будет.

struct B
{
   virtual void f();
};
 
struct D : B
{
   void f(int);   // D::f скроет B::f (не совпадающий список параметров)
};
 
struct D2 : D
{
   void f();      // D2::f переопределит B::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 [3], но не переопределяет виртуальную функцию, то в программе ошибка:

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 [4], и другая функция пытается переопределить её, то в программе ошибка (в C++ 11):

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

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

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

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

struct A
{
   virtual void f() requires true;  // Ошибка: virtual-функция с ограничением
};

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

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

[Ковариантные возвращаемые типы]

Если функция Derived::f переопределяет функцию Base::f, то её возвращаемые типы должны быть такими же, либо определенными через covariant. Два типа ковариантны, если все они удовлетворяют следующим требованиям:

• Оба типа указатели или ссылки (lvalue или rvalue) на классы. Многоуровневые указатели или ссылки не допускаются.
• Класс Base::f(), используемый по ссылке или указателю, должен быть однозначным и доступным напрямую или косвенно для класса Derived.
• Возвращаемый тип 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();       // virtual, переопределяет Base::vf1()
   void vf2(int);    // non-virtual, скрывает 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(). ~Derived() освобождает
               // ресурсы производного класса и затем вызывается Base::~Base(),
               // следуя обычному порядку удаления объектов.
}

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

Полезно следовать рекомендации: деструктор любого базового класса должен быть public и virtual, либо protected и non-virtual.

[Что происходит при вызовах конструктора и деструктора]

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

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

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();     // virtual-вызов V::f (хотя у D есть конечный переопределитель, D не существует)
   g();     // virtual-вызов B::g, который конечный переопределитель в B
 
   v->g();  // у v тип V, у которого базовый тип B, и виртуальный вызов запустит B::g,
            // как и ранее
 
   a->f();  // поскольку тип A не базовый для B, он принадлежит к другой ветви иерархии.
            // Попытка виртуального вызова через эту ветвь приведет к неопределенному
            // поведению даже когда A был уже полностью сконструирован в этом случае
            // (он был сконструирован перед B, поскольку появляется перед B в списке
            // базовых классов D). На практике виртуальный вызов A::f будет пытаться
            // использовать таблицу виртуальных функций B, поскольку это то, что
            // активно в время конструирования B).
}

[Ссылки]

1. virtual function specifier site:cppreference.com.
2. C++: производные классы (наследование).
3. C++: спецификатор override.
4. C++: спецификатор final.

 

Добавить комментарий


Защитный код
Обновить

Top of Page