Хитрый полиморфизм и виртуальные функции
У меня есть следующий код.
#include <iostream>
using namespace std;
class K {
public:
virtual void add_st(K* n) {
cout << "add_st (K*) from K\n";
}
};
class L: public K {
public:
virtual void add_st(L* a) {
cout << "add_st (L*) from L\n";
}
};
int main() {
L ob, ob2;
K k, *pl = &ob;
pl->add_st(&ob2);
return 0;
}
Выход этой программы будет:
add_st (K*) from K
Причина, по которой, если я ничего не пропустил, это таблица виртуальных функций. Объект генерируется от вершины иерархии до низшего класса.
Но этот код:
#include <iostream>
using namespace std;
class K {
public:
virtual void add_st() {
cout << "add_st (K*) from K\n";
}
};
class L: public K {
public:
virtual void add_st() {
cout << "add_st (L*) from L\n";
}
};
int main() {
L ob, ob2;
K k, *pl = &ob;
pl->add_st();
return 0;
}
Будет печатать
add_st (L*) from L
Зачем?
2 ответа
Виртуальные функции инвариантны в списке аргументов и ковариантны в типе возвращаемого значения.
Элементарный способ думать об этом состоит в том, что, когда виртуальная функция-член представлена в базовом классе, она определяет контракт.
Например, учитывая
struct K
{
virtual K* add_st(K* n);
};
контракт заключается в том, что add_st
принимает любой объект типаK
(по указателю) и возвращает объект типаK
(по указателю).
Это переопределит это
struct L : K
{
virtual K* add_st(K* a);
};
потому что контракт четко соблюден, и так будет:
struct M : K
{
virtual M* add_st(K* a);
};
потому что возвращение является объектом типа M
который по наследству также является объектом типа K
; договор выполнен.
Но это (случай в вопросе) не отменяет
struct N : K
{
virtual K* add_st(N* a);
};
потому что он не может принять любой объект типа K
только те, которые оба типа K
и введите N
, И это тоже
struct P : K
{
virtual K* add_st(void* a);
};
даже несмотря на то, что с точки зрения теории типов контравариантные параметры были бы совместимы, правда в том, что C++ поддерживает множественное наследование, а для повышений иногда требуется настройка указателя, поэтому контравариантные типы параметров нарушаются на уровне реализации.
Они создадут новую функцию (новый слот в v-таблице), которая перегружает и скрывает существующую функцию вместо ее переопределения. (Как говорит Джон Смит в своем ответе, объявление об использовании может быть использовано, чтобы избежать скрытия базовой версии)
И следующее является ошибкой, потому что подпись такая же, но тип возвращаемого значения несовместим:
struct Q : K
{
virtual void* add_st(K* a);
};
Здесь результатом может быть любой тип объекта, но этого недостаточно, для контракта требуется объект типа K
, И он не может переопределить существующую функцию, потому что параметры не отличаются. Так что это просто отвергнуто.
Для более подробной информации о дисперсии, вы можете прочитать о принципе замены Лискова.
Во-первых, сигнатура функции включает имя функции и типы ее параметров. В вашем первом примере имя функции совпадает, но типы ее параметров различны. Поэтому у них разные подписи. Поэтому в вашем первом примере функция в вашем дочернем классе не переопределяет функцию в своем родительском классе.
Во-вторых, есть также концепция overload
и имя скрывается. В вашем случае определение функции в первом примере скрывает свою родительскую функцию. Если вы перенесете родительскую функцию в ту же область, дочерняя функция перегрузит родительскую функцию, как это
class L: public K {
public:
using K::add_st;
virtual void add_st() {
cout << "add_st (L*) from L\n";
};