¿Pueden las funciones virtuales tener parámetros predeterminados?

164

Si declaro una clase base (o clase de interfaz) y especifico un valor predeterminado para uno o más de sus parámetros, ¿las clases derivadas tienen que especificar los mismos valores predeterminados y, de no ser así, qué valores predeterminados se manifestarán en las clases derivadas?

Anexo: También estoy interesado en cómo se puede manejar esto en los diferentes compiladores y cualquier aportación sobre la práctica "recomendada" en este escenario.

Arnold Spence
fuente
1
Esto parece algo fácil de probar. ¿Lo has probado?
andand
22
Estoy en el proceso de probarlo, pero no he encontrado información concreta de cómo "definió" el comportamiento, por lo que eventualmente encontraré una respuesta para mi compilador específico, pero eso no me dirá si todos los compiladores harán lo mismo cosa. También estoy interesado en la práctica recomendada.
Arnold Spence el
1
El comportamiento está bien definido, y dudo que encuentre un compilador que se equivoque (bueno, tal vez si prueba gcc 1.x, o VC ++ 1.0 o algo así). La práctica recomendada es en contra de hacer esto.
Jerry Coffin

Respuestas:

213

Los virtuales pueden tener valores predeterminados. Los valores predeterminados en la clase base no son heredados por las clases derivadas.

El valor predeterminado que se usa, es decir, la clase base 'o una clase derivada', está determinado por el tipo estático utilizado para realizar la llamada a la función. Si llama a través de un objeto de clase base, puntero o referencia, se utiliza el valor predeterminado indicado en la clase base. Por el contrario, si llama a través de un objeto de clase derivada, puntero o referencia, se utilizan los valores predeterminados indicados en la clase derivada. Hay un ejemplo debajo de la cita estándar que lo demuestra.

Algunos compiladores pueden hacer algo diferente, pero esto es lo que dicen los estándares C ++ 03 y C ++ 11:

8.3.6.10:

Una llamada a función virtual (10.3) utiliza los argumentos predeterminados en la declaración de la función virtual determinada por el tipo estático del puntero o referencia que denota el objeto. Una función de anulación en una clase derivada no adquiere argumentos predeterminados de la función que anula. Ejemplo:

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

Aquí hay un programa de muestra para demostrar qué valores predeterminados se seleccionan. Estoy usando structs aquí en lugar de classes simplemente por brevedad, classy structson exactamente iguales en casi todos los aspectos, excepto en la visibilidad predeterminada.

#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 salida de este programa (en MSVC10 y GCC 4.4) es:

Base 42
Der 42
Der 84
John Dibling
fuente
Gracias por la referencia, eso me dice el comportamiento que puedo esperar razonablemente en los compiladores (espero).
Arnold Spence el
Esta es una corrección de mi resumen anterior: aceptaré esta respuesta como referencia y mencionaré que la recomendación colectiva es que está bien tener parámetros predeterminados en funciones virtuales siempre que no cambien los parámetros predeterminados previamente especificados en un antepasado clase.
Arnold Spence el
Estoy usando gcc 4.8.1 y no obtengo un error de compilación "¡¡número incorrecto de argumentos" !!! Me llevó un día y medio encontrar el error ...
steffen
2
¿Pero hay alguna razón para eso? ¿Por qué está determinado por el tipo estático?
user1289
2
Clang-tidy trata los parámetros predeterminados en los métodos virtuales como algo no deseado y emite una advertencia al respecto: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/…
Martin Pecka
38

Este fue el tema de uno de los primeros gurús de la semana de Herb Sutter publicaciones .

Lo primero que dice sobre el tema es NO HAGAS ESO.

Con más detalle, sí, puede especificar diferentes parámetros predeterminados. No funcionarán de la misma manera que las funciones virtuales. Se llama a una función virtual en el tipo dinámico del objeto, mientras que los valores de los parámetros predeterminados se basan en el tipo estático.

Dado

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

deberías obtener A :: foo1 B :: foo2 B :: foo1

