As funções virtuais em linha são realmente sem sentido?

172

Eu recebi essa pergunta quando recebi um comentário de revisão de código dizendo que as funções virtuais não precisam estar embutidas.

Eu pensei que funções virtuais embutidas poderiam ser úteis em cenários em que funções são chamadas diretamente a objetos. Mas o contra-argumento me veio à mente: por que alguém iria querer definir virtual e depois usar objetos para chamar métodos?

É melhor não usar funções virtuais em linha, pois elas quase nunca são expandidas?

Fragmento de código que usei para análise:

class Temp
{
public:

    virtual ~Temp()
    {
    }
    virtual void myVirtualFunction() const
    {
        cout<<"Temp::myVirtualFunction"<<endl;
    }

};

class TempDerived : public Temp
{
public:

    void myVirtualFunction() const
    {
        cout<<"TempDerived::myVirtualFunction"<<endl;
    }

};

int main(void) 
{
    TempDerived aDerivedObj;
    //Compiler thinks it's safe to expand the virtual functions
    aDerivedObj.myVirtualFunction();

    //type of object Temp points to is always known;
    //does compiler still expand virtual functions?
    //I doubt compiler would be this much intelligent!
    Temp* pTemp = &aDerivedObj;
    pTemp->myVirtualFunction();

    return 0;
}
aJ.
fonte
1
Considere a possibilidade de compilar um exemplo com as opções necessárias para obter uma lista de assembler e, em seguida, mostrar ao revisor de código que, de fato, o compilador pode incorporar funções virtuais.
Thomas L Holaday
1
O acima descrito geralmente não será incorporado, porque você está chamando uma função virtual em auxílio da classe base. Embora dependa apenas de quão inteligente o compilador é. Se pudesse apontar que pTemp->myVirtualFunction()poderia ser resolvido como uma chamada não virtual, pode haver uma chamada em linha. Essa chamada referenciada é incorporada pelo g ++ 3.4.2: TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();Seu código não é.
doc
1
Uma coisa que o gcc realmente faz é comparar a entrada da tabela com um símbolo específico e, em seguida, usar uma variante embutida em um loop, se corresponder. Isso é especialmente útil se a função embutida estiver vazia e o loop puder ser eliminado nesse caso.
Simon Richter
1
@doc O compilador moderno tenta determinar em tempo de compilação os possíveis valores dos ponteiros. O simples uso de um ponteiro não é suficiente para impedir a inclusão em um nível significativo de otimização; O GCC ainda realiza simplificações com otimização zero!
precisa

Respostas:

153

As funções virtuais podem ser incorporadas algumas vezes. Um trecho das excelentes perguntas frequentes sobre C ++ :

"A única vez em que uma chamada virtual embutida pode ser incorporada é quando o compilador conhece a" classe exata "do objeto que é o alvo da chamada de função virtual. Isso pode acontecer apenas quando o compilador possui um objeto real em vez de um ponteiro ou referência a um objeto. Ou seja, com um objeto local, um objeto global / estático ou um objeto totalmente contido dentro de um composto. "

ya23
fonte
7
É verdade, mas vale lembrar que o compilador é livre para ignorar o especificador embutido, mesmo que a chamada possa ser resolvida em tempo de compilação e possa ser embutida.
sharptooth
6
Outra situação em que acho que o inlining pode acontecer é quando você chama o método, por exemplo, como-> Temp :: myVirtualFunction () - essa chamada ignora a resolução da tabela virtual e a função deve ser incorporada sem problemas - por que e se você ' d querer fazê-lo é outro tema :)
RnR
5
@RnR. Não é necessário ter 'this->', basta usar o nome qualificado. E esse comportamento ocorre para destruidores, construtores e, em geral, para operadores de atribuição (veja minha resposta).
9609 Richard Corden
2
sharptooth - true, mas o AFAIK é válido para todas as funções inline, não apenas para funções inline virtuais.
Colen
2
void f (const Base & lhs, const Base & rhs) {} ------ Na implementação da função, você nunca sabe o que lhs e rhs apontam até o tempo de execução.
Baiyan Huang
72

