Указатель базового класса, указывающий на производный класс, не может получить доступ к методу производного класса

Я изучаю наследование в C ++ 11, и я обнаружил, что если производный класс переопределил имя виртуальной функции, но с другим прототипом, указатель базового класса, назначенный указателем на производный класс, может получить доступ только к версии базового класса. функции. Функция производной версии недоступна. Интересно, почему это происходит.

class Enemy {
public:
  virtual void describe() { std::cout << "Enemy"; }
};
class Dragon : public Enemy {
public:
  virtual void describe(int dummy) { std::cout << "Dragon"; }
};

In main,

Dragon foo;
Enemy* pe = &foo;
pe->describe(); // Enemy
foo.describe(1); // Dragon
pe->describe(1); // no matching function, candidate is Enemy::describe()

Из того, что я знаю о таблицах виртуальных функций, производный объект, на который указывает pe (т.е. foo), должен иметь член vpointer, указывающий на vtable Dragon. Я также знаю, что переопределение имени функции в производном классе скроет все функции с тем же именем в базовом классе. Таким образом, в vtable Дракона адрес «описать» должен быть функцией с параметром int dummy.

Но оказывается, что pe может получить доступ к версии метода Enemy, которая должна быть скрытой. И pe не может получить доступ к версии Dragon метода, которая должна быть в vtable pe. Он работает так, как если бы использовалась vtable Enemy. Почему так происходит?

Обновление: думаю, теперь я более или менее понимаю механизмы, лежащие в основе этого. Вот моя гипотеза:

Поскольку это указатель на Enemy, программа сначала найдет имя метода в области Enemy. Если имя не найдено, компилятор выдает ошибку. Если он не виртуальный, то назовите его. Если он виртуальный, запишите смещение метода в vtable Enemy. Затем программа использует это смещение для доступа к нужному методу в vtable целевого объекта.

Если метод правильно переопределен, адрес функции в vtable целевого объекта по этому смещению был бы изменен. В противном случае это будет тот же адрес функции, что и в vtable Enemy, как в примере.

Поскольку describe Dragon с int dummy - это другой прототип, он добавляется в vtable Dragon после исходного describe, унаследованного от Enemy. Но к версии int dummy нельзя получить доступ из Enemy*, потому что vtable Enemy даже не имеет этого смещения.

Это правильно?


person Elucidase    schedule 15.10.2019    source источник
comment
pe использует Enemy API, и метод, к которому осуществляется доступ, не является частью этого API.   -  person Eljay    schedule 15.10.2019
comment
Я обновил свою гипотезу о том, что происходит в примере.   -  person Elucidase    schedule 15.10.2019


Ответы (3)


Фактически у вас есть:

class Enemy {
public:
  virtual void describe() { std::cout << "Enemy"; }
};

class Dragon : public Enemy {
public:
  // void describe() override { Enemy::describe(); } // Hidden
  virtual void describe(int dummy) { std::cout << "Dragon"; }
};

Выбор метода перегрузки осуществляется статически:

  • указатели / ссылки на Enemy см. только void Enemy::describe()

  • указатели / ссылки на Dragon видят только void Dragon::describe(int) (но могут явно иметь доступ к void Enemy::describe()).

Затем выполняется виртуальная отправка с типом времени выполнения.

So

Dragon foo;
Enemy* pe = &foo;

foo.describe();         // KO: Enemy::describe() not visible (1)
foo.Enemy::describe();  // OK: Enemy::describe()
foo.describe(1);        // OK: Dragon::describe(int)

pe->describe();         // OK: Enemy::describe()
pe->describe(1);        // KO: No Enemy::describe(int)
pe->Dragon::describe(1);// KO: Dragon is not a base class of Enemy

(1) можно исправить, изменив Dragon на

class Dragon : public Enemy {
public:
  using Enemy::describe; // Unhide Enemy::describe()

  virtual void describe(int dummy) { std::cout << "Dragon"; }
};
person Jarod42    schedule 15.10.2019
comment
Но почему pe->describe - это статическая привязка (отправка) вместо динамической привязки? Я предполагал, что во время выполнения, когда программа пытается отправить виртуальный метод, она просматривает таблицу виртуальных функций объекта. В данном случае это vtable Dragon. - person Elucidase; 15.10.2019
comment
void (C::*)describe() и void (C::*)describe(int) - это две разные подписи. В таблице Dragon vtable будет 2 записи, одна для Enemy::describe() (которую он не отменяет, поэтому будет иметь то же значение, что и экземпляр Enemy), одна для Dragon::describe(int). - person Jarod42; 15.10.2019

Функции с одним и тем же именем, но с разными сигнатурами, по сути, являются разными функциями.

Объявив virtual void describe(int dummy) в своем Dragon классе, вы объявили новую виртуальную функцию, не отменяя исходную (virtual void describe() в Enemy). Вы можете переопределить виртуальные функции только с той же сигнатурой.

Вы не можете вызвать describe(1) для указателя на Enemy, потому что c ++ вызывает функцию в соответствии с типом времени компиляции экземпляра (хотя такой вызов может быть динамически отправлен для вызова фактического метода переопределения).

person Meowmere    schedule 15.10.2019
comment
Но почему здесь функция отправляется во время компиляции, а не во время выполнения? Насколько я понимаю, если функция отправляется во время выполнения, программа должна быть способна найти describe в vtable Dragon, хотя это не связано с describe в Enemy. Это правда? - person Elucidase; 15.10.2019
comment
@Elucidase Функция присутствует в vtable, но к ней нельзя получить доступ с помощью указателя на Enemy, потому что Enemy не имеет этой функции-члена (у нее есть функция с тем же именем, но не такая же функция-член). - person Meowmere; 15.10.2019
comment
@Elucidase Также подумайте с точки зрения компилятора. Если у вас есть другой тип, скажем, Soldier, который также является производным от Enemy, у которого есть метод describe, но с подписью describe() (без int). Вы даете компилятору указатель на Enemy и вызываете для него describe(1). Как компилятор узнает в компиляторе, имеет ли он тип Dragon или тип Soldier? Виртуальные методы - хороший способ создать надежный интерфейс для вызова функций, а не импровизировать с различными параметрами. - person Meowmere; 15.10.2019
comment
Но когда функция переопределена должным образом, программа может получить доступ к vtable Dragon с помощью указателя Enemy*. И из того, что я узнал, похоже, что vtable и динамическая отправка - это именно те механизмы, которые позволяют справляться с ситуациями, когда тип неизвестен во время компиляции. - person Elucidase; 15.10.2019
comment
@Elucidase То, что вы предлагаете, теоретически возможно (свидетельство: dynamic_cast в c ++), но они просто не разрешены в вашем контексте. - person Meowmere; 15.10.2019

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

Это точно так же, как если бы вы вызвали функцию в базовом классе «яблоко» и функцию в производном классе «банан». Поскольку в базовом классе нет функции «банан», очевидно, что вы не можете вызвать ее в базовом классе. Очевидно, что банановая функция в производном классе не отменяет функцию в базовом классе.

Я также знаю, что переопределение имени функции в производном классе скроет все функции с тем же именем в базовом классе.

Это неверно. Он скрывает его только в том случае, если у него то же имя, но и идентичные параметры (и любые квалификаторы, если они есть или отсутствуют).

person Sam Varshavchik    schedule 15.10.2019