Palavra-chave “virtual” em C ++ para funções em classes derivadas. Isso é necessário?

221

Com a definição de estrutura dada abaixo ...

struct A {
    virtual void hello() = 0;
};

Abordagem # 1:

struct B : public A {
    virtual void hello() { ... }
};

Abordagem # 2:

struct B : public A {
    void hello() { ... }
};

Existe alguma diferença entre essas duas maneiras de substituir a função hello?

Anarki
fonte
65
No C ++ 11, você pode escrever "void hello () override {}" para declarar explicitamente que está substituindo um método virtual. O compilador falhará se não existir um método virtual básico e tiver a mesma legibilidade que colocar "virtual" na classe descendente.
ShadowChaser
Na verdade, no C ++ 11 do gcc, escrever a substituição de void hello () {} na classe derivada é bom porque a classe base especificou que o método hello () é virtual. Em outras palavras, o uso da palavra virtual na classe derivada não é necessário / obrigatório, para o gcc / g ++ de qualquer maneira. (Estou usando a versão 4.9.2 do gcc em um RPi 3) Mas é uma boa prática incluir a palavra-chave virtual no método da classe derivada de qualquer maneira.
Will

Respostas:

183

Eles são exatamente os mesmos. Não há diferença entre eles, a não ser que a primeira abordagem exija mais digitação e seja potencialmente mais clara.

James McNellis
fonte
25
Isso é verdade, mas o Mozilla C ++ Portability Guide recomenda sempre o uso virtual, porque "alguns compiladores" emitem avisos se você não o fizer. Pena que eles não mencionam exemplos de tais compiladores.
Sergei Tachenov 04/02
5
Eu também acrescentaria que marcá-lo explicitamente como virtual ajudará a lembrá-lo de tornar o destruidor virtual também.
Lfalin
1
Apenas para mencionar, mesmo aplicável a destruição virtual
Atul
6
@SergeyTachenov de acordo com o comentário de clifford à sua própria resposta , o exemplo de tais compiladores é o armcc.
Ruslan
4
@Rasmi, o novo guia de portabilidade está aqui , mas agora recomenda o uso da overridepalavra - chave.
Sergei Tachenov 14/09/16
83

A 'virtualidade' de uma função é propagada implicitamente; no entanto, pelo menos um compilador que eu uso gera um aviso se a virtualpalavra-chave não for usada explicitamente; portanto, convém usá-la apenas para manter o compilador quieto.

De um ponto de vista puramente estilístico, a inclusão da virtualpalavra - chave claramente 'anuncia' o fato ao usuário de que a função é virtual. Isso será importante para qualquer outra subclasse B sem ter que verificar a definição de A. Para hierarquias profundas de classe, isso se torna especialmente importante.

Clifford
fonte
12
Que compilador é esse?
James McNellis
35
@ James: armcc (compilador do ARM para dispositivos ARM)
Clifford
55

A virtualpalavra-chave não é necessária na classe derivada. Aqui está a documentação de suporte, do C ++ Draft Standard (N3337) (ênfase minha):

10.3 Funções virtuais

2 Se uma função de membro virtual vffor declarada em uma classe Basee em uma classe Derived, derivada direta ou indiretamente de Base, uma função de membro vfcom o mesmo nome, lista de tipos de parâmetro (8.3.5), qualificação cv e qualificador ref ( ou ausência do mesmo) que Base::vfé declarado, então Derived::vftambém é virtual ( independentemente de ser ou não declarado ) e substituiBase::vf .

R Sahu
fonte
5
Esta é de longe a melhor resposta aqui.
Fantastic Mr Fox
33

Não, a virtualpalavra-chave nas substituições de funções virtuais das classes derivadas não é necessária. Mas vale a pena mencionar uma armadilha relacionada: uma falha em substituir uma função virtual.

