Qual é o objetivo da palavra-chave "final" no C ++ 11 para funções?

143

Qual é o objetivo da finalpalavra - chave no C ++ 11 para funções? Entendo que isso evita a substituição de funções por classes derivadas, mas se for esse o caso, não basta declarar como não virtuais suas finalfunções? Há outra coisa que estou perdendo aqui?

lezebulon
fonte
30
" não basta declarar como não virtuais suas funções" finais " " Não, as funções de substituição são implicitamente virtuais, independentemente de você usar a virtualpalavra - chave ou não.
ildjarn
13
@ildjarn isso não é verdade, se eles não foram declaradas como virtual na classe super, você não pode derivar de uma classe e transformar um método não-virtual em um virtual ..
Dan O
10
@ DanO eu acho que você não pode substituir, mas você pode "ocultar" um método dessa maneira ... o que leva a muitos problemas, pois as pessoas não pretendem ocultar métodos.
21412 Alex Kremer
16
@ DanO: Se não for virtual na super classe, não seria "anulado".
ildjarn
2
Novamente, " substituir " tem um significado específico aqui, que é dar comportamento polimórfico a uma função virtual. No seu exemplo funcnão é virtual; portanto, não há nada a substituir e, portanto, nada a ser marcado como overrideou final.
ildjarn

Respostas:

129

O que está faltando, como idljarn já mencionado em um comentário, é que, se você estiver substituindo uma função de uma classe base, não poderá marcá-la como não virtual:

struct base {
   virtual void f();
};
struct derived : base {
   void f() final;       // virtual as it overrides base::f
};
struct mostderived : derived {
   //void f();           // error: cannot override!
};
David Rodríguez - dribeas
fonte
Obrigado! este é o ponto que eu estava faltando: isto é, que mesmo suas aulas "folha" precisa marcar sua função como até mesmo virtual se eles pretendem substituir funções, e não para ser substituído si
lezebulon
8
@lezebulon: Suas classes folha não precisam marcar uma função como virtual se a superclasse a declarar virtual.
Dan O
5
Os métodos nas classes folha são implicitamente virtuais se forem virtuais na classe base. Eu acho que os compiladores devem avisar se esse 'virtual' implícito está faltando.
Aaron McDaid
@AaronMcDaid: Os compiladores geralmente alertam sobre o código que, sendo correto, pode causar confusão ou erros. Eu nunca vi alguém surpreendido por esse recurso específico da linguagem de uma maneira que pudesse causar qualquer problema, por isso não sei realmente como esse erro pode ser útil. Pelo contrário, esquecer o virtualpode causar erros, e o C ++ 11 adicionou a overridetag a uma função que detectará essa situação e falhará na compilação quando uma função que pretende substituir realmente oculta
David Rodríguez - dribeas
1
Nas notas de alteração do GCC 4.9: "Novo módulo de análise de herança de tipo aprimorando a desvirtualização. A desvirtualização agora leva em consideração os espaços de nome anônimos e a palavra-chave final do C ++ 11" - portanto, não se trata apenas de açúcar sintático, mas também de um potencial benefício de otimização.
precisa saber é o seguinte
126
  • É para impedir que uma classe seja herdada. Da Wikipedia :

    O C ++ 11 também adiciona a capacidade de impedir a herança de classes ou simplesmente impedir a substituição de métodos em classes derivadas. Isso é feito com o identificador especial final. Por exemplo:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
    
  • Também é usado para marcar uma função virtual para impedir que ela seja substituída nas classes derivadas:

    struct Base2 {
        virtual void f() final;
    };
    
    struct Derived2 : Base2 {
        void f(); // ill-formed because the virtual function Base2::f has 
                  // been marked final
    };
    

A Wikipedia ainda faz um ponto interessante :

Observe que overridenem finalas palavras-chave do idioma são. Eles são tecnicamente identificadores; eles só ganham significado especial quando usados ​​nesses contextos específicos . Em qualquer outro local, eles podem ser identificadores válidos.

Isso significa que o seguinte é permitido:

int const final = 0;     // ok
int const override = 1;  // ok
Nawaz
fonte
1
Obrigado, mas eu esqueci de mencionar que a minha pergunta em causa o uso de "final" com métodos
lezebulon
Você mencionou @lezebulon :-) "qual é o objetivo da palavra-chave" final "no C ++ 11 para funções ". (ênfase)
Aaron McDaid
Você editou? Não vejo nenhuma mensagem que diga "editado x minutos atrás por lezebulon". Como isso aconteceu? Talvez você tenha editado muito rapidamente depois de enviá-lo?
Aaron McDaid
5
@ Aaron: as edições feitas cinco minutos após a publicação não são refletidas no histórico de revisões.
Ildjarn
@Nawaz: por que não são palavras-chave, apenas especificadores? Por motivos de compatibilidade, é possível que o código preexistente antes do C ++ 11 use final e substitua para outros fins?
Destructor
45

