As funções virtuais podem ter parâmetros padrão?

164

Se eu declarar uma classe base (ou classe de interface) e especificar um valor padrão para um ou mais de seus parâmetros, as classes derivadas precisam especificar os mesmos padrões e, se não, quais padrões se manifestarão nas classes derivadas?

Adendo: Também estou interessado em saber como isso pode ser tratado em diferentes compiladores e em qualquer entrada na prática "recomendada" nesse cenário.

Arnold Spence
fonte
1
Parece uma coisa fácil de testar. Tentaste?
andand
22
Estou tentando fazê-lo, mas não encontrei informações concretas sobre o comportamento "definido", por isso, encontrarei uma resposta para meu compilador específico, mas isso não me dirá se todos os compiladores farão o mesmo coisa. Também estou interessado na prática recomendada.
Arnold Spence
1
O comportamento está bem definido, e duvido que você encontre um compilador que entenda errado (bem, talvez se você testar o gcc 1.x ou o VC ++ 1.0 ou algo assim). A prática recomendada é contra fazer isso.
Jerry Coffin

Respostas:

213

Os virtuais podem ter padrões. Os padrões na classe base não são herdados pelas classes derivadas.

Qual padrão é usado - ou seja, a classe base 'ou uma classe derivada' - é determinada pelo tipo estático usado para fazer a chamada para a função. Se você chamar através de um objeto, ponteiro ou referência da classe base, o padrão indicado na classe base será usado. Por outro lado, se você chamar através de um objeto de classe derivada, ponteiro ou referência serão usados ​​os padrões indicados na classe derivada. Há um exemplo abaixo da cotação padrão que demonstra isso.

Alguns compiladores podem fazer algo diferente, mas é o que dizem os padrões C ++ 03 e C ++ 11:

8.3.6.10:

Uma chamada de função virtual (10.3) usa os argumentos padrão na declaração da função virtual determinada pelo tipo estático do ponteiro ou referência que indica o objeto. Uma função de substituição em uma classe derivada não adquire argumentos padrão da função que substitui. Exemplo:

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

Aqui está um exemplo de programa para demonstrar quais padrões são selecionados. Estou usando structs aqui em vez de classes simplesmente por questões de brevidade - classe structsão exatamente iguais em quase todos os aspectos, exceto na visibilidade padrão.

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

A saída deste programa (no MSVC10 e GCC 4.4) é:

Base 42
Der 42
Der 84
John Dibling
fonte
Obrigado pela referência, que me diz o comportamento que posso razoavelmente esperar entre os compiladores (espero).
Arnold Spence
Esta é uma correção para o meu resumo anterior: vou aceitar esta resposta para referência e mencionar que a recomendação coletiva é de que não há problema em ter parâmetros padrão em funções virtuais, desde que eles não alterem os parâmetros padrão especificados anteriormente em um ancestral classe.
Arnold Spence
Estou usando o gcc 4.8.1 e não recebo um erro de compilação "número errado de argumentos" !!! Levei um dia e meio para descobrir o erro ...
Steffen
2
Mas existe alguma razão para isso? Por que é determinado pelo tipo estático?
user1289
2
Parâmetros padrão trata Clang-arrumados sobre métodos virtuais como algo indesejado e emite um aviso sobre isso: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/...
Martin Pecka
38

Este foi o tópico de um dos primeiros posts do Guru da Semana de Herb Sutter .

A primeira coisa que ele diz sobre o assunto é NÃO FAZER ISSO.

Em mais detalhes, sim, você pode especificar diferentes parâmetros padrão. Eles não funcionarão da mesma maneira que as funções virtuais. Uma função virtual é chamada no tipo dinâmico do objeto, enquanto os valores padrão dos parâmetros são baseados no 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();
}

você deve obter A :: foo1 B :: foo2 B :: foo1