C ++ 11 foi adicionado final. Isso muda a resposta aceita: não é mais necessário saber a classe exata do objeto, é suficiente saber que o objeto tem pelo menos o tipo de classe em que a função foi declarada final:

class A { 
  virtual void foo();
};
class B : public A {
  inline virtual void foo() final { } 
};
class C : public B
{
};

void bar(B const& b) {
  A const& a = b; // Allowed, every B is an A.
  a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C.
}
MSalters
fonte
Não foi possível incorporá-lo no VS 2017.
Yola 13/09
1
Eu não acho que funciona dessa maneira. A invocação de foo () através de um ponteiro / referência do tipo A nunca pode ser incorporada. Chamar b.foo () deve permitir inlining. A menos que você esteja sugerindo que o compilador já saiba que este é um tipo B, porque está ciente da linha anterior. Mas esse não é o uso típico.
Jeffrey Faust
Por exemplo, compare o código gerado para bar e bas aqui: godbolt.org/g/xy3rNh
Jeffrey Faust
@JeffreyFaust Não há razão para que a informação não deva ser propagada, existe? E iccparece fazê-lo, de acordo com esse link.
Alexey Romanov
Os compiladores @AlexeyRomanov têm liberdade para otimizar além do padrão, e certamente o fazem! Para casos simples como acima, o compilador pode conhecer o tipo e fazer essa otimização. As coisas raramente são simples assim, e não é típico poder determinar o tipo real de uma variável polimórfica em tempo de compilação. Eu acho que o OP se preocupa com 'em geral' e não com esses casos especiais.
Jeffrey Faust
37

Há uma categoria de funções virtuais em que ainda faz sentido tê-las em linha. Considere o seguinte caso:

class Base {
public:
  inline virtual ~Base () { }
};

class Derived1 : public Base {
  inline virtual ~Derived1 () { } // Implicitly calls Base::~Base ();
};

class Derived2 : public Derived1 {
  inline virtual ~Derived2 () { } // Implicitly calls Derived1::~Derived1 ();
};

void foo (Base * base) {
  delete base;             // Virtual call
}

A chamada para excluir 'base' realizará uma chamada virtual para chamar o destruidor correto da classe derivada; essa chamada não está embutida. No entanto, como cada destruidor chama seu destruidor pai (que nesses casos está vazio), o compilador pode incorporar essas chamadas, pois elas não chamam virtualmente as funções da classe base.

O mesmo princípio existe para construtores de classe base ou para qualquer conjunto de funções em que a implementação derivada também chame a implementação de classe base.

Richard Corden
fonte
23
Deve-se estar ciente, porém, de que chaves vazias nem sempre significam que o destruidor não faz nada. Os destruidores destroem por padrão todos os objetos membros da classe; portanto, se você tiver alguns vetores na classe base, isso poderá dar bastante trabalho nessas chaves vazias!
17372 Philip
14

Eu já vi compiladores que não emitem nenhuma tabela v se nenhuma função não-inline existir (e definida em um arquivo de implementação em vez de um cabeçalho). Eles lançariam erros como missing vtable-for-class-Aou algo semelhante, e você ficaria confuso como o inferno, como eu era.

De fato, isso não está em conformidade com o Padrão, mas isso acontece; considere colocar pelo menos uma função virtual que não esteja no cabeçalho (se apenas o destruidor virtual), para que o compilador possa emitir uma tabela de tabelas para a classe naquele local. Eu sei que isso acontece com algumas versões do gcc.

Como alguém mencionado, as funções virtuais embutidas às vezes podem ser um benefício , mas é claro que você as usará com mais frequência quando não souber o tipo dinâmico do objeto, porque esse foi o motivo todo virtualem primeiro lugar.

O compilador, no entanto, não pode ignorar completamente inline. Possui outras semânticas além de acelerar uma chamada de função. A linha implícita para definições em classe é o mecanismo que permite colocar a definição no cabeçalho: Somente inlinefunções podem ser definidas várias vezes em todo o programa, sem violar nenhuma regra. No final, ele se comporta como você o definiria apenas uma vez em todo o programa, mesmo que você tenha incluído o cabeçalho várias vezes em diferentes arquivos vinculados.