"final" também permite que uma otimização do compilador ignore a chamada indireta:

class IAbstract
{
public:
  virtual void DoSomething() = 0;
};

class CDerived : public IAbstract
{
  void DoSomething() final { m_x = 1 ; }

  void Blah( void ) { DoSomething(); }

};

com "final", o compilador pode chamar CDerived::DoSomething()diretamente de dentro Blah(), ou até mesmo inline. Sem ele, ele deve gerar uma chamada indireta dentro do Blah()porque Blah()poderia ser chamado dentro de uma classe derivada que foi substituída DoSomething().

chris green
fonte
29

Nada a acrescentar aos aspectos semânticos de "final".

Mas eu gostaria de acrescentar ao comentário de chris green que "final" pode se tornar uma técnica de otimização de compilador muito importante em um futuro não tão distante. Não apenas no caso simples que ele mencionou, mas também para hierarquias de classe do mundo real mais complexas que podem ser "fechadas" por "final", permitindo assim aos compiladores gerar código de despacho mais eficiente do que com a abordagem vtable usual.

Uma desvantagem principal do vtables é que, para qualquer objeto virtual (assumindo 64 bits em uma CPU Intel típica), o ponteiro sozinho consome 25% (8 de 64 bytes) de uma linha de cache. No tipo de aplicativos que gosto de escrever, isso dói muito. (E, pela minha experiência, é o argumento número 1 contra o C ++ do ponto de vista purista do desempenho, isto é, pelos programadores em C.)

Em aplicativos que exigem desempenho extremo, o que não é tão incomum para o C ++, isso pode realmente se tornar incrível, não sendo necessário contornar esse problema manualmente no estilo C ou no malabarismo estranho de modelos.

Essa técnica é conhecida como Desirtualização . Um termo que vale a pena lembrar. :-)

Há um excelente discurso recente de Andrei Alexandrescu que explica muito bem como você pode solucionar essas situações hoje e como "final" pode fazer parte da solução de casos semelhantes "automaticamente" no futuro (discutido com os ouvintes):

http://channel9.msdn.com/Events/GoingNative/2013/Writing-Quick-Code-in-Cpp-Quickly

Mario Knezović
fonte
23
8 é 25% de 64?
ildjarn
6
Alguém conhece um compilador que faça uso deles agora?
Vincent Fourmond 02/02
mesma coisa que eu quero dizer.
crazii
8

Final não pode ser aplicado a funções não virtuais.

error: only virtual member functions can be marked 'final'

Não seria muito significativo poder marcar um método não virtual como 'final'. Dado

struct A { void foo(); };
struct B : public A { void foo(); };
A * a = new B;
a -> foo(); // this will call A :: foo anyway, regardless of whether there is a B::foo

a->foo()sempre ligará A::foo.

Mas, se A :: foo estivesse virtual, então B :: foo o substituiria. Isso pode ser indesejável e, portanto, faria sentido tornar a função virtual final.

A questão é, porém, por que permitir final em funções virtuais. Se você tem uma hierarquia profunda:

struct A            { virtual void foo(); };
struct B : public A { virtual void foo(); };
struct C : public B { virtual void foo() final; };
struct D : public C { /* cannot override foo */ };

Em seguida, finalcoloca um "piso" sobre o quanto a substituição pode ser feita. Outras classes podem estender A e B e substituir suas foo, mas se uma classe estender C, não será permitido.

Portanto, provavelmente não faz sentido fazer o 'nível superior' final, mas pode fazer sentido mais abaixo.

(Acho que há espaço para estender as palavras final e substituir os membros não virtuais. Eles teriam um significado diferente.)

Aaron McDaid
fonte
obrigado pelo exemplo, é algo que eu não tinha certeza. Mas ainda: qual é o sentido de ter uma função final (e virtual)? Basicamente, você nunca seria capaz de usar o fato de que a função é virtual, uma vez que não pode ser substituído
lezebulon
@lezebulon, editei minha pergunta. Mas então eu notei a resposta do DanO - é uma boa resposta clara do que eu estava tentando dizer.
Aaron McDaid
Não sou especialista, mas sinto que, às vezes, pode fazer sentido criar uma função de nível superior final. Por exemplo, se você sabe que deseja tudo Shape- foo()algo predefinido e definido que nenhuma forma derivada deve modificar. Ou, estou errado e há um padrão melhor para empregar nesse caso? EDIT: Ah, talvez porque, nesse caso, não se deva simplesmente subir ao nível superior foo() virtual? Mas ainda assim, ele pode ser escondido, mesmo se chamado corretamente (polymorphically) via Shape*...
Andrew Cheong
8

