Hola Habr Si conoce la respuesta a la pregunta en el título, felicidades, no necesita este artículo. Está dirigido a principiantes en programación, como yo, que no siempre pueden comprender de manera independiente todas las complejidades de C ++ y otros lenguajes mecanografiados, y si pueden, es mejor aprender de los errores de otras personas de todos modos.
En este artículo, no solo responderé la pregunta "
¿Por qué necesitamos funciones virtuales en C ++? ", Sino que daré un ejemplo de mi práctica. Para una respuesta breve, puede recurrir a los motores de búsqueda que producen algo como lo siguiente: "
Se necesitan funciones virtuales para proporcionar polimorfismo, una de las tres ballenas OOP. Gracias a ellas, la máquina misma puede determinar el tipo de objeto por puntero sin cargar el programador con esta tarea "
. De acuerdo, pero la pregunta "por qué" permanece, aunque ahora significa un poco diferente: "
¿Por qué confiar en la máquina, gastar tiempo y memoria adicionales, si puede enviar el puntero usted mismo, porque el tipo de objeto al que se refiere es casi siempre conocido? " De hecho, el casting a primera vista deja las funciones virtuales sin trabajo, y es esto lo que se convierte en la causa de los conceptos erróneos y el código incorrecto. En proyectos pequeños, la pérdida es invisible, pero, como pronto verá, con el crecimiento del programa, las castas aumentan la lista en una progresión casi geométrica.
Primero, recordemos dónde se pueden necesitar castas y funciones virtuales. Un tipo se pierde cuando a un objeto declarado con el tipo A se le asigna una nueva operación para asignar memoria a un objeto de tipo B compatible con el tipo A, generalmente heredado de A. La mayoría de las veces el objeto no es uno, sino una matriz completa. Una matriz de punteros del mismo tipo, cada uno de los cuales espera la asignación de un área de memoria con objetos de tipos completamente diferentes. Aquí hay un ejemplo que consideraremos.
No lo alargaré por mucho tiempo, la tarea fue esta: basado en un documento marcado con el lenguaje de marcado de hipertexto Markedit (puede leer sobre esto
aquí ), construir un árbol de análisis y crear un archivo que contenga el mismo documento en el marcado HTML. Mi solución consiste en tres rutinas secuenciales: analizar el texto fuente en tokens, construir un árbol de sintaxis a partir de tokens y construir un documento HTML basado en él. Estamos interesados en la segunda parte.
El hecho es que los nodos del árbol de destino tienen diferentes tipos (sección, párrafo, nodo de texto, enlace, nota al pie, etc.), pero para los nodos principales, los punteros a los nodos secundarios se almacenan en la matriz y, por lo tanto, tienen un tipo: Nodo.
El analizador en sí, en una forma simplificada, funciona así: crea la "raíz" del árbol de sintaxis del
árbol con el tipo
Root , declara un puntero
open_node del tipo general
Node , al que se le asigna inmediatamente la dirección del
árbol , y la variable de
tipo del
tipo enumerado
Node_type , y luego el ciclo comienza,
iterando sobre los tokens desde el primer momento Hasta el final. En cada iteración, el tipo del nodo abierto
open_node se ingresa
primero en la variable de tipo (los tipos en forma de enumeración se almacenan en la estructura del nodo), seguido de la
instrucción switch , que verifica el tipo del siguiente token (los tipos de tokens ya son cuidadosamente proporcionados por el lexer). En cada rama del interruptor, se presenta otra rama que verifica la variable de
tipo , donde, como recordamos, está contenido el tipo del nodo abierto. Dependiendo de su valor, se realizan diferentes acciones, por ejemplo: agregar una lista de nodos de cierto tipo a un nodo abierto, abrir otro nodo de cierto tipo en un nodo abierto y pasar su dirección a
open_node , cerrar el nodo abierto, lanzar una excepción. Aplicable al tema del artículo, estamos interesados en el segundo ejemplo. Cada nodo abierto (y generalmente cada nodo que se puede abrir) ya contiene una matriz de punteros a nodos de tipo
Nodo . Por lo tanto, cuando abrimos un nuevo nodo en un nodo abierto (asignamos el área de memoria para un objeto de otro tipo al siguiente puntero de matriz), para un analizador semántico C ++, sigue siendo una instancia de tipo
Nodo sin adquirir nuevos campos y métodos. Ahora se asigna un puntero a la variable
open_node , sin perder el tipo de
nodo . Pero, ¿cómo trabajar con un puntero de un tipo de
nodo general cuando necesita llamar a un método, por ejemplo, un párrafo? Por ejemplo,
open_bold () , que abre un nodo de fuente en negrita en él? Después de todo,
open_bold () se declara y define como un método de la clase
Paragraph , y
Node lo desconoce por completo. Además,
open_node también se declara como un puntero a
Node , y debe aceptar métodos de todos los tipos de nodos de apertura.
Aquí hay dos soluciones: la obvia y la correcta. Obvio para un principiante es
static_cast , y las funciones virtuales son correctas. Veamos primero una rama del analizador de conmutador escrita usando el primer 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
No esta mal. Y ahora, no lo arrastraré por mucho tiempo, mostraré la misma sección de código escrita usando funciones virtuales:
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
La ganancia es obvia, pero ¿realmente la necesitamos? Después de todo, debe declarar en la clase
Node todos los métodos de todas las clases derivadas como virtuales y de alguna manera implementarlos en cada clase derivada. La respuesta es sí, de hecho. No hay tantos métodos específicamente en este programa (29), y su implementación en clases derivadas que no están relacionadas con ellos consiste en una sola línea:
throw string ("error!"); . Puede habilitar el modo creativo y crear una línea única para cada lanzamiento de excepción. Pero lo más importante: debido a la reducción del código, la cantidad de errores ha disminuido. La transmisión es una de las causas más importantes de errores en el código. Porque después de aplicar
static_cast, el compilador deja de jurar si la clase llamada está contenida en la clase dada. Mientras tanto, diferentes clases pueden contener diferentes métodos con el mismo nombre. En mi caso, ¡6 estaba oculto en el código! errores, mientras que uno de ellos se duplicó en varias ramas de conmutación. Aquí esta:
else if (type == Node:: open_node = static_cast<Title*>(open_node)->open_italic();
A continuación, debajo de los spoilers, traigo listas completas de la primera y segunda versión del analizador.
Analizador con casting 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
Analizador con acceso a métodos virtuales 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 líneas, el código se redujo a 487, ¡casi tres veces, sin contar la longitud de las líneas!Queda una pregunta: ¿qué pasa con el tiempo de entrega? ¿Cuántos milisegundos tenemos que pagar por la computadora para determinar el tipo de nodo abierto? Realicé un experimento: fijé el tiempo de trabajo del analizador en milisegundos en el primer y segundo casos para el mismo documento en la computadora de mi casa. Aquí está el resultado:Casting - 538 ms.Funciones virtuales - 1174 ms.Total, 636 ms: una tarifa por la compacidad del código y la ausencia de errores. ¿Es esto mucho? Posiblemente Pero si necesitamos un programa que funcione lo más rápido posible y requiera la menor memoria posible, no iríamos a OOP y lo escribiríamos en lenguaje ensamblador por completo, pasando una semana y arriesgándonos a cometer una gran cantidad de errores. Entonces, mi elección es donde static_cast y dynamic_cast se encuentran en el programa , reemplazarlos con funciones virtuales. Cual es tu opinion