Каковы правила динамической диспетчеризации в С++?

Интересно, как динамическая диспетчеризация действительно работает на С++. Чтобы проиллюстрировать мой вопрос, я начну с кода Java.

class A
{
  public void op(int x, double y) { System.out.println("a"); }
  public void op(double x, double y) { System.out.println("b"); }
}

class B extends A
{
  public void op(int x, double y) { System.out.println("c"); }
  public void op(int x, int y) { System.out.println("d"); }
}

class C extends B
{
  public void op(int x, int y) { System.out.println("e"); }
}

public class Pol
{
  public static void main(String[] args)
  {
    A a = new C();
    B b = new C();

    /* 1 */ a.op(2, 4);
    /* 2 */ b.op(2.0, 4.0);
  }
}

Вызов a.op(2, 4) будет печатать "c" , так как действительно компилятор:

  • смотрит в класс A (так как A объявляется переменной типа A), какой метод близок к op(int, int),
  • не может найти метод op(int, int), но находит метод op(int, double) (с одним авто-литьем intdouble),
  • затем фиксирует эту подпись.

Во время выполнения JVM:

  • ищет метод с сигнатурой op(int, double), установленный компилятором в класс C, но не находит его,
  • просматривает C суперкласс, т.е. B,
  • и, наконец, находит метод op(int, double), затем вызывает его.

Тот же принцип применяется к вызову b.op(2.0, 4.0), который печатает "b".

Теперь рассмотрим эквивалентный код в С++

#include <iostream>

class A
{
public:
  virtual void op(int x, double y) { std::cout << "a" << std::endl; }
  virtual void op(double x, double y) { std::cout << "b" << std::endl; }
};

class B : public A
{
public:
  void op(int x, double y) { std::cout << "c" << std::endl; }
  virtual void op(int x, int y) { std::cout << "d" << std::endl; }
};

class C : public B
{
public:
  void op(int x, int y) { std::cout << "e" << std::endl; }
};

int main()
{
  A *a = new C;
  B *b = new C;

  /* 1 */ a->op(2,  4);
  /* 2 */ b->op(2.0, 4.0);

  delete a;
  delete b;
}

a->op(2, 4) будет печатать "c" , как Java. Но b->op(2.0, 4.0) выводит "c" снова, и там, я потерян.

Каковы правила, применяемые при компиляции и во время выполнения на С++ для динамической диспетчеризации? (Обратите внимание, что у вас будет такое же поведение с кодом С++, если вы пишете virtual перед каждой функцией, здесь ничего не меняется)

Ответ 1

Для С++, когда вы выполняете b->op(2.0, 4.0);, компилятор просматривает B, находит метод, который он может вызвать (int x, double y), и использует его. Он не выглядит в суперклассе, если какой-либо метод в подклассе может обрабатывать вызов. Это называется методом скрытия, т.е. op(double, double) скрыт.

Если вы хотите, чтобы он выбрал версию (double x, double y), вам нужно сделать функцию видимой внутри B со следующим объявлением внутри B:

using A::op;

Дальнейшее объяснение правил

Ответ 2

Объявив новую перегрузку op в B, вы скрыли базовые версии. Компилятор будет отправлять только на основе "B", поэтому он выбирает op(int,double).

Ответ 3

Компилятор будет предупреждать/совершать ошибки при конверсиях, если вы сообщите об этом. Используя gcc, аргументы компилятора -Wconversion -Werror не позволяют компилировать ваш код, поскольку вы правы, здесь существует потенциальная потеря точности.

Учитывая, что вы не включили этот параметр компилятора, компилятор рад разрешить ваш вызов b- > op (double, double) в B:: op (int, double).

Пожалуйста, имейте в виду, что это решение времени компиляции, а не решение времени исполнения/полиморфности.

Фактическая vtable указателя "b" будет иметь метод op (int, int), доступный во время выполнения, но компилятор не знает об этом методе во время компиляции. Он может только предположить, что указатель b имеет тип B *.

Ответ 4

Вы начинаете с полиморфного поведения в базовом классе A. Затем, используя ту же подпись, вы не можете остановить это в производных классах.

Это не обязательно, если вы объявите тот же метод virtual или нет.

Вы должны изменить подпись!

Кроме того, у вас есть проблема видимости. Эта строка

B *b = new C;
b->op(2.0, 4.0);

Компилятор ищет метод в вашем классе B. op -методы скрывают методы с тем же именем класса A (разрешение перегрузки). Если он найдет что-то полезное, он просто использует его.