Können virtuelle Funktionen Standardparameter haben?

164

Wenn ich eine Basisklasse (oder Schnittstellenklasse) deklariere und einen Standardwert für einen oder mehrere ihrer Parameter spezifiziere, müssen die abgeleiteten Klassen dieselben Standardwerte angeben, und wenn nicht, welche Standardwerte werden in den abgeleiteten Klassen angezeigt?

Nachtrag: Ich bin auch daran interessiert, wie dies über verschiedene Compiler hinweg gehandhabt werden kann und welche Eingaben zur "empfohlenen" Praxis in diesem Szenario vorliegen.

Arnold Spence
quelle
1
Dies scheint leicht zu testen zu sein. Hast du es versucht?
undund
22
Ich bin gerade dabei, es zu versuchen, aber ich habe keine konkreten Informationen darüber gefunden, wie "definiert" das Verhalten sein würde, sodass ich irgendwann eine Antwort für meinen spezifischen Compiler finden werde, aber das sagt mir nicht, ob alle Compiler dasselbe tun werden Ding. Ich interessiere mich auch für empfohlene Praxis.
Arnold Spence
1
Das Verhalten ist gut definiert, und ich bezweifle, dass Sie einen Compiler finden, der es falsch macht (naja, vielleicht, wenn Sie gcc 1.x oder VC ++ 1.0 oder ähnliches testen). Die empfohlene Vorgehensweise ist dagegen.
Jerry Coffin

Antworten:

212

Virtuals haben möglicherweise Standardeinstellungen. Die Standardeinstellungen in der Basisklasse werden nicht von abgeleiteten Klassen geerbt.

Welche Standardeinstellung verwendet wird, dh die Basisklasse 'oder eine abgeleitete Klasse', wird durch den statischen Typ bestimmt, der zum Aufrufen der Funktion verwendet wird. Wenn Sie ein Basisklassenobjekt, einen Zeiger oder eine Referenz aufrufen, wird der in der Basisklasse angegebene Standard verwendet. Wenn Sie dagegen ein abgeleitetes Klassenobjekt, einen Zeiger oder eine Referenz aufrufen, werden die in der abgeleiteten Klasse angegebenen Standardwerte verwendet. Unter dem Standardzitat befindet sich ein Beispiel, das dies demonstriert.

Einige Compiler machen möglicherweise etwas anderes, aber dies ist, was die C ++ 03- und C ++ 11-Standards sagen:

8.3.6.10:

Ein virtueller Funktionsaufruf (10.3) verwendet die Standardargumente in der Deklaration der virtuellen Funktion, die durch den statischen Typ des Zeigers oder der Referenz bestimmt wird, die das Objekt bezeichnen. Eine überschreibende Funktion in einer abgeleiteten Klasse erhält keine Standardargumente von der Funktion, die sie überschreibt. Beispiel:

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

Hier ist ein Beispielprogramm, um zu demonstrieren, welche Standardeinstellungen übernommen wurden. Ich verwende structhier eher s als classnur der Kürze halber - classund bin structin fast jeder Hinsicht genau gleich, mit Ausnahme der Standardsichtbarkeit.

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

Die Ausgabe dieses Programms (unter MSVC10 und GCC 4.4) lautet:

Base 42
Der 42
Der 84
John Dibling
quelle
Vielen Dank für die Referenz, die mir das Verhalten zeigt, das ich bei Compilern vernünftigerweise erwarten kann (hoffe ich).
Arnold Spence
Dies ist eine Korrektur meiner vorherigen Zusammenfassung: Ich werde diese Antwort als Referenz akzeptieren und erwähnen, dass die kollektive Empfehlung lautet, dass es in Ordnung ist, Standardparameter in virtuellen Funktionen zu haben, solange sie nicht die zuvor in einem Vorfahren angegebenen Standardparameter ändern Klasse.
Arnold Spence
Ich benutze gcc 4.8.1 und bekomme keinen Kompilierungsfehler "falsche Anzahl von Argumenten" !!! Ich habe anderthalb Tage
gebraucht
2
Aber gibt es dafür einen Grund? Warum wird es durch den statischen Typ bestimmt?
user1289
2
Clang-tidy behandelt Standardparameter für virtuelle Methoden als unerwünscht und gibt eine Warnung dazu aus: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/…
Martin Pecka
38

Dies war das Thema eines der frühen Guru of the Week- Beiträge von Herb Sutter .

Das erste, was er zu diesem Thema sagt, ist, das nicht zu tun.

Ja, Sie können verschiedene Standardparameter angeben. Sie funktionieren nicht wie die virtuellen Funktionen. Für den dynamischen Typ des Objekts wird eine virtuelle Funktion aufgerufen, während die Standardparameterwerte auf dem statischen Typ basieren.

Gegeben

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

Sie sollten A :: foo1 B :: foo2 B :: foo1 erhalten

