Чи можуть віртуальні функції мати параметри за замовчуванням?


164

Якщо я оголошу базовий клас (або клас інтерфейсу) і вказую значення за замовчуванням для одного або декількох його параметрів, чи повинні похідні класи задавати однакові за замовчуванням, а якщо ні, то які за замовчуванням будуть проявлятися у похідних класах?

Додаток: Мене також цікавить, як це може бути оброблено в різних компіляторах та будь-який внесок щодо "рекомендованої" практики в цьому сценарії.


1
Це здається легким для перевірки. Ви пробували?
andand

22
Я зараз пробую це, але я не знайшов конкретної інформації про те, як "визначилася" поведінка, тому я врешті знайду відповідь для свого конкретного компілятора, але це не скаже мені, чи всі компілятори будуть робити те саме річ. Мене також цікавить рекомендована практика.
Арнольд Спенс

1
Поведінка чітко визначена, і я сумніваюся, ви знайдете компілятор, який помиляється (ну, можливо, якщо ви протестуєте gcc 1.x або VC ++ 1.0 чи щось подібне). Рекомендована практика проти цього робити.
Джеррі Труну

Відповіді:


213

У віртуалах можуть бути за замовчуванням. За замовчуванням у базовому класі не успадковуються похідні класи.

Який за замовчуванням використовується - тобто базовий клас 'або похідний клас', визначається статичним типом, який використовується для здійснення виклику функції. Якщо ви телефонуєте через об'єкт, вказівник або посилання базового класу, використовується базовий клас, позначений у базовому класі. І навпаки, якщо ви зателефонуєте через похідний об'єкт класу, вказівник або посилання використовуються за замовчуванням, позначені у похідному класі. Нижче є приклад "Стандарт", який демонструє це.

Деякі компілятори можуть робити щось інше, але ось що говорять стандарти C ++ 03 і C ++ 11:

8.3.6.10:

Виклик віртуальної функції (10.3) використовує аргументи за замовчуванням у декларації віртуальної функції, що визначається статичним типом вказівника або посиланням, що позначає об'єкт. Перевизначна функція у похідному класі не отримує аргументів за замовчуванням від функції, яку вона перекриває. Приклад:

struct A {
  virtual void f(int a = 7);
};
struct B : public A {
  void f(int a);
};
void m()
{
  B* pb = new B;
  A* pa = pb;
  pa->f(); //OK, calls pa->B::f(7)
  pb->f(); //error: wrong number of arguments for B::f()
}

Ось приклад програми, яка демонструє, які за замовчуванням вибираються. Я structтут використовую s, а не classпросто для стислості - classі structабсолютно однакові майже у всіх напрямках, окрім видимості за замовчуванням.

#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>

using std::stringstream;
using std::string;
using std::cout;
using std::endl;

struct Base { virtual string Speak(int n = 42); };
struct Der : public Base { string Speak(int n = 84); };

string Base::Speak(int n) 
{ 
    stringstream ss;
    ss << "Base " << n;
    return ss.str();
}

string Der::Speak(int n)
{
    stringstream ss;
    ss << "Der " << n;
    return ss.str();
}

int main()
{
    Base b1;
    Der d1;

    Base *pb1 = &b1, *pb2 = &d1;
    Der *pd1 = &d1;
    cout << pb1->Speak() << "\n"    // Base 42
        << pb2->Speak() << "\n"     // Der 42
        << pd1->Speak() << "\n"     // Der 84
        << endl;
}

Вихід цієї програми (для MSVC10 та GCC 4.4):

Base 42
Der 42
Der 84

Дякую за довідку, яка говорить мені про поведінку, яку я розумно очікую від компіляторів (сподіваюся).
Арнольд Спенс

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

Я використовую gcc 4.8.1, і я не отримую помилку компіляції "неправильна кількість аргументів" !!! Забрав мене півтора дня, щоб знайти помилку ...
steffen

2
Але чи є причина для цього? Чому він визначається статичним типом?
користувач1289

2
Clang-tidy розглядає параметри за замовчуванням у віртуальних методах як щось небажане і видає попередження про це: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/…
Мартін Печка

38

Це було темою одного з ранніх публікацій Гуру тижня Герб Саттера .

Перше, що він каже з цього приводу - НЕ РОБИТИ ЦЕ.

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

Дано

class A {
    virtual void foo(int i = 1) { cout << "A::foo" << i << endl; }
};
class B: public A {
    virtual void foo(int i = 2) { cout << "B::foo" << i << endl; }
};
void test() {
A a;
B b;
A* ap = &b;
a.foo();
b.foo();
ap->foo();
}

