Todos nós sabemos o que são funções virtuais em C ++, mas como elas são implementadas em um nível profundo?
A vtable pode ser modificada ou mesmo acessada diretamente em tempo de execução?
A vtable existe para todas as classes ou apenas para aquelas que têm pelo menos uma função virtual?
As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?
Ter uma única função virtual desacelera toda a classe? Ou apenas a chamada para a função que é virtual? E a velocidade é afetada se a função virtual for realmente substituída ou não, ou isso não tem efeito enquanto for virtual.
c++
polymorphism
virtual-functions
vtable
Brian R. Bondy
fonte
fonte
Inside the C++ Object Model
porStanley B. Lippman
. (Seção 4.2, páginas 124-131)Respostas:
Como as funções virtuais são implementadas em um nível profundo?
De "Funções Virtuais em C ++" :
A vtable pode ser modificada ou mesmo acessada diretamente em tempo de execução?
Universalmente, acredito que a resposta seja "não". Você poderia alterar a memória para encontrar a vtable, mas ainda não saberia como é a assinatura da função ao chamá-la. Qualquer coisa que você deseja alcançar com essa capacidade (que a linguagem suporta) deve ser possível sem acessar a vtable diretamente ou modificá-la em tempo de execução. Observe também que a especificação da linguagem C ++ não especifica que vtables são necessários - no entanto, é assim que a maioria dos compiladores implementa funções virtuais.
A vtable existe para todos os objetos ou apenas aqueles que têm pelo menos uma função virtual?
Eu acredito que a resposta aqui é "depende da implementação", uma vez que a especificação não requer vtables em primeiro lugar. No entanto, na prática, acredito que todos os compiladores modernos só criam uma vtable se uma classe tiver pelo menos 1 função virtual. Há uma sobrecarga de espaço associada à vtable e uma sobrecarga de tempo associada à chamada de uma função virtual versus uma função não virtual.
As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?
A resposta é que não é especificado pela especificação do idioma, portanto, depende da implementação. Chamar a função virtual pura resulta em comportamento indefinido se não estiver definido (o que geralmente não é) (ISO / IEC 14882: 2003 10.4-2). Na prática, ele aloca um slot na vtable para a função, mas não atribui um endereço a ela. Isso deixa a vtable incompleta, o que requer que as classes derivadas implementem a função e concluam a vtable. Algumas implementações simplesmente colocam um ponteiro NULL na entrada vtable; outras implementações colocam um ponteiro para um método fictício que faz algo semelhante a uma asserção.
Observe que uma classe abstrata pode definir uma implementação para uma função virtual pura, mas essa função só pode ser chamada com uma sintaxe de id qualificada (ou seja, especificando totalmente a classe no nome do método, semelhante a chamar um método de classe base de um classe derivada). Isso é feito para fornecer uma implementação padrão fácil de usar, enquanto ainda requer que uma classe derivada forneça uma substituição.
Ter uma única função virtual desacelera toda a classe ou apenas a chamada para a função que é virtual?
Isto está chegando ao limite do meu conhecimento, então alguém por favor me ajude aqui se eu estiver errado!
Eu acredito que apenas as funções que são virtuais na classe experimentam o impacto do tempo de desempenho relacionado à chamada de uma função virtual em vez de uma função não virtual. A sobrecarga de espaço para a classe existe de qualquer maneira. Observe que se houver uma vtable, haverá apenas 1 por classe , e não uma por objeto .
A velocidade é afetada se a função virtual for realmente substituída ou não, ou isso não tem efeito, desde que seja virtual?
Não acredito que o tempo de execução de uma função virtual que é substituída diminui em comparação com a chamada da função virtual base. No entanto, há uma sobrecarga de espaço adicional para a classe associada à definição de outra vtable para a classe derivada versus a classe base.
Recursos adicionais:
http://www.codersource.net/published/view/325/virtual_functions_in.aspx (via máquina de volta)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable
fonte
Não portável, mas se você não se importa com truques sujos, com certeza!
Na maioria dos compiladores que já vi, o vtbl * são os primeiros 4 bytes do objeto, e o conteúdo do vtbl é simplesmente um array de ponteiros de membro (geralmente na ordem em que foram declarados, com a classe base primeiro). É claro que existem outros layouts possíveis, mas é o que geralmente observei.
Agora, para puxar algumas travessuras ...
Alterando a classe em tempo de execução:
Substituindo um método para todas as instâncias (monkeypatching uma classe)
Este é um pouco mais complicado, pois o próprio vtbl provavelmente está na memória somente leitura.
É muito provável que o último faça os verificadores de vírus e o link despertar e notar, devido às manipulações do mprotect. Em um processo que usa o bit NX, ele pode falhar.
fonte
Ter uma única função virtual desacelera toda a classe?
Ter funções virtuais torna toda a classe mais lenta, na medida em que mais um item de dados precisa ser inicializado, copiado, ... ao lidar com um objeto de tal classe. Para uma classe com cerca de meia dúzia de membros, a diferença deve ser desprezível. Para uma classe que contém apenas um único
char
membro, ou nenhum membro, a diferença pode ser notável.Além disso, é importante notar que nem toda chamada para uma função virtual é uma chamada de função virtual. Se você tiver um objeto de um tipo conhecido, o compilador pode emitir código para uma chamada de função normal e pode até embutir essa função, se assim o desejar. É apenas quando você faz chamadas polimórficas, por meio de um ponteiro ou referência que pode apontar para um objeto da classe base ou para um objeto de alguma classe derivada, que você precisa da indireção vtable e paga por isso em termos de desempenho.
As etapas que o hardware deve realizar são essencialmente as mesmas, independentemente de a função ser substituída ou não. O endereço da vtable é lido do objeto, o ponteiro de função recuperado do slot apropriado e a função chamada por ponteiro. Em termos de desempenho real, as previsões do ramo podem ter algum impacto. Portanto, por exemplo, se a maioria de seus objetos se referem à mesma implementação de uma determinada função virtual, então há alguma chance de que o preditor de ramificação preveja corretamente qual função chamar antes mesmo que o ponteiro seja recuperado. Mas não importa qual função é a comum: pode ser a maioria dos objetos delegando ao caso base não sobrescrito ou a maioria dos objetos pertencentes à mesma subclasse e, portanto, delegando ao mesmo caso sobrescrito.
como eles são implementados em um nível profundo?
Eu gosto da ideia de jheriko para demonstrar isso usando uma implementação simulada. Mas eu usaria C para implementar algo semelhante ao código acima, para que o nível baixo seja mais facilmente visto.
classe pai Foo
classe derivada Bar
função f realizando chamada de função virtual
Como você pode ver, uma vtable é apenas um bloco estático na memória, contendo principalmente ponteiros de função. Cada objeto de uma classe polimórfica apontará para a vtable correspondente ao seu tipo dinâmico. Isso também torna a conexão entre o RTTI e as funções virtuais mais clara: você pode verificar o tipo de classe simplesmente olhando para qual vtable ela aponta. O texto acima é simplificado de várias maneiras, como, por exemplo, herança múltipla, mas o conceito geral é válido.
Se
arg
for do tipoFoo*
e você pegararg->vtable
, mas na verdade for um objeto do tipoBar
, você ainda obterá o endereço correto dovtable
. Isso porquevtable
é sempre o primeiro elemento no endereço do objeto, independentemente de ser chamadovtable
oubase.vtable
em uma expressão digitada corretamente.fonte
Normalmente com uma VTable, uma matriz de ponteiros para funções.
fonte
Esta resposta foi incorporada à resposta do Community Wiki
A resposta para isso é que ela não é especificada - chamar a função virtual pura resulta em um comportamento indefinido se ela não for definida (o que geralmente não é) (ISO / IEC 14882: 2003 10.4-2). Algumas implementações simplesmente colocam um ponteiro NULL na entrada vtable; outras implementações colocam um ponteiro para um método fictício que faz algo semelhante a uma asserção.
Observe que uma classe abstrata pode definir uma implementação para uma função virtual pura, mas essa função só pode ser chamada com uma sintaxe de id qualificada (ou seja, especificando totalmente a classe no nome do método, semelhante a chamar um método de classe base de um classe derivada). Isso é feito para fornecer uma implementação padrão fácil de usar, enquanto ainda requer que uma classe derivada forneça uma substituição.
fonte
Você pode recriar a funcionalidade de funções virtuais em C ++ usando ponteiros de função como membros de uma classe e funções estáticas como as implementações, ou usando ponteiros para funções de membro e funções de membro para as implementações. Existem apenas vantagens de notação entre os dois métodos ... na verdade, as chamadas de função virtuais são apenas uma conveniência de notação em si. Na verdade, a herança é apenas uma notação conveniente ... tudo pode ser implementado sem usar os recursos da linguagem para herança. :)
O código abaixo é uma porcaria não testado, provavelmente um código com bugs, mas espero que demonstre a ideia.
por exemplo
fonte
void(*)(Foo*) MyFunc;
isso é alguma sintaxe Java?Vou tentar tornar isso simples :)
Todos nós sabemos o que são funções virtuais em C ++, mas como elas são implementadas em um nível profundo?
Este é um array com ponteiros para funções, que são implementações de uma função virtual particular. Um índice nesta matriz representa um índice particular de uma função virtual definida para uma classe. Isso inclui funções virtuais puras.
Quando uma classe polimórfica deriva de outra classe polimórfica, podemos ter as seguintes situações:
A vtable pode ser modificada ou mesmo acessada diretamente em tempo de execução?
Não é o caminho padrão - não há API para acessá-los. Os compiladores podem ter algumas extensões ou APIs privadas para acessá-los, mas isso pode ser apenas uma extensão.
A vtable existe para todas as classes ou apenas para aquelas que têm pelo menos uma função virtual?
Apenas aquelas que possuem pelo menos uma função virtual (seja ela mesmo destrutora) ou derivam pelo menos uma classe que possui sua vtable ("é polimórfica").
As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?
Essa é uma implementação possível, mas não praticada. Em vez disso, geralmente há uma função que imprime algo como "função virtual pura chamada" e o faz
abort()
. A chamada para que pode ocorrer se você tentar chamar o método abstrato no construtor ou destruidor.Ter uma única função virtual desacelera toda a classe? Ou apenas a chamada para a função que é virtual? E a velocidade é afetada se a função virtual for realmente substituída ou não, ou isso não tem efeito enquanto for virtual.
A lentidão depende apenas se a chamada foi resolvida como uma chamada direta ou como uma chamada virtual. E nada mais importa. :)
Se você chamar uma função virtual por meio de um ponteiro ou referência a um objeto, ela sempre será implementada como uma chamada virtual - porque o compilador nunca pode saber que tipo de objeto será atribuído a esse ponteiro em tempo de execução, e se é de um classe na qual este método é substituído ou não. Apenas em dois casos o compilador pode resolver a chamada para uma função virtual como uma chamada direta:
final
na classe para a qual você tem um ponteiro ou referência por meio da qual você o chama ( somente em C ++ 11 ). Nesse caso, o compilador sabe que esse método não pode sofrer nenhuma modificação adicional e só pode ser o método desta classe.Observe, porém, que as chamadas virtuais têm apenas sobrecarga de desreferenciar dois ponteiros. Usar RTTI (embora disponível apenas para classes polimórficas) é mais lento do que chamar métodos virtuais, caso você encontre um caso para implementar a mesma coisa dessas duas maneiras. Por exemplo, definir
virtual bool HasHoof() { return false; }
e substituir apenas obool Horse::HasHoof() { return true; }
que forneceria a capacidade de chamarif (anim->HasHoof())
isso será mais rápido do que tentarif(dynamic_cast<Horse*>(anim))
. Isso ocorre porquedynamic_cast
, em alguns casos, é necessário percorrer a hierarquia de classes, mesmo recursivamente, para ver se pode ser construído o caminho a partir do tipo de ponteiro real e do tipo de classe desejado. Enquanto a chamada virtual é sempre a mesma - desreferenciando dois ponteiros.fonte
Aqui está uma implementação manual executável de tabela virtual em C ++ moderno. Tem uma semântica bem definida, sem hacks e não
void*
.Observação:
.*
e->*
são operadores diferentes de*
e->
. Os ponteiros de função de membro funcionam de maneira diferente.fonte
Cada objeto tem um ponteiro vtable que aponta para uma matriz de funções-membro.
fonte
Algo não mencionado aqui em todas essas respostas é no caso de herança múltipla, onde todas as classes base possuem métodos virtuais. A classe herdada possui vários ponteiros para um vmt. O resultado é que o tamanho de cada instância de tal objeto é maior. Todo mundo sabe que uma classe com métodos virtuais tem 4 bytes extras para o vmt, mas no caso de herança múltipla é para cada classe base que tem métodos virtuais vezes 4. 4 sendo o tamanho do ponteiro.
fonte
As respostas de Burly estão corretas aqui, exceto pela pergunta:
As classes abstratas simplesmente têm um NULL para o ponteiro de função de pelo menos uma entrada?
A resposta é que nenhuma tabela virtual é criada para classes abstratas. Não há necessidade, pois nenhum objeto dessas classes pode ser criado!
Em outras palavras, se tivermos:
O ponteiro vtbl acessado por meio de pB será o vtbl da classe D. É exatamente assim que o polimorfismo é implementado. Ou seja, como os métodos D são acessados por meio do pB. Não há necessidade de um vtbl para a classe B.
Em resposta ao comentário de Mike abaixo ...
Se a classe B em minha descrição tem um método virtual foo () que não é sobrescrito por D e um método virtual bar () que é sobrescrito, então o vtbl de D terá um ponteiro para foo de B () e para sua própria barra () . Ainda não há vtbl criado para B.
fonte
B
deve ser necessária. Só porque alguns de seus métodos têm implementações (padrão), não significa que eles precisam ser armazenados em uma vtable. Mas acabei de executar seu código (modulo algumas correções para torná-lo compilado)gcc -S
seguido porc++filt
e há claramente uma vtable paraB
incluída lá. Acho que pode ser porque a vtable também armazena dados RTTI, como nomes de classe e herança. Pode ser necessário para umdynamic_cast<B*>
. Mesmo-fno-rtti
não faz o vtable ir embora. Com, emclang -O3
vez degcc
, de repente se foi.prova de conceito muito fofa que fiz um pouco antes (para ver se a ordem de herança é importante); deixe-me saber se sua implementação de C ++ realmente o rejeita (minha versão do gcc só dá um aviso para atribuir estruturas anônimas, mas isso é um bug), estou curioso.
CCPolite.h :
CCPolite_constructor.h :
main.c :
resultado:
observe que, como nunca estou alocando meu objeto falso, não há necessidade de fazer qualquer destruição; os destruidores são colocados automaticamente no final do escopo dos objetos alocados dinamicamente para recuperar a memória do próprio literal do objeto e do ponteiro vtable.
fonte