Le funzioni virtuali possono avere parametri predefiniti?


164

Se dichiaro una classe di base (o classe di interfaccia) e specifico un valore predefinito per uno o più dei suoi parametri, le classi derivate devono specificare gli stessi valori predefiniti e, in caso contrario, quali valori predefiniti si manifesteranno nelle classi derivate?

Addendum: Sono anche interessato a come questo può essere gestito attraverso diversi compilatori e qualsiasi input sulla pratica "raccomandata" in questo scenario.


1
Sembra una cosa facile da testare. L'hai provato?
andand

22
Sto provando, ma non ho trovato informazioni concrete su come "definito" sarebbe il comportamento, quindi alla fine troverò una risposta per il mio compilatore specifico, ma ciò non mi dirà se tutti i compilatori faranno lo stesso cosa. Sono anche interessato alla pratica raccomandata.
Arnold Spence,

1
Il comportamento è ben definito e dubito che troverai un compilatore che lo sbaglia (bene, forse se testerai gcc 1.x o VC ++ 1.0 o qualcosa del genere). La pratica raccomandata è contraria a questo.
Jerry Coffin,

Risposte:


213

I virtuali possono avere valori predefiniti. I valori predefiniti nella classe base non sono ereditati dalle classi derivate.

Quale valore predefinito viene utilizzato, ovvero la classe base 'o una classe derivata' - è determinato dal tipo statico utilizzato per effettuare la chiamata alla funzione. Se si chiama un oggetto, un puntatore o un riferimento della classe base, viene utilizzato il valore predefinito indicato nella classe base. Al contrario, se si chiama attraverso un oggetto della classe derivata, vengono utilizzati il ​​puntatore o il riferimento ai valori predefiniti indicati nella classe derivata. C'è un esempio sotto la citazione standard che lo dimostra.

Alcuni compilatori possono fare qualcosa di diverso, ma questo è ciò che dicono gli standard C ++ 03 e C ++ 11:

8.3.6.10:

Una chiamata di funzione virtuale (10.3) utilizza gli argomenti predefiniti nella dichiarazione della funzione virtuale determinata dal tipo statico del puntatore o riferimento che indica l'oggetto. Una funzione di override in una classe derivata non acquisisce argomenti predefiniti dalla funzione che sovrascrive. Esempio:

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

Ecco un programma di esempio per dimostrare quali impostazioni predefinite vengono rilevate. Sto usando structs qui piuttosto che classes semplicemente per brevità - classe structsono esattamente gli stessi in quasi tutti i modi tranne la visibilità predefinita.

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

L'output di questo programma (su MSVC10 e GCC 4.4) è:

Base 42
Der 42
Der 84

Grazie per il riferimento, che mi dice il comportamento che posso ragionevolmente aspettarmi tra i compilatori (spero).
Arnold Spence,

Questa è una correzione al mio precedente riassunto: accetterò questa risposta come riferimento e menzionerò che la raccomandazione collettiva è che è ok avere parametri predefiniti nelle funzioni virtuali purché non modifichino parametri predefiniti precedentemente specificati in un antenato classe.
Arnold Spence,

Sto usando gcc 4.8.1 e non ottengo un errore di compilazione "numero errato di argomenti" !!! Mi ci sono voluti un giorno e mezzo per trovare il bug ...
Steffen,

2
Ma c'è qualche motivo per questo? Perché è determinato dal tipo statico?
user1289

2
Clang-tidy considera i parametri predefiniti sui metodi virtuali come qualcosa di indesiderato e lancia un avvertimento al riguardo: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/…
Martin Pecka,

38

Questo è stato l'argomento di uno dei primi post di Guru of the Week di Herb Sutter .

La prima cosa che dice sull'argomento è NON FARLO.

Più in dettaglio, sì, è possibile specificare diversi parametri predefiniti. Non funzioneranno allo stesso modo delle funzioni virtuali. Una funzione virtuale viene chiamata sul tipo dinamico dell'oggetto, mentre i valori dei parametri predefiniti si basano sul tipo statico.

Dato

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

dovresti ottenere A :: foo1 B :: foo2 B :: foo1


