Связывание и виртуальность
дата публикации: 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

Схема зависимостей классов представлена на рисунке:

Не виртуальное наследование

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