Johannes Schaub - litb
fonte
11

Bem, na verdade, as funções virtuais sempre podem ser incorporadas , desde que estejam estaticamente vinculadas: suponha que tenhamos uma classe abstrata Base com uma função virtual Fe classes derivadas Derived1e Derived2:

class Base {
  virtual void F() = 0;
};

class Derived1 : public Base {
  virtual void F();
};

class Derived2 : public Base {
  virtual void F();
};

Uma chamada hipotética b->F();(com bdo tipo Base*) é obviamente virtual. Mas você (ou o compilador ...) poderia reescrevê-lo dessa maneira (suponha que typeofseja uma typeidfunção semelhante a que retorne um valor que possa ser usado em a switch)

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // static, inlineable call
  case Derived2: b->Derived2::F(); break; // static, inlineable call
  case Base:     assert(!"pure virtual function call!");
  default:       b->F(); break; // virtual call (dyn-loaded code)
}

Embora ainda necessitemos de RTTI para a typeofchamada, a chamada pode ser efetivamente incorporada, basicamente, incorporando a vtable no fluxo de instruções e especializando a chamada para todas as classes envolvidas. Isso também pode ser generalizado, especializando apenas algumas classes (digamos, apenas Derived1):

switch (typeof(b)) {
  case Derived1: b->Derived1::F(); break; // hot path
  default:       b->F(); break; // default virtual call, cold path
}
CAFxX
fonte
Eles são compiladores que fazem isso? Ou isso é apenas especulação? Desculpe se sou excessivamente cético, mas seu tom na descrição acima parece mais ou menos - "eles poderiam fazer isso!", Que é diferente de "alguns compiladores fazem isso".
Alex Meiburg 19/08/19
Sim, Graal faz inlining polimórfico (também para LLVM bitcode via Sulong)
CAFxX
3

o inline realmente não faz nada - é uma dica. O compilador pode ignorá-lo ou pode incorporar um evento de chamada sem linha, se vir a implementação e gostar dessa idéia. Se a clareza do código estiver em risco, a linha deve ser removida.

dente afiado
fonte
2
Para compiladores que operam apenas em TUs únicas, eles podem apenas incorporar funções implicitamente para as quais eles têm a definição. Uma função só pode ser definida em várias TUs se você a incorporar. 'inline' é mais do que uma dica e pode ter uma melhoria drástica de desempenho para uma compilação g ++ / makefile.
9609 Richard Corden
3

Funções virtuais declaradas embutidas são embutidas quando chamadas por meio de objetos e ignoradas quando chamadas por meio de ponteiro ou referências.

tarachandverma
fonte
1

Com os compiladores modernos, não será prejudicial inibe-los. Alguns combos antigos de compilador / vinculador podem ter criado várias vtables, mas não acredito que isso seja um problema.


fonte
1

Um compilador só pode incorporar uma função quando a chamada pode ser resolvida sem ambiguidade no momento da compilação.

As funções virtuais, no entanto, são resolvidas no tempo de execução e, portanto, o compilador não pode incorporar a chamada, pois no tipo de compilação o tipo dinâmico (e, portanto, a implementação da função a ser chamada) não pode ser determinado.

PaulJWilliams
fonte
1
Quando você chamar um método de classe base da mesma ou de classe derivada da chamada é inequívoca e não-virtual
sharptooth
1
@ sharptooth: mas então seria um método inline não virtual. O compilador pode incorporar funções que você não solicitou e provavelmente sabe melhor quando incorporar ou não. Deixe decidir.
David Rodríguez - dribeas
1
@dribeas: Sim, é exatamente disso que estou falando. Eu apenas objetei à afirmação de que as intenções virtuais são resolvidas em tempo de execução - isso é verdade apenas quando a chamada é feita virtualmente, não para a classe exata.
sharptooth
Eu acredito que isso é um absurdo. Qualquer função sempre pode ser incorporada, não importa quão grande seja ou virtual ou não. Depende de como o compilador foi escrito. Se você não concordar, espero que seu compilador também não possa produzir código não incorporado. Ou seja: O compilador pode incluir código que, em tempo de execução, testa as condições que não pôde ser resolvido em tempo de compilação. É como se os compiladores modernos pudessem resolver valores constantes / reduzir expressões numéricas em tempo de compilação. Se uma função / método não está embutida, isso não significa que não pode estar embutida.
1