David Thornley
fonte
7
Obrigado. Um "Não faça isso" de Herb Sutter carrega algum peso.
Arnold Spence
2
@ArnoldSpence, na verdade Herb Sutter vai além desta recomendação. Ele acredita que uma interface não deve conter métodos virtuais: gotw.ca/publications/mill18.htm . Uma vez que seus métodos são concretos e não podem (não deveriam) ser substituídos, é seguro fornecer parâmetros padrão.
Mark Ransom
1
Eu acredito que o que ele quis dizer com "não faça isso " foi "não altere o valor padrão do parâmetro padrão" nos métodos de substituição, não "não especifique parâmetros padrão nos métodos virtuais"
Weipeng L
6

Essa é uma péssima idéia, porque os argumentos padrão que você obtém dependerão do tipo estático do objeto, enquanto a virtualfunção despachada dependerá do tipo dinâmico .

Ou seja, quando você chama uma função com argumentos padrão, os argumentos padrão são substituídos no tempo de compilação, independentemente de a função ser virtualou não.

@cppcoder ofereceu o seguinte exemplo em sua pergunta [fechada] :

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

Que produz a seguinte saída:

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

Com a ajuda da explicação acima, é fácil entender o porquê. No momento da compilação, o compilador substitui os argumentos padrão das funções de membro dos tipos estáticos dos ponteiros, tornando a mainfunção equivalente ao seguinte:

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

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

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

Como você pode ver nas outras respostas, este é um assunto complicado. Em vez de tentar fazer isso ou entender o que faz (se você precisar perguntar agora, o mantenedor terá que perguntar ou procurar daqui a um ano).

Em vez disso, crie uma função pública não virtual na classe base com parâmetros padrão. Em seguida, chama uma função virtual privada ou protegida que não possui parâmetros padrão e é substituída nas classes filho, conforme necessário. Então você não precisa se preocupar com os detalhes de como isso funcionaria e o código é muito óbvio.

Mark B
fonte
1
Não é nada complicado. Os parâmetros padrão são descobertos junto com a resolução de nomes. Eles seguem as mesmas regras.
Edward Strange
4

Essa é uma prova que você provavelmente pode descobrir razoavelmente bem testando (ou seja, é uma parte suficientemente mainstream da linguagem que a maioria dos compiladores quase certamente acerta e, a menos que você veja diferenças entre os compiladores, sua saída pode ser considerada bastante autoritativa).

#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
fonte
4
@GMan: [Cuidadosamente parecendo inocente] O que vaza? :-)
Jerry Coffin
Eu acho que ele está se referindo à falta de um destruidor virtual. Mas neste caso, não vai vazar.
precisa saber é o seguinte
1
@Jerry, o destruidor será virtual se você estiver excluindo objetos derivados através do ponteiro da classe base. Caso contrário, o destruidor da classe base será chamado para todos eles. Nisto está ok, pois não há destruidor. :-)
chappar
2
@ John: Originalmente não havia exclusões, que é o que eu estava me referindo. Ignorei totalmente a falta de um destruidor virtual. E ... @chappar: Não, não está tudo bem. Ele deve ter um destruidor virtual para ser excluído por meio de uma classe base, ou você terá um comportamento indefinido. (Esse código tem um comportamento indefinido.) Não tem nada a ver com quais dados ou destruidores as classes derivadas possuem.
precisa saber é o seguinte
@ Chappar: O código originalmente não excluiu nada. Embora seja principalmente irrelevante para a pergunta em questão, também adicionei um dtor virtual à classe base - com um dtor trivial, isso raramente importa, mas GMan está totalmente correto de que, sem ele, o código tem UB.
Jerry Coffin
4

Como outras respostas detalharam, é uma má ideia. No entanto, como ninguém menciona uma solução simples e eficaz, aqui está: Converta seus parâmetros em struct e, em seguida, você poderá ter valores padrão para membros de struct!

Então, ao invés de,

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

faça isso,

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