Czy funkcje wirtualne mogą mieć parametry domyślne?

164

Jeśli zadeklaruję klasę bazową (lub klasę interfejsu) i określę wartość domyślną dla co najmniej jednego z jej parametrów, czy klasy pochodne muszą określać te same wartości domyślne, a jeśli nie, to które wartości domyślne będą widoczne w klasach pochodnych?

Dodatek: Interesuje mnie również, jak można to załatwić w różnych kompilatorach, oraz wszelkie dane wejściowe dotyczące „zalecanych” praktyk w tym scenariuszu.

Arnold Spence
źródło
1
Wydaje się, że łatwo to sprawdzić. Próbowałeś tego?
i i
22
Jestem w trakcie wypróbowywania tego, ale nie znalazłem konkretnych informacji o tym, jak „zdefiniowane” będzie zachowanie, więc w końcu znajdę odpowiedź dla mojego konkretnego kompilatora, ale to nie powie mi, czy wszystkie kompilatory zrobią to samo rzecz. Interesuje mnie również zalecana praktyka.
Arnold Spence
1
Zachowanie jest dobrze zdefiniowane i wątpię, czy znajdziesz kompilator, który pomyli się (no cóż, może jeśli przetestujesz gcc 1.x lub VC ++ 1.0 lub coś w tym rodzaju). Zalecaną praktyką jest przeciwdziałanie temu.
Jerry Coffin

Odpowiedzi:

213

Wirtualne mogą mieć wartości domyślne. Wartości domyślne w klasie bazowej nie są dziedziczone przez klasy pochodne.

To, która wartość domyślna jest używana - tj. Klasa bazowa „czy klasa pochodna” - jest określane przez typ statyczny użyty do wywołania funkcji. Jeśli wywołujesz za pośrednictwem obiektu klasy bazowej, wskaźnika lub odwołania, używana jest wartość domyślna oznaczona w klasie bazowej. I odwrotnie, jeśli wywołujesz za pośrednictwem obiektu klasy pochodnej, używany jest wskaźnik lub odwołanie, wartości domyślne oznaczone w klasie pochodnej. Pokazuje to przykład poniżej standardowego cytatu.

Niektóre kompilatory mogą zrobić coś innego, ale tak mówią standardy C ++ 03 i C ++ 11:

8.3.6.10:

Wywołanie funkcji wirtualnej (10.3) wykorzystuje domyślne argumenty w deklaracji funkcji wirtualnej określone przez statyczny typ wskaźnika lub referencji oznaczającej obiekt. Funkcja przesłaniająca w klasie pochodnej nie pobiera argumentów domyślnych z funkcji, którą przesłania. Przykład:

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

Oto przykładowy program pokazujący, jakie ustawienia domyślne są pobierane. Używam structtutaj znaków s zamiast classes po prostu dla zwięzłości - classi structsą one dokładnie takie same pod każdym względem, z wyjątkiem domyślnej widoczności.

#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;
}

Dane wyjściowe tego programu (na MSVC10 i GCC 4.4) to:

Base 42
Der 42
Der 84
John Dibling
źródło
Dzięki za odniesienie, które mówi mi o zachowaniu, którego mogę się spodziewać po kompilatorach (mam nadzieję).
Arnold Spence
To jest korekta mojego poprzedniego podsumowania: zaakceptuję tę odpowiedź jako odniesienie i wspomnę, że zbiorowa rekomendacja jest taka, że ​​można mieć domyślne parametry w funkcjach wirtualnych, o ile nie zmieniają one domyślnych parametrów wcześniej określonych w przodku klasa.
Arnold Spence
Używam gcc 4.8.1 i nie pojawia się błąd kompilacji „zła liczba argumentów” !!! Znalezienie błędu
zajęło
2
Ale czy jest jakiś powód? Dlaczego zależy to od typu statycznego?
user1289
2
Clang-tidy traktuje domyślne parametry metod wirtualnych jako coś niepożądanego i wyświetla ostrzeżenie o tym: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/…
Martin Pecka
38

To był temat jednego z pierwszych postów Guru Tygodnia Herba Suttera .

Pierwszą rzeczą, jaką mówi na ten temat, jest: NIE ROBIĆ TEGO.

Bardziej szczegółowo, tak, możesz określić różne parametry domyślne. Nie będą działać tak samo, jak funkcje wirtualne. Funkcja wirtualna jest wywoływana dla typu dynamicznego obiektu, podczas gdy domyślne wartości parametrów są oparte na typie statycznym.

Dany

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

powinieneś otrzymać A :: foo1 B :: foo2 B :: foo1

