Em C ++, quando devo usar final na declaração de método virtual?


11

Eu sei que a finalpalavra-chave é usada para impedir que o método virtual seja substituído por classes derivadas. No entanto, não consigo encontrar nenhum exemplo útil quando realmente devo usar a finalpalavra-chave com o virtualmétodo Ainda mais, parece que o uso de finalmétodos virtuais é um mau cheiro, pois impede que os programadores estendam a classe no futuro.

Minha pergunta é a seguinte:

Existe algum caso útil quando eu realmente devo usar finalna virtualdeclaração de método?


Pelas mesmas razões que os métodos Java são finalizados?
Ordous 7/07

Em Java, todos os métodos são virtuais. O método final na classe base pode melhorar o desempenho. O C ++ possui métodos não virtuais, portanto não é um caso para essa linguagem. Você conhece outros bons casos? Eu gostaria de ouvi-los. :)
metamaker

Acho que não sou muito bom em explicar as coisas - estava tentando dizer exatamente a mesma coisa que Mike Nakis.
Ordous 07/07

Respostas:


12

Uma recapitulação do que a finalpalavra - chave faz: digamos que temos classe base Ae classe derivada B. A função f()pode ser declarada em Acomo virtual, significando que a classe Bpode substituí-la. Mas então a classe Bpode desejar que qualquer classe da qual seja derivada posteriormente Bnão possa substituí-la f(). É quando precisamos declarar f()como finalem B. Sem a finalpalavra - chave, uma vez que definimos um método como virtual, qualquer classe derivada estará livre para substituí-lo. A finalpalavra-chave é usada para acabar com essa liberdade.

Um exemplo de por que você precisaria disso: suponha que a classe Adefina uma função virtual prepareEnvelope()e a classe a Bsubstitua e a implemente como uma sequência de chamadas para seus próprios métodos virtuais stuffEnvelope(), lickEnvelope()e sealEnvelope(). O Class Bpretende permitir que as classes derivadas substituam esses métodos virtuais para fornecer suas próprias implementações, mas a classe Bnão deseja permitir que nenhuma classe derivada substitua prepareEnvelope()e, portanto, altere a ordem das coisas, lamba, lacre ou omita a chamada de uma delas. Portanto, neste caso, a classe Bdeclara prepareEnvelope()como final.


Primeiro, obrigado pela resposta, mas não é exatamente isso que escrevi na primeira frase da minha pergunta? O que eu realmente preciso é quando class B might wish that any class which is further derived from B should not be able to override f()? Existe algum caso no mundo real em que essa classe B e o método f () existam e se encaixam bem?
Metamaker

Sim, desculpe, espere, estou escrevendo um exemplo agora.
Mike Nakis

@ metamaker lá vai você.
Mike Nakis

1
Exemplo realmente bom, esta é a melhor resposta até agora. THX!
7285 metamaker

