Могут ли виртуальные функции иметь параметры по умолчанию?

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()
}

Вот пример программы, чтобы продемонстрировать, какие значения по умолчанию выбраны. Я использую structs здесь, а не classes просто для краткости - 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, и я не получаю ошибку компиляции "неправильное количество аргументов" !!! У меня
ушло
2
Но есть ли причина для этого? Почему это определяется статическим типом?
user1289
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
Спасибо. «Не делай этого» от Херба Саттера несет некоторый вес.
Арнольд Спенс
2
@ArnoldSpence, на самом деле Херб Саттер выходит за рамки этой рекомендации. Он считает, что интерфейс вообще не должен содержать виртуальные методы: gotw.ca/publications/mill18.htm . Как только ваши методы конкретны и не могут (не должны) быть переопределены, можно задавать им параметры по умолчанию.
Марк Рэнсом
1
Я полагаю, что он имел в виду «не делай этого »: «не меняйте значение по умолчанию для параметра по умолчанию» в переопределяющих методах, а не «не
задавайте
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);
Oktalist
источник
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, деструктор должен быть виртуальным, если вы удаляете производный объект через указатель базового класса. В противном случае для всех из них будет вызван деструктор базового класса. В этом все нормально, так как деструктора нет. :-)
chappar
2
@John: Первоначально не было удалений, о чем я и говорил. Я полностью проигнорировал отсутствие виртуального деструктора. И ... @chappar: Нет, это не хорошо. У него должен быть виртуальный деструктор, который нужно удалить через базовый класс, иначе вы получите неопределенное поведение. (Этот код имеет неопределенное поведение.) Он не имеет ничего общего с данными или деструкторами производных классов.
GManNickG
@Chappar: код изначально ничего не удалял. Хотя это в основном не имеет отношения к рассматриваемому вопросу, я также добавил виртуальный dtor в базовый класс - с тривиальным dtor это редко имеет значение, но GMan совершенно прав, что без него код имеет UB.
Джерри Гроб
4

Как подробно описали другие ответы, это плохая идея. Однако, поскольку никто не упоминает о простом и эффективном решении, вот оно: Преобразуйте ваши параметры в struct, и тогда у вас могут быть значения по умолчанию для членов структуры!

Так что вместо

//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)
Шиталь шах
источник