7
Grazie. Un "Non farlo" di Herb Sutter ha un certo peso.
Arnold Spence,

2
@ArnoldSpence, infatti Herb Sutter va oltre questa raccomandazione. Crede che un'interfaccia non dovrebbe contenere affatto metodi virtuali: gotw.ca/publications/mill18.htm . Una volta che i tuoi metodi sono concreti e non possono (non dovrebbero) essere ignorati, è sicuro fornire loro i parametri predefiniti.
Mark Ransom,

1
Credo che ciò che egli intende per "non fare che " era "non modificare il valore predefinito del parametro di default" in sovrascrivendo i metodi, non "non specificare i parametri di default nei metodi virtuali"
Weipeng L

6

Questa è una cattiva idea, perché gli argomenti predefiniti che otterrai dipenderanno dal tipo statico dell'oggetto, mentre la virtualfunzione inviata dipenderà dal tipo dinamico .

Vale a dire, quando si chiama una funzione con argomenti predefiniti, gli argomenti predefiniti vengono sostituiti in fase di compilazione, indipendentemente dal fatto che la funzione lo sia virtualo meno.

@cppcoder ha offerto il seguente esempio nella sua domanda [chiusa] :

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

Che produce il seguente output:

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

Con l'aiuto della spiegazione sopra, è facile capire perché. Al momento della compilazione, il compilatore sostituisce gli argomenti predefiniti dalle funzioni membro dei tipi statici dei puntatori, rendendo la mainfunzione equivalente alla seguente:

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

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

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

4

Come puoi vedere dalle altre risposte, questo è un argomento complicato. Invece di provare a fare questo o capire cosa fa (se devi chiedere ora, il manutentore dovrà chiedere o cercare tra un anno).

Invece, crea una funzione pubblica non virtuale nella classe base con parametri predefiniti. Quindi chiama una funzione virtuale privata o protetta che non ha parametri predefiniti e viene sovrascritta nelle classi figlio secondo necessità. Quindi non devi preoccuparti dei dettagli di come funzionerebbe e il codice è molto ovvio.


1
Non è affatto complicato. I parametri predefiniti vengono rilevati insieme alla risoluzione dei nomi. Seguono le stesse regole.
Edward Strange,

4

Questo è uno che puoi probabilmente capire abbastanza bene testando (cioè, è una parte sufficientemente tradizionale del linguaggio che la maggior parte dei compilatori quasi sicuramente capisce bene e, a meno che tu non veda differenze tra i compilatori, il loro output può essere considerato abbastanza autorevole).

#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: [Guardando con attenzione innocente] Che perdite? :-)
Jerry Coffin

Penso che si riferisca alla mancanza di un distruttore virtuale. Ma in questo caso non colerà.
John Dibling,

1
@Jerry, il distruttore è virtuale se si sta eliminando l'oggetto derivato tramite il puntatore della classe base. Altrimenti verrà chiamato il distruttore della classe base per tutti. In questo va bene in quanto non esiste un distruttore. :-)
Chappar

2
@John: Inizialmente non c'erano cancellazioni, che è ciò a cui mi riferivo. Ho completamente ignorato la mancanza di un distruttore virtuale. E ... @chappar: No, non va bene. Si deve avere un distruttore virtuale da eliminare attraverso una classe base, o si ottiene un comportamento indefinito. (Questo codice ha un comportamento indefinito.) Non ha nulla a che fare con quali dati o distruttori hanno le classi derivate.
GManNickG,

@Chappar: il codice inizialmente non cancellava nulla. Sebbene sia per lo più irrilevante per la domanda in corso, ho anche aggiunto un dtor virtuale alla classe base - con un banale dtor, raramente importa, ma GMan ha perfettamente ragione che senza di esso, il codice ha UB.
Jerry Coffin,

4

Come altre risposte hanno dettagliato, è una cattiva idea. Tuttavia, poiché nessuno menziona una soluzione semplice ed efficace, eccola qui: converti i tuoi parametri in struct e quindi puoi avere i valori predefiniti in struct struct!

Quindi invece di

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

Fai questo,

//good idea
struct Param1 {
  int x = 0, y = 0, z = 0;
};
virtual method1(const Param1& p)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.