Oi Habr. Se você souber a resposta para a pergunta no título, parabéns, não precisa deste artigo. É dirigido a iniciantes em programação, como eu, que nem sempre conseguem entender de maneira independente todos os meandros do C ++ e de outras linguagens digitadas; se puderem, é melhor aprender com os erros de outras pessoas.
Neste artigo, não responderei apenas à pergunta "
Por que precisamos de funções virtuais em C ++ ", mas darei um exemplo da minha prática. Para uma breve resposta, você pode recorrer aos mecanismos de pesquisa que produzem algo como o seguinte: "
As funções virtuais são necessárias para fornecer polimorfismo - uma das três baleias OOP. Graças a elas, a própria máquina pode determinar o tipo de objeto por ponteiro, sem carregar o programador com esta tarefa "
. Ok, mas a pergunta "por que" permanece, embora agora signifique um pouco diferente: "
Por que confiar na máquina, gaste tempo e memória extras, se você pode fazer o podcast do ponteiro, porque o tipo de objeto ao qual se refere é quase sempre conhecido? " De fato, a transmissão à primeira vista deixa as funções virtuais sem trabalho, e é isso que causa conceitos errados e código incorreto. Em pequenos projetos, a perda é invisível, mas, como você verá em breve, com o crescimento do programa, as castas aumentam a listagem em uma progressão quase geométrica.
Primeiro, vamos lembrar onde castas e funções virtuais podem ser necessárias. Um tipo é perdido quando um objeto declarado com o tipo A recebe uma nova operação para alocar memória para um objeto do tipo B compatível com o tipo A, geralmente herdado de A. Na maioria das vezes, o objeto não é um, mas uma matriz inteira. Uma matriz de ponteiros do mesmo tipo, cada um deles aguardando a atribuição de uma área de memória com objetos de tipos completamente diferentes. Aqui está um exemplo que consideraremos.
Não vou arrastar por muito tempo, a tarefa era a seguinte: com base em um documento marcado com a linguagem de marcação de hipertexto Markedit (você pode ler sobre isso
aqui ), construa uma árvore de análise e crie um arquivo contendo o mesmo documento na marcação HTML. Minha solução consiste em três rotinas seqüenciais: analisar o texto de origem em tokens, criar uma árvore de sintaxe a partir de tokens e criar um documento HTML em sua base. Estamos interessados na segunda parte.
O fato é que os nós da árvore de destino têm tipos diferentes (seção, parágrafo, nó de texto, link, nota de rodapé etc.), mas para nós pai, ponteiros para nós filhos são armazenados na matriz e, portanto, têm um tipo - Nó.
O analisador em si, de forma simplificada, funciona assim: cria a “raiz” da árvore de sintaxe da
árvore com o tipo
Raiz , declara um ponteiro
open_node do tipo geral
Node , que é imediatamente atribuído ao endereço da
árvore e a variável de
tipo do
tipo enumerado
Node_type e, em seguida, o loop inicia,
repetindo os tokens desde o primeiro até o último. A cada iteração, o tipo do nó aberto
open_node é inserido
primeiro na variável de tipo (os tipos na forma de uma enumeração são armazenados na estrutura do nó), seguidos pela
instrução switch , que verifica o tipo do próximo token (os tipos de tokens já são fornecidos com cuidado pelo lexer). Em cada ramificação do comutador, é apresentada outra ramificação que verifica a variável de
tipo , onde, como lembramos, o tipo de nó aberto está contido. Dependendo do seu valor, diferentes ações são executadas, por exemplo: adicione uma lista de nós de um determinado tipo a um nó aberto, abra outro nó de um determinado tipo em um nó aberto e passe seu endereço para
open_node , feche o nó aberto e
gere uma exceção. Aplicável ao tópico do artigo, estamos interessados no segundo exemplo. Cada nó aberto (e geralmente todos os nós que podem ser abertos) já contém uma matriz de ponteiros para nós do tipo
Node . Portanto, quando abrimos um novo nó em um nó aberto (atribuímos a área de memória para um objeto de outro tipo ao próximo ponteiro da matriz), para um analisador semântico C ++, ele permanece uma instância do tipo
Node sem adquirir novos campos e métodos. Um ponteiro para ele agora é atribuído à variável
open_node , sem perder o tipo de
Node . Mas como trabalhar com um ponteiro de um tipo de
nó geral quando você precisa chamar um método, por exemplo, um parágrafo? Por exemplo,
open_bold () , que abre um nó de fonte em negrito? Afinal,
open_bold () é declarado e definido como um método da classe
Paragraph , e
Node não o
conhece completamente. Além disso,
open_node também
é declarado como um ponteiro para
Nó e deve aceitar métodos de todos os tipos de nós de abertura.
Existem duas soluções aqui: a óbvia e a correta. O óbvio para um iniciante é o
static_cast , e as funções virtuais estão corretas. Vamos primeiro examinar uma ramificação do analisador de comutador escrita usando o primeiro método:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::SECTION) { open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->open_bold(); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->open_bold(); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->open_bold(); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::LINK) open_node = static_cast<Link*>(open_node)->open_bold(); else
Nada mal. E agora, não vou arrastá-lo por muito tempo, mostrarei a mesma seção de código escrita usando funções virtuais:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::SECTION) { open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::UNORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else
O ganho é óbvio, mas realmente precisamos dele? Afinal, você deve declarar na classe
Node todos os métodos de todas as classes derivadas como virtuais e, de alguma forma, implementá-los em cada classe derivada. A resposta é sim, de fato. Não existem muitos métodos especificamente neste programa (29), e sua implementação em classes derivadas que não estão relacionadas a elas consiste em apenas uma linha:
throw string ("error!"); . Você pode ativar o modo criativo e criar uma linha exclusiva para cada lançamento de exceção. Mas o mais importante - devido à redução de código, o número de erros diminuiu. A transmissão é uma das causas mais importantes de erros no código. Porque, após aplicar
static_cast, o compilador para de xingar se a classe chamada estiver contida na classe especificada. Enquanto isso, classes diferentes podem conter métodos diferentes com o mesmo nome. No meu caso, 6 estava oculto no código !!! erros, enquanto um deles foi duplicado em várias ramificações do comutador. Aqui está:
else if (type == Node:: open_node = static_cast<Title*>(open_node)->open_italic();
Em seguida, nos spoilers, trago listagens completas da primeira e da segunda versões do analisador.
Analisador com fundição Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->add_text("\n"); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->add_text("\n"); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->add_text("\n"); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::LINK) { open_node = static_cast<Link*>(open_node)->add_text(lexer[i].lexeme); } else
Analisador com acesso a métodos virtuais Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH || type == Node::TITLE || type == Node::QUOTE || type == Node::TITLE || type == Node::QUOTE) open_node = open_node->add_text("\n"); else if (type == Node::UNORDERED_LIST || type == Node::ORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); } else
De 1357 linhas, o código foi reduzido para 487 - quase três vezes, sem contar o comprimento das linhas!Uma pergunta permanece: e quanto ao prazo de entrega? Quantos milissegundos temos que pagar pelo próprio computador para determinar o tipo de nó aberto? Realizei um experimento - corrigi o tempo de trabalho do analisador em milissegundos no primeiro e no segundo casos para o mesmo documento no meu computador doméstico. Aqui está o resultado:Fundição - 538 ms.Funções virtuais - 1174 ms.Total, 636 ms - uma taxa pela compactação do código e pela ausência de erros. Isso é muito? Possivelmente. Mas se precisarmos de um programa que funcione o mais rápido possível e exija a menor quantidade possível de memória, não iremos ao OOP e o escreveremos na linguagem assembly, gastando uma semana e arriscando-se a cometer um grande número de erros. Portanto, minha escolha é onde quer que static_cast e dynamic_cast se encontrem no programa , substitua-os por funções virtuais. Qual a sua opinião?