David Thornley
źródło
7
Dzięki. „Nie rób tego” od Herba Suttera ma pewne znaczenie.
Arnold Spence
2
@ArnoldSpence, w rzeczywistości Herb Sutter wykracza poza to zalecenie. Uważa, że ​​interfejs w ogóle nie powinien zawierać metod wirtualnych: gotw.ca/publications/mill18.htm . Gdy metody są konkretne i nie można ich (nie należy) nadpisywać, można bezpiecznie nadać im domyślne parametry.
Mark Ransom
1
Myślę, że przez „nie rób tego ” miał na myśli „ nie zmieniaj domyślnej wartości parametru domyślnego” w metodach nadpisywania, a nie „nie określaj parametrów domyślnych w metodach wirtualnych”
Weipeng L
6

To zły pomysł, ponieważ domyślne argumenty, które otrzymasz, będą zależały od statycznego typu obiektu, podczas gdy virtualfunkcja wysyłana do będzie zależała od typu dynamicznego .

Oznacza to, że kiedy wywołujesz funkcję z domyślnymi argumentami, domyślne argumenty są zastępowane w czasie kompilacji, niezależnie od tego, czy funkcja jest, virtualczy nie.

@cppcoder podał następujący przykład w swoim [zamkniętym] pytaniu :

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

Który daje następujący wynik:

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

Na podstawie powyższego wyjaśnienia łatwo zrozumieć, dlaczego. W czasie kompilacji kompilator zastępuje domyślne argumenty z funkcji składowych typów statycznych wskaźników, dzięki czemu mainfunkcja jest równoważna z poniższym:

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

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

    B* bb = new B();
    bb->display(9);
Oktalist
źródło
4

Jak widać z innych odpowiedzi, jest to skomplikowany temat. Zamiast próbować to zrobić lub zrozumieć, co robi (jeśli będziesz musiał zapytać teraz, opiekun będzie musiał zapytać lub sprawdzić to za rok od teraz).

Zamiast tego utwórz publiczną niewirtualną funkcję w klasie bazowej z domyślnymi parametrami. Następnie wywołuje prywatną lub chronioną funkcję wirtualną, która nie ma parametrów domyślnych i jest zastępowana w klasach potomnych w razie potrzeby. Wtedy nie musisz martwić się o szczegóły, jak to będzie działać, a kod jest bardzo oczywisty.

Mark B.
źródło
1
To wcale nie jest skomplikowane. Domyślne parametry są wykrywane wraz z rozpoznawaniem nazw. Przestrzegają tych samych zasad.
Edward Strange
4

Jest to taki, który prawdopodobnie możesz dość dobrze zrozumieć, testując (tj. Jest to wystarczająco mainstreamowa część języka, że ​​większość kompilatorów prawie na pewno dobrze to rozumie i jeśli nie widzisz różnic między kompilatorami, ich wynik można uznać za całkiem autorytatywny).

#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;
}
Jerry Coffin
źródło
4
@GMan: [ostrożnie wyglądając niewinnie] Co przecieka? :-)
Jerry Coffin
Myślę, że odnosi się do braku wirtualnego destruktora. Ale w tym przypadku nie wycieknie.
John Dibling
1
@Jerry, destruktor jest wirtualny, jeśli usuwasz obiekt pochodny za pomocą wskaźnika klasy bazowej. W przeciwnym razie dla nich wszystkich zostanie wywołany destruktor klasy bazowej. W tym jest w porządku, ponieważ nie ma destruktora. :-)
chappar
2
@John: Początkowo nie było żadnych usunięć, o czym mówiłem. Całkowicie zignorowałem brak wirtualnego destruktora. I ... @chappar: Nie, nie jest w porządku. To musi mieć wirtualny destruktor zostać usunięty przez klasy bazowej, lub masz niezdefiniowane zachowanie. (Ten kod ma niezdefiniowane zachowanie.) Nie ma nic wspólnego z danymi lub destruktorami, które mają klasy pochodne.
GManNickG,
@Chappar: Kod pierwotnie niczego nie usunął. Chociaż jest to w większości nieistotne dla omawianego pytania, dodałem również wirtualnego dtora do klasy bazowej - z trywialnym dtorem rzadko ma to znaczenie, ale GMan jest całkowicie poprawny, bez niego kod ma UB.
Jerry Coffin
4

Jak podają inne odpowiedzi, jest to zły pomysł. Ponieważ jednak nikt nie wspomina o prostym i skutecznym rozwiązaniu, oto jest: Konwertuj parametry na strukturę, a wtedy możesz mieć wartości domyślne na składowe struktury!

Więc zamiast

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

Zrób to,

//good idea
struct Param1 {
  int x = 0, y = 0, z = 0;
};
virtual method1(const Param1& p)
Shital Shah
źródło