Les fonctions virtuelles peuvent-elles avoir des paramètres par défaut?

164

Si je déclare une classe de base (ou une classe d'interface) et spécifie une valeur par défaut pour un ou plusieurs de ses paramètres, les classes dérivées doivent-elles spécifier les mêmes valeurs par défaut et si ce n'est pas le cas, quelles valeurs par défaut se manifesteront dans les classes dérivées?

Addendum: Je suis également intéressé par la manière dont cela peut être géré entre différents compilateurs et par toute entrée sur la pratique «recommandée» dans ce scénario.

Arnold Spence
la source
1
Cela semble une chose facile à tester. L'as tu essayé?
et
22
Je suis en train de l'essayer mais je n'ai pas trouvé d'informations concrètes sur la façon dont le comportement serait "défini", donc je trouverai éventuellement une réponse pour mon compilateur spécifique mais cela ne me dira pas si tous les compilateurs feront de même chose. Je suis également intéressé par la pratique recommandée.
Arnold Spence
1
Le comportement est bien défini, et je doute que vous trouviez un compilateur qui se trompe (enfin, peut-être si vous testez gcc 1.x, ou VC ++ 1.0 ou quelque chose comme ça). La pratique recommandée est contre le faire.
Jerry Coffin

Réponses:

213

Les virtuels peuvent avoir des valeurs par défaut. Les valeurs par défaut de la classe de base ne sont pas héritées par les classes dérivées.

La valeur par défaut utilisée - c'est-à-dire la classe de base «ou une classe dérivée» - est déterminée par le type statique utilisé pour effectuer l'appel à la fonction. Si vous appelez via un objet de classe de base, un pointeur ou une référence, la valeur par défaut indiquée dans la classe de base est utilisée. Inversement, si vous appelez via un objet de classe dérivée, un pointeur ou une référence, les valeurs par défaut indiquées dans la classe dérivée sont utilisées. Il y a un exemple sous la citation standard qui le démontre.

Certains compilateurs peuvent faire quelque chose de différent, mais c'est ce que disent les normes C ++ 03 et C ++ 11:

8.3.6.10:

Un appel de fonction virtuelle (10.3) utilise les arguments par défaut dans la déclaration de la fonction virtuelle déterminée par le type statique du pointeur ou de la référence désignant l'objet. Une fonction de substitution dans une classe dérivée n'acquiert pas les arguments par défaut de la fonction qu'elle remplace. Exemple:

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

Voici un exemple de programme pour montrer quelles valeurs par défaut sont sélectionnées. J'utilise structs ici plutôt que classes simplement par souci de concision - classet structsont exactement les mêmes dans presque tous les sens, sauf la visibilité par défaut.

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

La sortie de ce programme (sur MSVC10 et GCC 4.4) est:

Base 42
Der 42
Der 84
John Dibling
la source
Merci pour la référence, cela me dit le comportement auquel je peux raisonnablement m'attendre entre les compilateurs (j'espère).
Arnold Spence
Ceci est une correction à mon résumé précédent: j'accepterai cette réponse pour la référence et mentionnerai que la recommandation collective est qu'il est correct d'avoir des paramètres par défaut dans les fonctions virtuelles tant qu'ils ne changent pas les paramètres par défaut précédemment spécifiés dans un ancêtre classe.
Arnold Spence
J'utilise gcc 4.8.1 et je n'obtiens pas d'erreur de compilation "mauvais nombre d'arguments" !!! Il m'a fallu un jour et demi pour trouver le bogue ...
steffen
2
Mais y a-t-il une raison à cela? Pourquoi est-il déterminé par le type statique?
user1289
2
Clang-tidy traite les paramètres par défaut des méthodes virtuelles comme quelque chose de indésirable et émet un avertissement à ce sujet: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/…
Martin Pecka
38

C'était le sujet de l'un des premiers messages de Herb Sutter sur le gourou de la semaine .

La première chose qu'il dit sur le sujet est NE FAITES PAS CELA.

Plus en détail, oui, vous pouvez spécifier différents paramètres par défaut. Ils ne fonctionneront pas de la même manière que les fonctions virtuelles. Une fonction virtuelle est appelée sur le type dynamique de l'objet, tandis que les valeurs de paramètre par défaut sont basées sur le type statique.

Donné

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

vous devriez obtenir A :: foo1 B :: foo2 B :: foo1

