Связывание и виртуальность
дата публикации: 2016-07-04Сначала, приведу несколько полезных понятий, которые взяты мной из книги: Design Patterns: Elements of Reusable Object-Oriented Software
Сигнатура операции – это имя операции, объекты, передаваемые в качестве параметров и возвращаемый результат.
Интерфейс объекта – множество сигнатур всех операций объекта.
Тип – имя интерфейса. Вообще говоря, один объект может иметь несколько типов (например, при множественном наследовании от нескольких интерфейсов). Бывает и такое, что разные объекты имеют один тип (например, при наследовании от одного интерфейса).
Теперь непосредственно о раннем и позднем связывании. Раннее связывание (статическое связывание) осуществляется на этапе компиляции. К примеру, имеется шаблонная функция, которая вызывается дважды с переменными разных типов. Это приводит к созданию двух функций на этапе компиляции, что несложно подтвердить, получив код ассемблера следующего примера.
template T max(T& a, T& b) { return (a > b) ? a : b; } int main() { int ai = 10, bi = 20; double ad = 7, bd = 6; cout << max(ai, bi) << endl; cout << max(ad, bd) << endl; return 0; }
Сигнатурой операции в данном случае является следующая строчка кода T max(T& a, T& b). Вызов метода с именем max осуществляется дважды и логично предположить, что на этапе компиляции создается две функции. Код ассемблера:
int max(int&, int&): pushq %rbp movq %rsp, %rbp movq %rdi, -8(%rbp) movq %rsi, -16(%rbp) movq -8(%rbp), %rax movl (%rax), %edx movq -16(%rbp), %rax movl (%rax), %eax cmpl %eax, %edx jle .L4 movq -8(%rbp), %rax movl (%rax), %eax jmp .L6 .L4: movq -16(%rbp), %rax movl (%rax), %eax .L6: popq %rbp ret double max(double&, double&): pushq %rbp movq %rsp, %rbp movq %rdi, -8(%rbp) movq %rsi, -16(%rbp) movq -8(%rbp), %rax movsd (%rax), %xmm0 movq -16(%rbp), %rax movsd (%rax), %xmm1 ucomisd %xmm1, %xmm0 jbe .L13 movq -8(%rbp), %rax movsd (%rax), %xmm0 jmp .L11 .L13: movq -16(%rbp), %rax movsd (%rax), %xmm0 .L11: popq %rbp ret
Код получен с помощью онлайн компилятора gcc.godbolt.org.
Позднее связывание, его еще иногда называют динамическим связыванием тесно связано с понятием виртуальности и полиморфизма. Такой тип связывания осуществляется на этапе выполнения программы, а не на этапе компиляции. Приведу пример:
class Base { public: Base(); virtual ~Base(); virtual void info() = 0; }; Base::Base() { cout << "I'm Constructor Base" << endl; } Base::~Base() { cout << "I'm Destructor Base" << endl; } class Derived1 : public Base { public: Derived1(); virtual ~Derived1(); void info(); }; Derived1::Derived1() { cout << "I'm Constructor Derived1" << endl; } Derived1::~Derived1() { cout << "I'm Destructor Derived1" << endl; } void Derived1::info() { cout << "I'm Derived1" << endl; } class Derived2 : public Base { public: Derived2(); virtual ~Derived2(); void info(); }; Derived2::Derived2() { cout << "I'm Constructor Derived2" << endl; } Derived2::~Derived2() { cout << "I'm Destructor Derived2" << endl; } void Derived2::info() { cout << "I'm Derived2" << endl; } int main() { Base *der1 = new Derived1(); Base *der2 = new Derived2(); der1->info(); der2->info(); delete der1; delete der2; return 0; }
Результатом выполнения кода будет:
I'm Constructor Base I'm Constructor Derived1 I'm Constructor Base I'm Constructor Derived2 I'm Derived1 I'm Derived2 I'm Destructor Derived1 I'm Destructor Base I'm Destructor Derived2 I'm Destructor Base
При вызове метода info для разных переменных der1 и der2 будут вызываться разные реализации. При этом сигнатуры методов идентичны. Дело в том, что, когда происходит вызов метода, сначала происходит выбор нужного адреса метода по известному интерфейсу и адресу вызывающего объекта и лишь затем вызов реализации по выбранному адресу. Вот в этом и есть суть полиморфизма, виртуальных методов и позднего (динамического) связывания. Заодно в примере продемонстрирована работа виртуального деструктора.
Теперь рассмотрим особенности виртуального наследования, для чего приведем следующий код:
class Base { public: Base(){ cout << "I'm Constructor Base" << endl; } }; class Derived1 : virtual public Base { public: Derived1(){ cout << "I'm Constructor Derived1" << endl; } }; class Derived2 : virtual public Base { public: Derived2(){ cout << "I'm Constructor Derived2" << endl; } }; class Derived3 : virtual public Base, public Derived1, public Derived2 { public: Derived3(){ cout << "I'm Constructor Derived3" << endl; } }; int main() { Derived3 obj; }
Результатом выполнения кода будет:
I'm Constructor Base I'm Constructor Derived1 I'm Constructor Derived2 I'm Constructor Derived3
Схема зависимостей классов представлена на рисунке:

Если убрать ключевое слово virtual в конструкциях наследования классов Derived1, Derived2 и Derived3 мы получим следующий результат работы кода:
I'm Constructor Base I'm Constructor Base I'm Constructor Derived1 I'm Constructor Base I'm Constructor Derived2 I'm Constructor Derived3
Схема зависимостей классов представлена на рисунке:

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