1
Então obrigado por um tempo perdido. Não consegui encontrar um bom exemplo na Internet, perdi muito tempo pesquisando. Eu colocaria +1 na resposta, mas não posso, pois tenho baixa reputação. :( Talvez mais tarde.
metamaker 07/07

2

Geralmente, do ponto de vista do design, é útil poder marcar as coisas como imutáveis. Da mesma forma, o constcompilador fornece proteções e indica que um estado não deve ser alterado. finalPode ser usado para indicar que o comportamento não deve ser alterado ainda mais na hierarquia de herança.

Exemplo

Considere um videogame no qual os veículos levam o jogador de um local para outro. Todos os veículos devem verificar se estão viajando para um local válido antes da partida (certificando-se de que a base no local não seja destruída, por exemplo). Podemos começar usando o idioma da interface não virtual (NVI) para garantir que essa verificação seja feita independentemente do veículo.

class Vehicle
{
public:
    virtual ~Vehicle {}

    bool transport(const Location& location)
    {
        // Mandatory check performed for all vehicle types. We could potentially
        // throw or assert here instead of returning true/false depending on the
        // exceptional level of the behavior (whether it is a truly exceptional
        // control flow resulting from external input errors or whether it's
        // simply a bug for the assert approach).
        if (valid_location(location))
            return travel_to(location);

        // If the location is not valid, no vehicle type can go there.
        return false;
    }

private:
    // Overridden by vehicle types. Note that private access here
    // does not prevent derived, nonfriends from being able to override
    // this function.
    virtual bool travel_to(const Location& location) = 0;
};

Agora, digamos que temos veículos voadores em nosso jogo, e algo que todos os veículos voadores exigem e têm em comum é que eles devem passar por uma inspeção de segurança dentro do hangar antes da decolagem.

Aqui, podemos finalgarantir que todos os veículos voadores passarão por essa inspeção e também comunicar esse requisito de design dos veículos voadores.

class FlyingVehicle: public Vehicle
{
private:
    bool travel_to(const Location& location) final
    {
        // Mandatory check performed for all flying vehicle types.
        if (safety_inspection())
            return fly_to(location);

        // If the safety inspection fails for a flying vehicle, 
        // it will not be allowed to fly to the location.
        return false;
    }

    // Overridden by flying vehicle types.
    virtual void safety_inspection() const = 0;
    virtual void fly_to(const Location& location) = 0;
};

Ao usar finaldessa maneira, estamos efetivamente estendendo a flexibilidade do idioma da interface não virtual para fornecer um comportamento uniforme na hierarquia de herança (mesmo como uma reflexão tardia, combatendo o frágil problema da classe base) para as próprias funções virtuais. Além disso, adquirimos espaço de manobra para fazer alterações centrais que afetam todos os tipos de veículos voadores como uma reflexão tardia, sem modificar cada implementação de veículo voador existente.

Este é um exemplo de uso final. Existem contextos que você encontrará onde simplesmente não faz sentido a substituição de uma função de membro virtual - isso pode levar a um design frágil e a uma violação dos seus requisitos de design.

É aí que finalé útil do ponto de vista de design / arquitetura.

Também é útil do ponto de vista de um otimizador, pois fornece ao otimizador essas informações de design que permitem desirtualizar as chamadas de função virtual (eliminando a sobrecarga dinâmica de despacho e, muitas vezes de maneira mais significativa, eliminando uma barreira de otimização entre o chamador e o chamado).

Questão

Dos comentários:

Por que final e virtual seriam usados ​​ao mesmo tempo?

Não faz sentido para uma classe base na raiz de uma hierarquia declarar uma função como ambos virtuale final. Isso me parece bastante tolo, pois faria com que o compilador e o leitor humano tivessem que passar por obstáculos desnecessários que podem ser evitados simplesmente evitando-se de virtualimediato nesse caso. No entanto, as subclasses herdam funções de membro virtual da seguinte forma:

struct Foo
{
   virtual ~Foo() {}
   virtual void f() = 0;
};

struct Bar: Foo
{
   /*implicitly virtual*/ void f() final {...}
};

Nesse caso, se Bar::fusa ou não explicitamente a palavra-chave virtual, Bar::fé uma função virtual. A virtualpalavra-chave se torna opcional neste caso. Portanto, pode fazer sentido Bar::fser especificado como final, mesmo que seja uma função virtual ( final pode ser usada para funções virtuais).

E algumas pessoas podem preferir, estilisticamente, indicar explicitamente que Bar::fé virtual, assim:

struct Bar: Foo
{
   virtual void f() final {...}
};

Para mim, é meio redundante usar ambos virtuale finalespecificadores para a mesma função neste contexto (da mesma forma virtuale override), mas é uma questão de estilo nesse caso. Algumas pessoas podem achar que virtualcomunica algo valioso aqui, como usar externpara declarações de função com ligação externa (mesmo que seja opcional sem outros qualificadores de ligação).


Como você planeja substituir o privatemétodo dentro de sua Vehicleclasse? Você não quis dizer em protectedvez disso?
Andy

3
O @DavidPacker privatenão se estende à substituição (um pouco contra-intuitivo). Especificadores públicos / protegidos / privados para funções virtuais aplicam-se apenas a chamadores e não a overriders, de maneira grosseira. Uma classe derivada pode substituir funções virtuais de sua classe base, independentemente de sua visibilidade.

O @DavidPacker protectedpode fazer um pouco mais de sentido intuitivo. Eu apenas prefiro alcançar a menor visibilidade possível sempre que posso. Eu acho que a razão pela qual a linguagem é projetada dessa maneira é que, caso contrário, funções-membro virtuais privadas não fariam sentido algum fora do contexto da amizade, já que nenhuma classe, mas um amigo, poderia substituí-las se os especificadores de acesso importassem no contexto de substituir e não apenas de ligar.

Eu pensei que configurá-lo como privado não seria compilado. Por exemplo, nem Java nem C # permitem isso. C ++ me surpreende todos os dias. Mesmo após 10 anos de programação.
Andy

@DavidPacker Também me tropeçou quando o encontrei. Talvez eu deva fazer isso protectedpara evitar confundir os outros. Acabei colocando um comentário, pelo menos, descrevendo como as funções virtuais privadas ainda podem ser substituídas.

0
  1. Ele permite muitas otimizações, porque pode ser conhecido em tempo de compilação qual função é chamada.

  2. Cuidado ao jogar a palavra "cheiro de código". "final" não torna impossível estender a aula. Clique duas vezes na palavra "final", pressione backspace e estenda a classe. NO ENTANTO final é uma excelente documentação que o desenvolvedor não espera que você substitua a função e que, por isso, o próximo desenvolvedor deve ter muito cuidado, porque a classe pode parar de funcionar corretamente se o método final for substituído dessa maneira.


Por que finale virtualsempre seria usado ao mesmo tempo?
Robert Harvey

1
Ele permite muitas otimizações, porque pode ser conhecido em tempo de compilação qual função é chamada. Você pode explicar como ou fornecer uma referência? NO ENTANTO final é uma excelente documentação que o desenvolvedor não espera que você substitua a função e que, por isso, o próximo desenvolvedor deve ter muito cuidado, porque a classe pode parar de funcionar corretamente se o método final for substituído dessa maneira. Não é um cheiro ruim?
metamaker

@RobertHarvey Estabilidade da ABI. Em qualquer outro caso, provavelmente deveria ser finale override.
Deduplicator 01/01

@ metaetamaker Eu tive o mesmo Q, discutido nos comentários aqui - programmers.stackexchange.com/questions/256778/… - e, engraçado, tropeçamos em várias outras discussões, só desde que pedimos! Basicamente, é sobre se um compilador / vinculador otimizador pode determinar se uma referência / ponteiro pode representar uma classe derivada e, portanto, precisa de uma chamada virtual. Se o método (ou classe) provavelmente não puder ser substituído além do tipo estático da referência, eles podem destirtualizar: call diretamente. Se houver alguma dúvida - com frequência - eles devem ligar virtualmente. Declarando finalpode realmente ajudá-los
underscore_d
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.