Um caso de uso para a palavra-chave 'final' de que gosto é o seguinte:

// This pure abstract interface creates a way
// for unit test suites to stub-out Foo objects
class FooInterface
{
public:
   virtual void DoSomething() = 0;
private:
   virtual void DoSomethingImpl() = 0;
};

// Implement Non-Virtual Interface Pattern in FooBase using final
// (Alternatively implement the Template Pattern in FooBase using final)
class FooBase : public FooInterface
{
public:
    virtual void DoSomething() final { DoFirst(); DoSomethingImpl(); DoLast(); }
private:
    virtual void DoSomethingImpl() { /* left for derived classes to customize */ }
    void DoFirst(); // no derived customization allowed here
    void DoLast(); // no derived customization allowed here either
};

// Feel secure knowing that unit test suites can stub you out at the FooInterface level
// if necessary
// Feel doubly secure knowing that your children cannot violate your Template Pattern
// When DoSomething is called from a FooBase * you know without a doubt that
// DoFirst will execute before DoSomethingImpl, and DoLast will execute after.
class FooDerived : public FooBase
{
private:
    virtual void DoSomethingImpl() {/* customize DoSomething at this location */}
};
YoungJohn
fonte
1
Sim, este é essencialmente um exemplo do Padrão de Método de Modelo. E antes do C ++ 11, sempre foi o TMP que me fez desejar que o C ++ tivesse um recurso de linguagem como "final", como o Java.
Kaitain
6

final adiciona uma intenção explícita de não ter sua função substituída e causará um erro do compilador caso isso seja violado:

struct A {
    virtual int foo(); // #1
};
struct B : A {
    int foo();
};

Como o código está, ele compila e B::foosubstitui A::foo. B::fooA propósito, também é virtual. No entanto, se mudarmos o número 1 para virtual int foo() final, esse é um erro do compilador e não podemos substituir A::foomais nas classes derivadas.

Observe que isso não nos permite "reabrir" uma nova hierarquia, ou seja, não há como criar B::foouma nova função não relacionada que possa estar independentemente à frente de uma nova hierarquia virtual. Depois que uma função é final, ela nunca pode ser declarada novamente em nenhuma classe derivada.

Kerrek SB
fonte
5

A palavra-chave final permite declarar um método virtual, substituí-lo N vezes e depois exigir que 'isso não possa mais ser substituído'. Seria útil restringir o uso de sua classe derivada, para que você possa dizer "Eu sei que minha super classe permite que você substitua isso, mas se você quiser derivar de mim, não poderá!".

struct Foo
{
   virtual void DoStuff();
}

struct Bar : public Foo
{
   void DoStuff() final;
}

struct Babar : public Bar
{
   void DoStuff(); // error!
}

Como outros pôsteres apontaram, ele não pode ser aplicado a funções não virtuais.

Um objetivo da palavra-chave final é impedir a substituição acidental de um método. No meu exemplo, DoStuff () pode ter sido uma função auxiliar que a classe derivada simplesmente precisa renomear para obter o comportamento correto. Sem final, o erro não seria descoberto até o teste.

Dan O
fonte
1

A palavra-chave final em C ++, quando adicionada a uma função, impede que ela seja substituída por uma classe base. Além disso, quando adicionado a uma classe, impede a herança de qualquer tipo. Considere o exemplo a seguir, que mostra o uso do especificador final. Este programa falha na compilação.

#include <iostream>
using namespace std;

class Base
{
  public:
  virtual void myfun() final
  {
    cout << "myfun() in Base";
  }
};
class Derived : public Base
{
  void myfun()
  {
    cout << "myfun() in Derived\n";
  }
};

int main()
{
  Derived d;
  Base &b = d;
  b.myfun();
  return 0;
}

Além disso:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}
Krishna Ganeriwal
fonte
0

Suplemento à resposta de Mario Knezović:

class IA
{
public:
  virtual int getNum() const = 0;
};

class BaseA : public IA
{
public:
 inline virtual int getNum() const final {return ...};
};

class ImplA : public BaseA {...};

IA* pa = ...;
...
ImplA* impla = static_cast<ImplA*>(pa);

//the following line should cause compiler to use the inlined function BaseA::getNum(), 
//instead of dynamic binding (via vtable or something).
//any class/subclass of BaseA will benefit from it

int n = impla->getNum();

O código acima mostra a teoria, mas não foi realmente testado em compiladores reais. Muito apreciado se alguém colar uma saída desmontada.

crazii
fonte