Compreensão / requisitos para o polimorfismo
Para entender o polimorfismo - como o termo é usado na Ciência da Computação -, ajuda a começar com um teste simples e sua definição. Considerar:
Type1 x;
Type2 y;
f(x);
f(y);
Aqui, f()
é realizar alguma operação e receber valores x
e y
como entradas.
Para exibir polimorfismo, f()
deve ser capaz de operar com valores de pelo menos dois tipos distintos (por exemplo, int
e double
), localizando e executando códigos distintos de tipo apropriado.
Mecanismos C ++ para polimorfismo
Polimorfismo explícito especificado pelo programador
Você pode escrever de f()
forma que ele possa operar em vários tipos de qualquer uma das seguintes maneiras:
Pré-processando:
#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; }
Modelos:
template <typename T>
void f(T& x) { x += 2; }
Expedição 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
Outros mecanismos relacionados
O polimorfismo fornecido pelo compilador para tipos incorporados, conversões padrão e conversão / coerção são discutidos posteriormente para fins de completude como:
- eles são geralmente intuitivamente entendidos de qualquer maneira (justificando uma reação " oh, essa "),
- eles impactam o limiar ao exigir e uniformidade no uso dos mecanismos acima, e
- explicação é uma distração minuciosa de conceitos mais importantes.
Terminologia
Categorização adicional
Dados os mecanismos polimórficos acima, podemos categorizá-los de várias maneiras:
1 - Os modelos são extremamente flexíveis. O SFINAE (veja também std::enable_if
) permite efetivamente vários conjuntos de expectativas para o polimorfismo paramétrico. Por exemplo, você pode codificar que, quando o tipo de dados que você está processando tiver um .size()
membro, você usará uma função, caso contrário, outra função que não precisa .size()
(mas provavelmente sofre de alguma forma - por exemplo, usar o mais lento strlen()
ou não imprimir como útil uma mensagem no log). Você também pode especificar comportamentos ad-hoc quando o modelo é instanciado com parâmetros específicos, deixando alguns parâmetros paramétricos ( especialização parcial do modelo ) ou não ( especialização completa ).
"Polimórfico"
Alf Steinbach comenta que no padrão C ++ polimórfico refere-se apenas ao polimorfismo em tempo de execução usando despacho virtual. General Comp. Sci. o significado é mais abrangente, conforme o glossário do criador de C ++, Bjarne Stroustrup ( http://www.stroustrup.com/glossary.html ):
polimorfismo - fornecendo uma interface única para entidades de diferentes tipos. As funções virtuais fornecem polimorfismo dinâmico (tempo de execução) por meio de uma interface fornecida por uma classe base. Funções e modelos sobrecarregados fornecem polimorfismo estático (em tempo de compilação). TC ++ PL 12.2.6, 13.6.1, D&E 2.9.
Essa resposta - como a pergunta - relaciona os recursos do C ++ ao Comp. Sci. terminologia.
Discussão
Com o padrão C ++ usando uma definição mais restrita de "polimorfismo" que o Comp. Sci. comunidade, para garantir um entendimento mútuo para o seu público, considere ...
- usando terminologia inequívoca ("podemos tornar esse código reutilizável para outros tipos?" ou "podemos usar o despacho virtual?" em vez de "podemos tornar esse código polimórfico?") e / ou
- definindo claramente sua terminologia.
Ainda assim, o que é crucial para ser um ótimo programador de C ++ é entender o que o polimorfismo realmente está fazendo por você ...
permitindo escrever código "algorítmico" uma vez e depois aplicá-lo a muitos tipos de dados
... e esteja ciente de como os diferentes mecanismos polimórficos correspondem às suas necessidades reais.
O polimorfismo em tempo de execução é adequado:
- entrada processada por métodos de fábrica e cuspida como uma coleção de objetos heterogênea manipulada via
Base*
s,
- implementação escolhida em tempo de execução com base em arquivos de configuração, opções de linha de comando, configurações de interface do usuário etc.,
- a implementação variou em tempo de execução, como para um padrão de máquina de estado.
Quando não há um driver claro para o polimorfismo em tempo de execução, as opções em tempo de compilação geralmente são preferíveis. Considerar:
- o aspecto de compilar o que é chamado de classes modeladas é preferível a interfaces gordas que falham no tempo de execução
- SFINAE
- CRTP
- otimizações (muitas incluindo eliminação de inline e de código morto, desenrolamento de loop, matrizes estáticas baseadas em pilha e heap)
__FILE__
, __LINE__
concatenação literal de cadeias de caracteres e outros recursos exclusivos das macros (que permanecem ruins ;-))
- modelos e macros testam o uso semântico, mas não restringem artificialmente como esse suporte é fornecido (como o despacho virtual costuma exigir, substituindo exatamente as substituições da função de membro)
Outros mecanismos de apoio ao polimorfismo
Como prometido, para completar, vários tópicos periféricos são abordados:
- sobrecargas fornecidas pelo compilador
- conversões
- lança / coerção
Esta resposta termina com uma discussão de como as opções acima se combinam para capacitar e simplificar o código polimórfico - especialmente o polimorfismo paramétrico (modelos e macros).
Mecanismos de mapeamento para operações específicas de tipo
> Sobrecargas implícitas fornecidas pelo compilador
Conceitualmente, o compilador sobrecarrega muitos operadores para tipos internos. Não é conceitualmente diferente da sobrecarga especificada pelo usuário, mas é listada, pois é facilmente ignorada. Por exemplo, você pode adicionar ao int
s e double
s usando a mesma notação x += 2
eo compilador produz:
- instruções de CPU específicas do tipo
- um resultado do mesmo tipo.
A sobrecarga se estende sem problemas para tipos definidos pelo usuário:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Sobrecargas fornecidas pelo compilador para tipos básicos são comuns em linguagens de computador de alto nível (3GL +), e a discussão explícita do polimorfismo geralmente implica algo mais. (2GLs - linguagens assembly - geralmente exigem que o programador use explicitamente diferentes mnemônicos para diferentes tipos.)
> Conversões padrão
A quarta seção do padrão C ++ descreve as conversões padrão.
O primeiro ponto resume bem (de um rascunho antigo - espero que ainda esteja substancialmente correto):
-1- Conversões padrão são conversões implícitas definidas para tipos internos. A cláusula conv enumera o conjunto completo dessas conversões. Uma sequência de conversão padrão é uma sequência de conversões padrão na seguinte ordem:
Conversão zero ou uma do conjunto a seguir: conversão lvalue para rvalue, conversão de matriz em ponteiro e conversão de função em ponteiro.
Conversão zero ou uma do conjunto a seguir: promoções integrais, promoção de ponto flutuante, conversões integrais, conversões de ponto flutuante, conversões integrais flutuantes, conversões de ponteiro, conversões de ponteiro, conversões de ponteiro para membro e conversões booleanas.
Zero ou uma conversão de qualificação.
[Nota: uma sequência de conversão padrão pode estar vazia, ou seja, não pode conter conversões. ] Uma sequência de conversão padrão será aplicada a uma expressão, se necessário, para convertê-la em um tipo de destino necessário.
Essas conversões permitem códigos como:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Aplicando o teste anterior:
Para ser polimórfico, [ a()
] deve ser capaz de operar com valores de pelo menos dois tipos distintos (por exemplo, int
e double
), localizando e executando o código apropriado ao tipo .
a()
ele próprio executa código especificamente para double
e, portanto, não é polimórfico.
Mas, na segunda chamada para a()
o compilador sabe para gerar código de tipo apropriado para uma "promoção de ponto flutuante" (Standard §4) para converter 42
a 42.0
. Esse código extra está na função de chamada . Discutiremos o significado disso na conclusão.
> Coerção, elencos, construtores implícitos
Esses mecanismos permitem que as classes definidas pelo usuário especifiquem comportamentos semelhantes às conversões padrão dos tipos internos. Vamos dar uma olhada:
int a, b;
if (std::cin >> a >> b)
f(a, b);
Aqui, o objeto std::cin
é avaliado em um contexto booleano, com a ajuda de um operador de conversão. Isso pode ser agrupado conceitualmente com "promoções integrais" e outras das conversões padrão no tópico acima.
Construtores implícitos efetivamente fazem a mesma coisa, mas são controlados pelo tipo de conversão:
f(const std::string& x);
f("hello"); // invokes `std::string::string(const char*)`
Implicações de sobrecargas, conversões e coerção fornecidas pelo compilador
Considerar:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Se queremos que a quantia x
seja tratada como um número real durante a divisão (ou seja, 6,5 em vez de arredondada para 6), precisamos apenas mudar para typedef double Amount
.
Isso é bom, mas não teria sido também muito trabalho para tornar o código explicitamente "tipo correto":
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;
} }
Mas considere que podemos transformar a primeira versão em template
:
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
É por causa desses pequenos "recursos de conveniência" que ele pode ser facilmente instanciado para um int
ou outro double
e funcionar como pretendido. Sem esses recursos, precisaríamos de elencos explícitos, traços de tipo e / ou classes de política, alguma confusão detalhada e propensa a erros 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;
}
Portanto, sobrecarga de operador fornecida pelo compilador para tipos internos, conversões padrão, fundição / coerção / construtores implícitos - todos eles contribuem com suporte sutil ao polimorfismo. A partir da definição na parte superior desta resposta, eles abordam "localizando e executando código apropriado ao tipo" mapeando:
Eles não estabelecem contextos polimórficos por si mesmos, mas ajudam a capacitar / simplificar o código dentro de tais contextos.
Você pode se sentir enganado ... não parece muito. O significado é que, em contextos polimórficos paramétricos (ou seja, dentro de modelos ou macros), estamos tentando oferecer suporte a uma variedade arbitrariamente grande de tipos, mas geralmente queremos expressar operações sobre eles em termos de outras funções, literais e operações projetadas para um pequeno conjunto de tipos. Reduz a necessidade de criar funções ou dados quase idênticos por tipo quando a operação / valor é logicamente o mesmo. Esses recursos cooperam para adicionar uma atitude de "melhor esforço", fazendo o que é intuitivamente esperado, usando as funções e os dados disponíveis limitados e parando apenas com um erro quando há ambiguidade real.
Isso ajuda a limitar a necessidade de código polimórfico que suporte código polimórfico, criando uma rede mais rígida em torno do uso de polimorfismo, para que o uso localizado não force o uso generalizado e disponibilizando os benefícios do polimorfismo conforme necessário, sem impor os custos de expor a implementação em tempo de compilação, tenha várias cópias da mesma função lógica no código do objeto para dar suporte aos tipos usados e ao realizar expedição virtual em vez de chamadas internas ou, pelo menos, resolvidas em tempo de compilação. Como é típico em C ++, o programador tem muita liberdade para controlar os limites dentro dos quais o polimorfismo é usado.