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 xe ycomo entradas.
Para exibir polimorfismo, f()deve ser capaz de operar com valores de pelo menos dois tipos distintos (por exemplo, inte 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 ints e doubles usando a mesma notação x += 2eo 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, inte double), localizando e executando o código apropriado ao tipo .
a()ele próprio executa código especificamente para doublee, 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 42a 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 xseja 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 intou outro doublee 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.