Как определить, использует ли компилятор раннее или позднее связывание с виртуальной функцией? - PullRequest
8 голосов
/ 30 сентября 2011

У меня есть следующий код:

class Pet {
public:
  virtual string speak() const { return ""; }
};

class Dog : public Pet {
public:
  string speak() const { return "Bark!"; }
};

int main() {
  Dog ralph;
  Pet* p1 = &ralph;
  Pet& p2 = ralph;
  Pet p3;

  // Late binding for both:
  cout << "p1->speak() = " << p1->speak() <<endl;
  cout << "p2.speak() = " << p2.speak() << endl;

  // Early binding (probably):
  cout << "p3.speak() = " << p3.speak() << endl;
}

Меня попросили определить, использует ли компилятор раннее или позднее связывание для окончательного вызова функции.Я искал в Интернете, но не нашел ничего, чтобы помочь мне.Может кто-нибудь сказать мне, как я буду выполнять эту задачу?

Ответы [ 6 ]

5 голосов
/ 30 сентября 2011

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

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

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

3 голосов
/ 30 сентября 2011

Вы всегда можете использовать хак: D

//...
Pet p3;
memset(&p3, 0, sizeof(p3));
//...

Если компилятор использует указатель vtbl, угадайте, что произойдет:>

p3.speak()  // here
2 голосов
/ 30 сентября 2011

Используется раннее связывание. У вас есть объект типа P3. Хотя это базовый класс с определением виртуальной функции, тип является конкретным и известным во время компиляции, поэтому он не должен учитывать отображение виртуальной функции в производные классы.

Это почти так же, как если бы вы вызывали speak () в конструкторе Pet - даже при создании производных объектов, когда конструктор базового класса выполняет тип объекта, это тип base, поэтому функция не будет использовать v-таблица, она будет вызывать версию базового типа.

По сути, раннее связывание - это привязка во время компиляции, а позднее - привязка во время выполнения. Привязка во время выполнения используется только в тех случаях, когда у компилятора недостаточно информации о типе во время компиляции для разрешения вызова.

2 голосов
/ 30 сентября 2011

Посмотрите на сгенерированный код. Например. в Visual Studio вы можете установить точку останова, затем щелкнуть правой кнопкой мыши и выбрать «Перейти к разборке».

1 голос
/ 30 сентября 2011

Фактически, компилятор не обязан использовать ни один из них, просто чтобы убедиться, что вызывается правильная функция. В этом случае ваш объект имеет конкретный тип Pet, поэтому до тех пор, пока вызывается Pet::speak, компилятор "делает правильные вещи".

Теперь, учитывая, что компилятор может статически видеть тип объекта, я подозреваю, что большинство компиляторов оптимизируют виртуальный вызов, но не требуется, чтобы они это делали.

Если вы хотите знать, что делает ваш конкретный компилятор, единственный способ - обратиться к его документации, исходному коду или сгенерированной разборке.

0 голосов
/ 30 сентября 2011

Я просто подумал, как сказать во время выполнения, без догадок. Вы можете просто переопределить vptr ваших полиморфных классов с помощью 0 и посмотреть, вызывается ли метод или вы получаете ошибку сегментации. Вот что я получу для моего примера:

Concrete: Base
Concrete: Derived
Pointer: Base
Pointer: Derived
DELETING VPTR!
Concrete: Base
Concrete: Derived
Segmentation fault

Где Concrete: T означает, что вызов виртуальной функции-члена T через конкретный тип был успешным. Аналогично, Pointer: T говорит, что вызов функции-члена T через указатель Base был успешным.


Для справки, это моя тестовая программа:

#include <iostream>
#include <string.h>

struct Base {
  unsigned x;
  Base() : x(0xEFBEADDEu) {
  }
  virtual void foo() const {
    std::cout << "Base" << std::endl;
  }
};

struct Derived : Base {
  unsigned y;
  Derived() : Base(), y(0xEFCDAB89u) {
  }
  void foo() const {
    std::cout << "Derived" << std::endl;
  }
};

template <typename T>
void dump(T* p) {
  for (unsigned i = 0; i < sizeof(T); i++) {
    std::cout << std::hex << (unsigned)(reinterpret_cast<unsigned char*>(p)[i]);
  }
  std::cout << std::endl;
}

void callfoo(Base* b) {
  b->foo();
}

int main() {
  Base b;
  Derived d;
  dump(&b);
  dump(&d);
  std::cout << "Concrete: ";
  b.foo();
  std::cout << "Concrete: ";
  d.foo();
  std::cout << "Pointer: ";
  callfoo(&b);
  std::cout << "Pointer: ";
  callfoo(&d);
  std::cout << "DELETING VPTR!" << std::endl;
  memset(&b,0,6);
  memset(&d,0,6);
  std::cout << "Concrete: ";
  b.foo();
  std::cout << "Concrete: ";
  d.foo();
  std::cout << "Pointer: ";
  callfoo(&b);
  std::cout << "Pointer: ";
  callfoo(&d);
  return 0;
}
...