David Thornley
fuente
77
Gracias. Un "No hagas eso" de Herb Sutter tiene algo de peso.
Arnold Spence el
2
@ArnoldSpence, de hecho, Herb Sutter va más allá de esta recomendación. Él cree que una interfaz no debería contener métodos virtuales en absoluto: gotw.ca/publications/mill18.htm . Una vez que sus métodos son concretos y no pueden (no deberían) ser anulados, es seguro darles los parámetros predeterminados.
Mark Ransom
1
Creo que lo que quiso decir con "no hagas eso " fue "no cambies el valor predeterminado del parámetro predeterminado" en los métodos de anulación, no "no especifique los parámetros predeterminados en los métodos virtuales"
Weipeng L
6

Esta es una mala idea, porque los argumentos predeterminados que obtendrá dependerán del tipo estático del objeto, mientras que la virtualfunción enviada dependerá del tipo dinámico .

Es decir, cuando llama a una función con argumentos predeterminados, los argumentos predeterminados se sustituyen en tiempo de compilación, independientemente de si la función es virtualo no.

@cppcoder ofreció el siguiente ejemplo en su pregunta [cerrada] :

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

Lo que produce el siguiente resultado:

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

Con la ayuda de la explicación anterior, es fácil ver por qué. En tiempo de compilación, el compilador sustituye los argumentos predeterminados de las funciones miembro de los tipos estáticos de los punteros, haciendo que la mainfunción sea equivalente a lo siguiente:

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

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

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

Como puede ver en las otras respuestas, este es un tema complicado. En lugar de intentar hacer esto o entender lo que hace (si tiene que preguntar ahora, el responsable tendrá que preguntar o buscarlo dentro de un año).

En su lugar, cree una función pública no virtual en la clase base con parámetros predeterminados. Luego llama a una función virtual privada o protegida que no tiene parámetros predeterminados y se anula en las clases secundarias según sea necesario. Entonces no tiene que preocuparse por los detalles de cómo funcionaría y el código es muy obvio.

Mark B
fuente
1
No es para nada complicado. Los parámetros predeterminados se descubren junto con la resolución de nombre. Siguen las mismas reglas.
Edward Strange el
4

Este es uno que probablemente pueda resolver razonablemente bien mediante la prueba (es decir, es una parte del lenguaje lo suficientemente convencional como para que la mayoría de los compiladores lo entiendan correctamente y, a menos que vea diferencias entre los compiladores, su salida puede considerarse bastante autorizada)

#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
fuente
44
@GMan: [Cuidadosamente inocente] ¿Qué filtra? :-)
Jerry Coffin
Creo que se refiere a la falta de un destructor virtual. Pero en este caso no se filtrará.
John Dibling
1
@Jerry, el destructor debe ser virtual si está eliminando un objeto derivado a través del puntero de clase base. De lo contrario, se llamará al destructor de clase base para todos ellos. En esto está bien ya que no hay destructor. :-)
chappar
2
@John: Originalmente no había eliminaciones, a eso me refería. Ignore totalmente la falta de un destructor virtual. Y ... @chappar: No, no está bien. Se debe tener un destructor virtual se va a eliminar a través de una clase base, o se obtiene un comportamiento indefinido. (Este código tiene un comportamiento indefinido). No tiene nada que ver con qué datos o destructores tienen las clases derivadas.
GManNickG
@Chappar: el código originalmente no eliminó nada. Aunque es sobre todo irrelevante para la pregunta en cuestión, también he agregado un dtor virtual a la clase base: con un dtor trivial, rara vez importa, pero GMan tiene toda la razón de que sin él, el código tiene UB.
Jerry Coffin
4

Como han detallado otras respuestas, es una mala idea. Sin embargo, como nadie menciona una solución simple y efectiva, aquí está: ¡Convierta sus parámetros en estructura y luego puede tener valores predeterminados para miembros de estructura!

Entonces, en lugar de

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

hacer esto,

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