David Thornley
quelle
7
Vielen Dank. Ein "Tu das nicht" von Herb Sutter hat etwas Gewicht.
Arnold Spence
2
@ArnoldSpence, tatsächlich geht Herb Sutter über diese Empfehlung hinaus. Er glaubt, dass eine Schnittstelle überhaupt keine virtuellen Methoden enthalten sollte: gotw.ca/publications/mill18.htm . Sobald Ihre Methoden konkret sind und nicht überschrieben werden können (sollten), können Sie ihnen sicher Standardparameter geben.
Mark Ransom
1
Ich glaube, was er mit "nicht tun " meinte , war "den Standardwert des Standardparameters nicht ändern" in überschreibenden Methoden, nicht "Standardparameter in virtuellen Methoden nicht angeben"
Weipeng L
6

Dies ist eine schlechte Idee, da die Standardargumente, die Sie erhalten, vom statischen Typ des Objekts virtualabhängen , während die Funktion, an die gesendet wird, vom dynamischen Typ abhängt .

Das heißt, wenn Sie eine Funktion mit Standardargumenten aufrufen, werden die Standardargumente beim Kompilieren ersetzt, unabhängig davon, ob es sich um eine Funktion handelt virtualoder nicht.

@cppcoder hat in seiner [geschlossenen] Frage das folgende Beispiel angeboten :

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

Welches erzeugt die folgende Ausgabe:

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

Anhand der obigen Erklärung ist leicht zu erkennen, warum. Zur Kompilierungszeit ersetzt der Compiler die Standardargumente aus den Elementfunktionen der statischen Typen der Zeiger, sodass die mainFunktion der folgenden entspricht:

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

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

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

Wie Sie den anderen Antworten entnehmen können, ist dies ein kompliziertes Thema. Anstatt dies zu versuchen oder zu verstehen, was es tut (wenn Sie jetzt fragen müssen, muss der Betreuer in einem Jahr danach fragen oder nachschlagen).

Erstellen Sie stattdessen eine öffentliche nicht virtuelle Funktion in der Basisklasse mit Standardparametern. Anschließend wird eine private oder geschützte virtuelle Funktion aufgerufen, die keine Standardparameter hat und bei Bedarf in untergeordneten Klassen überschrieben wird. Dann müssen Sie sich keine Gedanken darüber machen, wie es funktionieren würde, und der Code ist sehr offensichtlich.

Mark B.
quelle
1
Es ist überhaupt nicht kompliziert. Standardparameter werden zusammen mit der Namensauflösung ermittelt. Sie folgen den gleichen Regeln.
Edward Strange
4

Dies ist eine, die Sie wahrscheinlich durch Testen ziemlich gut herausfinden können (dh es ist ein ausreichend allgemeiner Teil der Sprache, dass die meisten Compiler es mit ziemlicher Sicherheit richtig machen, und wenn Sie keine Unterschiede zwischen Compilern sehen, kann ihre Ausgabe als ziemlich maßgeblich angesehen werden).

#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 Sarg
quelle
4
@ GMan: [Sorgfältig unschuldig aussehend] Was leckt? :-)
Jerry Coffin
Ich denke, er bezieht sich auf das Fehlen eines virtuellen Destruktors. In diesem Fall tritt jedoch kein Leck aus.
John Dibling
1
@ Jerry, der Destruktor ist virtuell, wenn Sie das abgeleitete Objekt über den Basisklassenzeiger löschen. Andernfalls wird der Basisklassen-Destruktor für alle aufgerufen. Dies ist in Ordnung, da es keinen Destruktor gibt. :-)
Chappar
2
@ John: Ursprünglich gab es keine Löschungen, worauf ich mich bezog. Ich habe das Fehlen eines virtuellen Destruktors völlig ignoriert. Und ... @chappar: Nein, es ist nicht in Ordnung. Es muss einen virtuellen Destruktor haben, um über eine Basisklasse gelöscht zu werden, sonst erhalten Sie undefiniertes Verhalten. (Dieser Code hat ein undefiniertes Verhalten.) Er hat nichts mit den Daten oder Destruktoren der abgeleiteten Klassen zu tun.
GManNickG
@Chappar: Der Code hat ursprünglich nichts gelöscht. Obwohl es für die vorliegende Frage größtenteils irrelevant ist, habe ich der Basisklasse auch einen virtuellen Dtor hinzugefügt - mit einem trivialen Dtor spielt es selten eine Rolle, aber GMan ist völlig richtig, dass der Code ohne ihn UB hat.
Jerry Coffin
4

Wie andere Antworten ausführlich dargelegt haben, ist es eine schlechte Idee. Da jedoch niemand eine einfache und effektive Lösung erwähnt, ist dies hier: Konvertieren Sie Ihre Parameter in struct und dann können Sie Standardwerte für struct-Mitglieder festlegen!

Also statt

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

mach das,

//good idea
struct Param1 {
  int x = 0, y = 0, z = 0;
};
virtual method1(const Param1& p)
Shital Shah
quelle