David Thornley
la source
7
Merci. Un "Ne faites pas ça" de Herb Sutter a un certain poids.
Arnold Spence
2
@ArnoldSpence, en fait Herb Sutter va au-delà de cette recommandation. Il croit qu'une interface ne devrait pas du tout contenir de méthodes virtuelles: gotw.ca/publications/mill18.htm . Une fois que vos méthodes sont concrètes et ne peuvent (ne devraient pas) être remplacées, il est prudent de leur donner des paramètres par défaut.
Mark Ransom
1
Je crois que ce qu'il voulait dire par "ne faites pas ça " était "ne changez pas la valeur par défaut du paramètre par défaut" dans les méthodes de remplacement, pas "ne spécifiez pas les paramètres par défaut dans les méthodes virtuelles"
Weipeng L
6

C'est une mauvaise idée, car les arguments par défaut que vous obtenez dépendront du type statique de l'objet, tandis que la virtualfonction distribuée dépendra du type dynamique .

Autrement dit, lorsque vous appelez une fonction avec des arguments par défaut, les arguments par défaut sont substitués au moment de la compilation, que la fonction le soit virtualou non.

@cppcoder a donné l'exemple suivant dans son [clos] question :

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

Ce qui produit la sortie suivante:

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

À l'aide de l'explication ci-dessus, il est facile de comprendre pourquoi. Au moment de la compilation, le compilateur remplace les arguments par défaut des fonctions membres des types statiques des pointeurs, ce qui rend la mainfonction équivalente à ce qui suit:

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

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

    B* bb = new B();
    bb->display(9);
Oktaliste
la source
4

Comme vous pouvez le voir dans les autres réponses, c'est un sujet compliqué. Au lieu d'essayer de le faire ou de comprendre ce qu'il fait (si vous devez demander maintenant, le responsable devra le demander ou le rechercher dans un an).

Au lieu de cela, créez une fonction publique non virtuelle dans la classe de base avec les paramètres par défaut. Ensuite, il appelle une fonction virtuelle privée ou protégée qui n'a pas de paramètres par défaut et est remplacée dans les classes enfants si nécessaire. Ensuite, vous n'avez pas à vous soucier des détails de la façon dont cela fonctionnerait et le code est très évident.

Marque B
la source
1
Ce n'est pas du tout compliqué. Les paramètres par défaut sont découverts avec la résolution des noms. Ils suivent les mêmes règles.
Edward Strange
4

C'est une partie que vous pouvez probablement comprendre raisonnablement bien en testant (c'est-à-dire que c'est une partie suffisamment courante du langage pour que la plupart des compilateurs y parviennent presque certainement et à moins que vous ne voyiez des différences entre les compilateurs, leur sortie peut être considérée comme faisant assez bien autorité).

#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
la source
4
@GMan: [regardant attentivement innocent] Quelles fuites? :-)
Jerry Coffin
Je pense qu'il fait référence à l'absence de destructeur virtuel. Mais dans ce cas, il ne fuira pas.
John Dibling
1
@Jerry, le destructeur doit être virtuel si vous supprimez un objet dérivé via un pointeur de classe de base. Sinon, le destructeur de classe de base sera appelé pour tous. En cela, c'est correct car il n'y a pas de destructeur. :-)
chappar
2
@John: À l'origine, il n'y avait pas de suppression, c'est ce à quoi je faisais référence. J'ai totalement ignoré l'absence de destructeur virtuel. Et ... @chappar: Non, ça ne va pas. Il doit avoir un destructeur virtuel à supprimer via une classe de base, sinon vous obtenez un comportement indéfini. (Ce code a un comportement non défini.) Il n'a rien à voir avec les données ou les destructeurs des classes dérivées.
GManNickG
@Chappar: Le code à l'origine n'a rien supprimé. Bien que cela ne soit généralement pas pertinent pour la question à l'étude, j'ai également ajouté un dtor virtuel à la classe de base - avec un dtor trivial, cela compte rarement, mais GMan a tout à fait raison de dire que sans lui, le code a UB.
Jerry Coffin
4

Comme d'autres réponses l'ont détaillé, c'est une mauvaise idée. Cependant, puisque personne ne mentionne une solution simple et efficace, la voici: Convertissez vos paramètres en struct et vous pourrez alors avoir des valeurs par défaut pour les membres de struct!

Donc au lieu de,

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

fais ça,

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