ви повинні отримати A :: foo1 B :: foo2 B :: foo1


7
Дякую. "Не робіть цього" від Herb Sutter має певну вагу.
Арнольд Спенс

2
@ArnoldSpence, насправді Герб Саттер виходить за рамки цієї рекомендації. Він вважає, що інтерфейс взагалі не повинен містити віртуальних методів: gotw.ca/publications/mill18.htm . Після того, як ваші методи конкретні і їх не можна (не слід) переосмислювати, можна безпечно задавати їм параметри за замовчуванням.
Марк Рансом

1
Я вважаю, що він мав на увазі під «не робити цього » те, що «не змінюйте значення за замовчуванням параметру за замовчуванням» у переважаючих методах, не «не вказуйте параметри за замовчуванням у віртуальних методах»
Weipeng L

6

Це погана ідея, оскільки аргументи за замовчуванням, які ви отримаєте, залежатимуть від статичного типу об'єкта, тоді як virtualфункція, що надсилається, залежатиме від динамічного типу.

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

@cppcoder запропонував такий приклад у своєму [закритому] запитанні :

struct A {
    virtual void display(int i = 5) { std::cout << "Base::" << i << "\n"; }
};
struct B : public A {
    virtual void display(int i = 9) override { std::cout << "Derived::" << i << "\n"; }
};

int main()
{
    A * a = new B();
    a->display();

    A* aa = new A();
    aa->display();

    B* bb = new B();
    bb->display();
}

Що дає наступний вихід:

Derived::5
Base::5
Derived::9

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

    A * a = new B();
    a->display(5);

    A* aa = new A();
    aa->display(5);

    B* bb = new B();
    bb->display(9);

4

Як видно з інших відповідей, це складний предмет. Замість того, щоб намагатися це зробити чи зрозуміти, що це робить (якщо вам доведеться запитати зараз, технічному обслуговувачу доведеться запитати чи шукати це через рік).

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


1
Це зовсім не складно. Параметри за замовчуванням виявляються разом із роздільною здатністю імені. Вони дотримуються тих же правил.
Едвард Странд

4

Це те, що, ймовірно, можна зрозуміти досить добре, тестуючи (тобто це достатньо основна частина мови, що більшість компіляторів майже напевно правильно його опрацьовує, і якщо ви не побачите відмінностей між компіляторами, їх вихід може вважатися досить авторитетним).

#include <iostream>

struct base { 
    virtual void x(int a=0) { std::cout << a; }
    virtual ~base() {}
};

struct derived1 : base { 
    void x(int a) { std:: cout << a; }
};

struct derived2 : base { 
    void x(int a = 1) { std::cout << a; }
};

int main() { 
    base *b[3];
    b[0] = new base;
    b[1] = new derived1;
    b[2] = new derived2;

    for (int i=0; i<3; i++) {
        b[i]->x();
        delete b[i];
    }

    derived1 d;
    // d.x();       // won't compile.
    derived2 d2;
    d2.x();
    return 0;
}

4
@GMan: [Обережно дивлячись невинні] Що протікає? :-)
Джеррі Коффін

Я думаю, що він має на увазі відсутність віртуального деструктора. Але в цьому випадку вона не просочиться.
Джон Дайблінг

1
@Jerry, деструктор має бути віртуальним, якщо ви видаляєте похідний об'єкт через покажчик базового класу. Інакше деструктор базового класу буде викликаний для всіх. У цьому це нормально, оскільки деструктора немає. :-)
четвер

2
@John: Спочатку не було делетів, про що я мав на увазі. Я повністю ігнорував відсутність віртуального деструктора. І ... @chappar: Ні, це не нормально. Він повинен мати віртуальний деструктор, який потрібно видалити через базовий клас, або ви отримаєте невизначене поведінку. (Цей код має невизначену поведінку.) Він не має нічого спільного з тими даними або деструкторами, які мають похідні класи.
GManNickG

@Chappar: Спочатку код нічого не видалив. Незважаючи на те, що це питання в основному не має значення, я також додав віртуальний dtor до базового класу - з тривіальним dtor це рідко має значення, але GMan цілком правильний, що без нього код має UB.
Джері Коффін

4

Як детально описані інші відповіді, її погана ідея. Однак, оскільки ніхто не згадує просте і ефективне рішення, ось воно: Перетворіть ваші параметри в структура, і тоді ви можете мати значення за замовчуванням для членів структури!

Тож замість того,

//bad idea
virtual method1(int x = 0, int y = 0, int z = 0)

зробити це,

//good idea
struct Param1 {
  int x = 0, y = 0, z = 0;
};
virtual method1(const Param1& p)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.