Comprensión de / requisitos para el polimorfismo
Para comprender el polimorfismo, como se usa el término en Ciencias de la Computación, es útil comenzar con una prueba simple y su definición. Considerar:
Type1 x;
Type2 y;
f(x);
f(y);
Aquí, f()
es realizar alguna operación y se le están dando valores x
y y
como entradas.
Para polimorfismo exposición, f()
debe ser capaz de funcionar con valores de al menos dos distinto tipos (por ejemplo, int
y double
), encontrando y ejecutando un código distinto apropiado para el tipo.
Mecanismos C ++ para polimorfismo
Polimorfismo explícito especificado por el programador
Puedes escribir f()
manera que pueda operar en varios tipos de cualquiera de las siguientes maneras:
Preprocesamiento:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
Sobrecarga:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
Plantillas:
template <typename T>
void f(T& x) { x += 2; }
Despacho virtual:
struct Base { virtual Base& operator+=(int) = 0; };
struct X : Base
{
X(int n) : n_(n) { }
X& operator+=(int n) { n_ += n; return *this; }
int n_;
};
struct Y : Base
{
Y(double n) : n_(n) { }
Y& operator+=(int n) { n_ += n; return *this; }
double n_;
};
void f(Base& x) { x += 2; } // run-time polymorphic dispatch
Otros mecanismos relacionados
El polimorfismo proporcionado por el compilador para los tipos incorporados, las conversiones estándar y la conversión / coerción se analizan más adelante para completarlo como:
- son comúnmente entendidos intuitivamente de todos modos (garantizando un " reacción de oh, esa "),
- afectan el umbral al requerir y la fluidez en el uso de los mecanismos anteriores, y
- La explicación es una distracción incómoda de conceptos más importantes.
Terminología
Categorización adicional
Dados los mecanismos polimórficos anteriores, podemos clasificarlos de varias maneras:
1 - Las plantillas son extremadamente flexibles. SFINAE (ver también std::enable_if
) efectivamente permite varios conjuntos de expectativas para el polimorfismo paramétrico. Por ejemplo, puede codificar que cuando el tipo de datos que está procesando tiene un .size()
miembro, usará una función, de lo contrario, otra función que no necesita .size()
(pero presumiblemente sufre de alguna manera, por ejemplo, usar la strlen()
impresión más lenta o no útil un mensaje en el registro). También puede especificar comportamientos ad-hoc cuando la plantilla se instancia con parámetros específicos, ya sea dejando algunos parámetros paramétricos ( especialización parcial de plantilla ) o no ( especialización completa ).
"Polimórfico"
Alf Steinbach comenta que en el polimórfico estándar de C ++ solo se refiere al polimorfismo en tiempo de ejecución mediante el envío virtual. General Comp. Sci. el significado es más inclusivo, según el glosario del creador de C ++, Bjarne Stroustrup ( http://www.stroustrup.com/glossary.html ):
polimorfismo: proporciona una interfaz única a entidades de diferentes tipos. Las funciones virtuales proporcionan polimorfismo dinámico (tiempo de ejecución) a través de una interfaz proporcionada por una clase base. Las funciones y plantillas sobrecargadas proporcionan polimorfismo estático (tiempo de compilación). TC ++ PL 12.2.6, 13.6.1, D&E 2.9.
Esta respuesta, como la pregunta, relaciona las características de C ++ con el Comp. Sci. terminología.
Discusión
Con el estándar C ++ que utiliza una definición más estrecha de "polimorfismo" que el Comp. Sci. comunidad, para garantizar la comprensión mutua de su audiencia, considere ...
- usando una terminología inequívoca ("¿podemos hacer que este código sea reutilizable para otros tipos?" o "¿podemos usar el envío virtual?" en lugar de "¿podemos hacer que este código sea polimórfico?"), y / o
- Definiendo claramente su terminología.
Aún así, lo que es crucial para ser un gran programador de C ++ es entender lo que el polimorfismo realmente está haciendo por usted ...
permitiéndole escribir código "algorítmico" una vez y luego aplicarlo a muchos tipos de datos
... y luego sea muy consciente de cómo los diferentes mecanismos polimórficos satisfacen sus necesidades reales.
Trajes de polimorfismo en tiempo de ejecución:
- entrada procesada por métodos de fábrica y escupida como una colección de objetos heterogéneos manejada a través de
Base*
s,
- implementación elegida en tiempo de ejecución basada en archivos de configuración, modificadores de línea de comandos, configuraciones de IU, etc.
- la implementación varió en tiempo de ejecución, como para un patrón de máquina de estado.
Cuando no hay un controlador claro para el polimorfismo en tiempo de ejecución, a menudo son preferibles las opciones de tiempo de compilación. Considerar:
- el aspecto de compilar lo que se llama de las clases con plantillas es preferible a las interfaces gordas que fallan en tiempo de ejecución
- SFINAE
- CRTP
- optimizaciones (muchas de ellas incluyen eliminación de código inactivo y en línea, desenrollado de bucles, matrices basadas en pila estática frente a montón)
__FILE__
, __LINE__
concatenación literal de cadenas y otras capacidades únicas de macros (que siguen siendo malvadas ;-))
- Se admite el uso semántico de las plantillas y macros de prueba, pero no restrinja artificialmente cómo se proporciona ese soporte (ya que el despacho virtual tiende a requerir que se anulen las funciones de los miembros que coinciden exactamente)
Otros mecanismos que apoyan el polimorfismo
Según lo prometido, para completar, se cubren varios temas periféricos:
- sobrecargas proporcionadas por el compilador
- conversiones
- moldes / coerción
Esta respuesta concluye con una discusión sobre cómo se combina lo anterior para potenciar y simplificar el código polimórfico, especialmente el polimorfismo paramétrico (plantillas y macros).
Mecanismos para mapear operaciones específicas de tipo
> Sobrecargas implícitas proporcionadas por el compilador
Conceptualmente, el compilador sobrecarga muchos operadores para los tipos incorporados. No es conceptualmente diferente de la sobrecarga especificada por el usuario, pero se enumera porque se pasa por alto fácilmente. Por ejemplo, se puede añadir a int
s y double
s utilizando la misma notación x += 2
y el compilador produce:
- instrucciones de CPU específicas del tipo
- Un resultado del mismo tipo.
La sobrecarga se extiende sin problemas a los tipos definidos por el usuario:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Las sobrecargas proporcionadas por el compilador para los tipos básicos son comunes en los lenguajes informáticos de alto nivel (3GL +), y la discusión explícita del polimorfismo generalmente implica algo más. (2GLs - lenguajes de ensamblaje - a menudo requieren que el programador use explícitamente diferentes mnemónicos para diferentes tipos).
> Conversiones estándar
La cuarta sección del estándar C ++ describe las conversiones estándar.
El primer punto resume muy bien (de un borrador anterior, con suerte todavía sustancialmente correcto):
-1- Las conversiones estándar son conversiones implícitas definidas para los tipos integrados. La cláusula conv enumera el conjunto completo de tales conversiones. Una secuencia de conversión estándar es una secuencia de conversiones estándar en el siguiente orden:
Cero o una conversión del siguiente conjunto: conversión lvalue-to-rvalue, conversión de matriz a puntero y conversión de función a puntero.
Cero o una conversión del siguiente conjunto: promociones integrales, promoción de punto flotante, conversiones integrales, conversiones de punto flotante, conversiones integrales flotantes, conversiones de puntero, conversiones de puntero a miembro y conversiones booleanas.
Cero o una conversión de calificación.
[Nota: una secuencia de conversión estándar puede estar vacía, es decir, no puede consistir en conversiones. ] Se aplicará una secuencia de conversión estándar a una expresión si es necesario para convertirla a un tipo de destino requerido.
Estas conversiones permiten códigos como:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Aplicando la prueba anterior:
Para ser polimórfico, [ a()
] debe poder operar con valores de al menos dos tipos distintos (por ejemplo, int
y double
), encontrando y ejecutando código apropiado para cada tipo .
a()
en sí mismo ejecuta código específicamente para double
y, por lo tanto, no es polimórfico.
Sin embargo, en la segunda convocatoria para a()
el compilador sabe para generar el código de tipo apropiado para una "promoción de coma flotante" (Norma § 4) para convertir 42
a 42.0
. Ese código adicional está en la función de llamada . Discutiremos la importancia de esto en la conclusión.
> Coerción, moldes, constructores implícitos
Estos mecanismos permiten que las clases definidas por el usuario especifiquen comportamientos similares a las conversiones estándar de los tipos incorporados. Echemos un vistazo:
int a, b;
if (std::cin >> a >> b)
f(a, b);
Aquí, el objeto std::cin
se evalúa en un contexto booleano, con la ayuda de un operador de conversión. Esto se puede agrupar conceptualmente con "promociones integrales" y otros de las conversiones estándar en el tema anterior.
Los constructores implícitos efectivamente hacen lo mismo, pero están controlados por el tipo de conversión:
f(const std::string& x);
f("hello"); // invokes `std::string::string(const char*)`
Implicaciones de sobrecargas, conversiones y coerción proporcionadas por el compilador
Considerar:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Si queremos que la cantidad x
sea tratada como un número real durante la división (es decir, ser 6.5 en lugar de redondearse a 6), solo necesitamos cambiar a typedef double Amount
.
Eso es bueno, pero no habría sido demasiado trabajo hacer que el código explícitamente "escriba correcto":
void f() void f()
{ {
typedef int Amount; typedef double Amount;
Amount x = 13; Amount x = 13.0;
x /= 2; x /= 2.0;
std::cout << double(x) * 1.1; std::cout << x * 1.1;
} }
Pero considere que podemos transformar la primera versión en template
:
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Debido a esas pequeñas "características de conveniencia", se puede crear una instancia tan fácil para cualquiera int
o double
para trabajar según lo previsto. Sin estas características, necesitaríamos conversiones explícitas, rasgos de tipo y / o clases de políticas, algún desorden detallado y propenso a errores como:
template <typename Amount, typename Policy>
void f()
{
Amount x = Policy::thirteen;
x /= static_cast<Amount>(2);
std::cout << traits<Amount>::to_double(x) * 1.1;
}
Por lo tanto, la sobrecarga del operador proporcionada por el compilador para tipos incorporados, conversiones estándar, fundición / coerción / constructores implícitos: todos contribuyen con un sutil soporte para el polimorfismo. Desde la definición en la parte superior de esta respuesta, abordan "encontrar y ejecutar código apropiado para el tipo" mediante el mapeo:
Ellos no establecen contextos polimórficos por sí mismos, pero sí ayudar a potenciar / código de simplificar el interior de dichos contextos.
Puede sentirse engañado ... no parece mucho. La importancia es que en contextos polimórficos paramétricos (es decir, dentro de plantillas o macros), estamos tratando de admitir una amplia gama de tipos arbitrariamente, pero a menudo queremos expresar operaciones sobre ellos en términos de otras funciones, literales y operaciones que fueron diseñadas para un Pequeño conjunto de tipos. Reduce la necesidad de crear funciones o datos casi idénticos por tipo cuando la operación / valor es lógicamente el mismo. Estas características cooperan para agregar una actitud de "mejor esfuerzo", haciendo lo que intuitivamente se espera mediante el uso de las funciones y los datos disponibles limitados y solo se detienen con un error cuando existe una ambigüedad real.
Esto ayuda a limitar la necesidad de código polimórfico que admita código polimórfico, dibujando una red más ajustada alrededor del uso del polimorfismo para que el uso localizado no fuerce el uso generalizado, y haciendo que los beneficios del polimorfismo estén disponibles según sea necesario sin imponer los costos de tener que exponer la implementación en tiempo de compilación, tener múltiples copias de la misma función lógica en el código objeto para admitir los tipos utilizados, y al hacer despacho virtual en lugar de llamadas en línea o al menos llamadas resueltas en tiempo de compilación. Como es típico en C ++, el programador tiene mucha libertad para controlar los límites dentro de los cuales se usa el polimorfismo.