Nos casos em que a chamada de função é inequívoca e a função é um candidato adequado para embutir, o compilador é inteligente o suficiente para embutir o código de qualquer maneira.

O resto do tempo "inline virtual" é um absurdo e, de fato, alguns compiladores não compilarão esse código.

sombra da Lua
fonte
Qual versão do g ++ não compila virtuais virtuais?
Thomas L Holaday
Hum. O 4.1.1 que tenho aqui agora parece ser feliz. Encontrei problemas com esta base de código usando um 4.0.x. Acho que minhas informações estão desatualizadas, editadas.
Moonshadow
0

Faz sentido criar funções virtuais e depois chamá-las em objetos, em vez de referências ou ponteiros. Scott Meyer recomenda, em seu livro "c ++ eficaz", nunca redefinir uma função não virtual herdada. Isso faz sentido, porque quando você cria uma classe com uma função não virtual e redefine a função em uma classe derivada, pode usá-la corretamente, mas não pode ter certeza de que outras pessoas a usarão corretamente. Além disso, você pode usá-lo incorretamente posteriormente. Portanto, se você cria uma função em uma classe base e deseja que seja redifinável, torne-a virtual. Se faz sentido criar funções virtuais e chamá-las em objetos, também faz sentido incorporá-las.

Balthazar
fonte
0

Na verdade, em alguns casos, adicionar "inline" a uma substituição final virtual pode fazer com que seu código não seja compilado; portanto, às vezes há uma diferença (pelo menos no compilador do VS2017s)!

Na verdade, eu estava executando uma função de substituição final em linha virtual no VS2017, adicionando o padrão c ++ 17 para compilar e vincular e, por algum motivo, falhou quando estou usando dois projetos.

Eu tinha um projeto de teste e uma DLL de implementação que eu sou teste de unidade. No projeto de teste, eu estou tendo um arquivo "linker_includes.cpp" que inclui os arquivos * .cpp do outro projeto necessário. Eu sei ... Eu sei que posso configurar o msbuild para usar os arquivos de objeto da DLL, mas lembre-se de que é uma solução específica da Microsoft, embora a inclusão dos arquivos cpp não esteja relacionada ao sistema de compilação e muito mais fácil para a versão um arquivo cpp que arquivos xml e configurações do projeto e ...

O interessante foi que eu estava constantemente recebendo erros do vinculador do projeto de teste. Mesmo se eu adicionasse a definição das funções ausentes, copie e cole e não por meio de include! Tão estranho. O outro projeto foi criado e não há conexão entre os dois além de marcar uma referência de projeto, portanto, há uma ordem de criação para garantir que ambos sejam sempre criados ...

Eu acho que é algum tipo de bug no compilador. Não faço ideia se ele existe no compilador enviado com o VS2020, porque estou usando uma versão mais antiga, porque alguns SDK funcionam apenas com isso adequadamente :-(

Eu só queria acrescentar que não apenas marcá-los como inline pode significar algo, mas pode até fazer com que seu código não seja compilado em algumas circunstâncias raras! Isso é estranho, mas é bom saber.

PS: O código no qual estou trabalhando é relacionado à computação gráfica, então prefiro inlining e é por isso que usei final e inline. Eu mantive o especificador final para esperar que a compilação do lançamento seja inteligente o suficiente para compilar a DLL, incluindo-a mesmo sem que eu indique diretamente ...

PS (Linux) .: Espero que o mesmo não aconteça no gcc ou no clang, pois eu costumava fazer esse tipo de coisa. Não sei de onde vem esse problema ... Prefiro fazer c ++ no Linux ou pelo menos com alguns gcc, mas às vezes o projeto é diferente em necessidades.

prenex
fonte