Тип повернення віртуальної функції C ++


81

Чи можливо для успадкованого класу реалізувати віртуальну функцію з іншим типом повернення (не використовуючи шаблон як повернення)?

Відповіді:


86

У деяких випадках так, це дозволено, щоб похідний клас перевизначав віртуальну функцію, використовуючи інший тип повернення, доки тип повернення є коваріантним з вихідним типом повернення. Наприклад, розглянемо наступне:

class Base {
public:
    virtual ~Base() {}

    virtual Base* clone() const = 0;
};

class Derived: public Base {
public:
    virtual Derived* clone() const {
        return new Derived(*this);
    }
};

Тут Baseвизначається чисто віртуальна функція, що називається, cloneяка повертає a Base *. У похідній реалізації цю віртуальну функцію замінено, використовуючи тип повернення Derived *. Хоча тип повернення не такий, як у базовому, це абсолютно безпечно, тому що в будь-який час, коли ви пишете

Base* ptr = /* ... */
Base* clone = ptr->clone();

Виклик clone()завжди повертає покажчик на Baseоб’єкт, оскільки навіть якщо він повертає a Derived*, цей вказівник неявно конвертується в a, Base*і операція чітко визначена.

Загальніше, тип повернення функції ніколи не вважається частиною її підпису. Ви можете замінити функцію-член будь-яким типом повернення, якщо тип повернення є коваріантним.


9
Ця помилка "Ви можете замінити функцію-член будь-яким типом повернення" неправильна. Ви можете замінити, поки тип повернення ідентичний або коваріантний (що ви пояснили), крапка. Тут немає більш загального випадку.
bronekk

1
@ bronekk - У решті цитованого вами речення зазначено, що новий тип повернення повинен бути використаний у будь-якому місці оригінального типу; тобто новий тип коваріантний оригіналу.
templatetypedef

що решта речення неправильна; уявіть, як замінити Base*на longі Derived*з int(або навпаки, не має значення). Це не спрацює.
bronekk

@ bronekk- Ах так, я не думав про це! Дякуємо, що вказали на це.
templatetypedef

1
Те, що тип можна використовувати в будь-якому місці оригінального типу, і коваріація - це два різні контексти. Коваріант означає, що взаємозв'язок між типами, що реалізують функцію, і зв'язок між повернутими типами змінюється однаково. Контраваріант (хоча в C ++ не корисний) - це протилежний контекст. Деякі мови дозволяють використовувати противаріантні аргументи в динамічній диспетчериці (якщо база приймає об'єкт типу T, похідний тип може приймати T ', де T' є базою T - коли ви спускаєтесь по одній ієрархії, ви рухаєтеся вгору по іншій) ).
Девід Родрігес - dribeas

54

Так. Типи повернення можуть бути різними, якщо вони є коваріантними . Стандарт С ++ описує це так (§10.3 / 5):

Тип повернення функції, що перевизначає функцію, повинен бути або ідентичним типу повернення функції, що перевизначується, або коваріанта класам функцій. Якщо функція D::fзамінює функцію B::f, тип повернення функцій є коваріантним, якщо задовольняє наступним критеріям:

  • обидва є вказівниками на класи або посиланнями на класи 98)
  • клас у типі повернення B::fє тим самим класом, що і клас у типі повернення D::fабо, є однозначним прямим або непрямим базовим класом класу у типі повернення D::fта доступний уD
  • обидва вказівники або посилання мають однакову cv-кваліфікацію, а тип класу у типі повернення D::fмає таку ж cv-кваліфікацію, як або менше cv-кваліфікацію, ніж тип класу у типі повернення B::f.

Виноска 98 зазначає, що "багаторівневі вказівники на класи або посилання на багаторівневі вказівники на класи не дозволяються".

Коротше кажучи, якщо Dє підтипом B, то тип повернення функції в Dповинен бути підтипом типу повернення функції в B. Найпоширеніший приклад - це випадки, коли типи повернення самі базуються на Dта B, але не обов’язково. Розглянемо це, де ми розділяємо дві ієрархії типів:

struct Base { /* ... */ };
struct Derived: public Base { /* ... */ };

struct B {
  virtual Base* func() { return new Base; }
  virtual ~B() { }
};
struct D: public B {
  Derived* func() { return new Derived; }
};

int main() {
  B* b = new D;
  Base* base = b->func();
  delete base;
  delete b;
}

Причина, по якій це працює, полягає в тому, що будь-який абонент, який телефонує func, очікує Baseпокажчика. BaseПідійде будь-який вказівник. Отже, якщо D::funcобіцяє завжди повертати Derivedпокажчик, то він завжди буде задовольняти контракт, викладений класом предка, оскільки будь-який Derivedпокажчик може неявно перетворюватися на Baseпокажчик. Таким чином, абоненти завжди отримають те, що очікують.


Окрім того, що дозволяють змінювати тип повернення, деякі мови також дозволяють змінювати типи параметрів функції заміщення. Коли вони це роблять, їм, як правило, потрібно бути противаріантними . Тобто, якщо B::fприймає a Derived*, тоді D::fбуде дозволено прийняти a Base*. Нащадкам дозволено бути вільнішими в тому, що вони приймуть, і жорсткішими у тому, що вони повернуть. С ++ не допускає противаріантності типу параметра. Якщо ви зміните типи параметрів, C ++ вважає це абсолютно новою функцією, тому ви починаєте перевантажувати та приховувати. Докладніше про цю тему див. У розділі Коваріація та контраваріація (інформатика) у Вікіпедії.


2
Це фактична особливість чи побічний ефект від типу повернення, який не використовується у роздільній здатності?
Мартін Йорк

1
@Martin, безумовно, особливість. Я майже впевнений, що роздільна здатність перевантаження не мала до цього ніякого відношення. Повертається тип буде використовуватися , якщо ви перевизначення функції.
Роб Кеннеді

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.