Хитрый полиморфизм и виртуальные функции

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

#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";
}; 
Другие вопросы по тегам