A falha na substituição ocorre se você pretende substituir uma função virtual em uma classe derivada, mas comete um erro na assinatura para que ela declare uma função virtual nova e diferente. Essa função pode ser uma sobrecarga da função de classe base ou pode ter um nome diferente. Independentemente de você usar ou não a virtualpalavra - chave na declaração da função de classe derivada, o compilador não poderá dizer que você deseja substituir uma função de uma classe base.

Esta armadilha é, no entanto, felizmente resolvida pelo recurso de linguagem de substituição explícita do C ++ 11 , que permite ao código fonte especificar claramente que uma função de membro se destina a substituir uma função de classe base:

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

O compilador emitirá um erro em tempo de compilação e o erro de programação será imediatamente óbvio (talvez a função em Derivado deva ter tomado a floatcomo argumento).

Consulte WP: C ++ 11 .

Colin D Bennett
fonte
11

Adicionar a palavra-chave "virtual" é uma boa prática, pois melhora a legibilidade, mas não é necessário. As funções declaradas virtuais na classe base e com a mesma assinatura nas classes derivadas são consideradas "virtuais" por padrão.

Sujay Ghosh
fonte
7

Não há diferença para o compilador, quando você escreve o virtual na classe derivada ou o omite.

Mas você precisa olhar para a classe base para obter essas informações. Portanto, eu recomendaria adicionar a virtualpalavra - chave também na classe derivada, se você quiser mostrar ao ser humano que essa função é virtual.

harper
fonte
2

Há uma diferença considerável quando você tem modelos e começa a tomar as classes base como parâmetro (s) do modelo:

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

A parte divertida disso é que agora você pode definir funções de interface e não interface posteriormente para definir classes. Isso é útil para interfaces de trabalho entre bibliotecas (não confie nisso como um processo de design padrão de uma única biblioteca). Não custa nada permitir isso para todas as suas aulas - você pode atétypedef usar B se quiser.

Observe que, se você fizer isso, também poderá declarar copiar / mover construtores como modelos: permitir construir a partir de interfaces diferentes permite 'transmitir' entre diferentes B<> tipos .

É questionável se você deve adicionar suporte para o const A&in t_hello(). O motivo usual para essa reescrita é deixar a especialização baseada em herança para a baseada em modelo, principalmente por razões de desempenho. Se você continuar suportando a interface antiga, dificilmente poderá detectar (ou impedir) o uso antigo.

lorro
fonte
1

A virtualpalavra-chave deve ser adicionada às funções de uma classe base para torná-las substituíveis. No seu exemplo, struct Aé a classe base. virtualnão significa nada para usar essas funções em uma classe derivada. No entanto, se você deseja que sua classe derivada também seja uma classe base, e deseja que essa função seja substituível, será necessário colocá-la virtuallá.

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

Aqui Cherda de B, portanto, Bnão é a classe base (também é uma classe derivada) e Cé a classe derivada. O diagrama de herança é assim:

A
^
|
B
^
|
C

Portanto, você deve colocar as virtualfunções na frente das possíveis classes base que podem ter filhos. virtualpermite que seus filhos substituam suas funções. Não há nada errado em colocar ovirtual funções na frente das classes derivadas, mas isso não é obrigatório. No entanto, é recomendável, porque se alguém quiser herdar da classe derivada, não ficará satisfeito com o fato de a substituição do método não funcionar conforme o esperado.

Portanto, coloque-se virtualà frente das funções em todas as classes envolvidas na herança, a menos que você tenha certeza de que a classe não terá filhos que precisem substituir as funções da classe base. É uma boa prática.

Galáxia
fonte
0

Certamente incluirei a palavra-chave Virtual para a classe filho, porque

  • Eu. Legibilidade.
  • ii. Essa classe filho pode ser derivada mais abaixo, você não deseja que o construtor da classe derivada adicional chame essa função virtual.
user2264698
fonte
1
Acho que ele quer dizer que, sem marcar a função filho como virtual, um programador que deriva da classe filho mais tarde pode não perceber que a função é realmente virtual (porque nunca olhou para a classe base) e pode chamá-la potencialmente durante a construção ( que pode ou não fazer a coisa certa).
PfhorSlayer