Comprensione / requisiti per il polimorfismo
Comprendere il polimorfismo - come il termine è usato in Informatica - aiuta a partire da un semplice test e definizione di esso. Tener conto di:
Type1 x;
Type2 y;
f(x);
f(y);
Qui, f()
è di eseguire alcune operazioni e viene dato valori x
e y
come input.
Per esibire polimorfismo, f()
deve essere in grado di operare con valori di almeno due tipi distinti (es. int
E double
), trovando ed eseguendo un codice distinto appropriato al tipo.
Meccanismi C ++ per polimorfismo
Polimorfismo esplicito specificato dal programmatore
Puoi scrivere in modo f()
tale che possa operare su più tipi in uno dei seguenti modi:
pre-elaborazione:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
Sovraccarico:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
Modelli:
template <typename T>
void f(T& x) { x += 2; }
Invio virtuale:
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
Altri meccanismi correlati
Il polimorfismo fornito dal compilatore per i tipi incorporati, le conversioni standard e il casting / coercizione sono discussi in seguito per completezza come:
- sono comunque comunemente comprensibili intuitivamente (che garantisce una reazione " oh, quella "),
- incidono sulla soglia nel richiedere e senza soluzione di continuità nell'uso i meccanismi di cui sopra e
- la spiegazione è una seccante distrazione da concetti più importanti.
Terminologia
Ulteriore categorizzazione
Dati i meccanismi polimorfici sopra, possiamo classificarli in vari modi:
1 - I modelli sono estremamente flessibili. SFINAE (vedi anche std::enable_if
) consente effettivamente diverse serie di aspettative per il polimorfismo parametrico. Ad esempio, potresti codificare che quando il tipo di dati che stai elaborando ha un .size()
membro, utilizzerai una funzione, altrimenti un'altra funzione che non è necessaria .size()
(ma presumibilmente soffre in qualche modo, ad esempio utilizzando il più lento strlen()
o non stampare come utile un messaggio nel registro). È inoltre possibile specificare comportamenti ad hoc quando il modello viene istanziato con parametri specifici, lasciando alcuni parametri parametrici ( specializzazione parziale del modello ) o meno ( specializzazione completa ).
"Polimorfica"
Alf Steinbach commenta che nello standard C ++ il polimorfismo si riferisce solo al polimorfismo di runtime usando l'invio virtuale. Comp. Generale Sci. il significato è più inclusivo, secondo il glossario del creatore C ++ Bjarne Stroustrup ( http://www.stroustrup.com/glossary.html ):
polimorfismo: fornire un'unica interfaccia a entità di diverso tipo. Le funzioni virtuali forniscono polimorfismo dinamico (runtime) attraverso un'interfaccia fornita da una classe base. Le funzioni e i template sovraccaricati forniscono polimorfismo statico (tempo di compilazione). TC ++ PL 12.2.6, 13.6.1, D&E 2.9.
Questa risposta - come la domanda - mette in relazione le funzionalità C ++ con il Comp. Sci. terminologia.
Discussione
Con lo standard C ++ si usa una definizione più ristretta di "polimorfismo" rispetto al Comp. Sci. comunità, per garantire la comprensione reciproca per il tuo pubblico considera ...
- usando una terminologia non ambigua ("possiamo rendere questo codice riutilizzabile per altri tipi?" o "possiamo usare il dispacciamento virtuale?" piuttosto che "possiamo rendere questo codice polimorfico?"), e / o
- definire chiaramente la tua terminologia.
Tuttavia, ciò che è cruciale per essere un grande programmatore C ++ è capire cosa sta realmente facendo il polimorfismo per te ...
che consente di scrivere una volta il codice "algoritmico" e quindi applicarlo a molti tipi di dati
... e quindi sii molto consapevole di come i diversi meccanismi polimorfici soddisfano i tuoi bisogni reali.
Polimorfismi run-time:
- input elaborato con metodi di fabbrica e sputato come una raccolta di oggetti eterogenea gestita tramite
Base*
s,
- implementazione scelta in fase di esecuzione in base a file di configurazione, opzioni della riga di comando, impostazioni dell'interfaccia utente ecc.,
- l'implementazione variava in fase di esecuzione, ad esempio per un modello di macchina a stati.
Quando non esiste un chiaro driver per il polimorfismo di runtime, le opzioni di compilazione sono spesso preferibili. Tener conto di:
- l'aspetto di compilazione-quello che viene chiamato delle classi basate su modelli è preferibile alle interfacce fat che falliscono in fase di esecuzione
- SFINAE
- CRTP
- ottimizzazioni (molte tra cui inline ed eliminazione di dead code, srotolamento di loop, array basati su stack statici vs heap)
__FILE__
, __LINE__
concatenazione letterale di stringhe e altre capacità uniche di macro (che rimangono malvagie ;-))
- modelli e macro testano l'utilizzo semantico supportato, ma non limitano artificialmente il modo in cui viene fornito quel supporto (come tende la spedizione virtuale richiedendo esattamente le sostituzioni della funzione membro corrispondente)
Altri meccanismi a supporto del polimorfismo
Come promesso, per completezza vengono trattati diversi argomenti periferici:
- sovraccarichi forniti dal compilatore
- conversioni
- calchi / coercizione
Questa risposta si conclude con una discussione sul modo in cui quanto sopra si combina per potenziare e semplificare il codice polimorfico - in particolare il polimorfismo parametrico (modelli e macro).
Meccanismi per la mappatura su operazioni specifiche del tipo
> Sovraccarichi impliciti forniti dal compilatore
Concettualmente, il compilatore sovraccarica molti operatori per i tipi predefiniti. Non è concettualmente diverso dal sovraccarico specificato dall'utente, ma è elencato in quanto facilmente trascurabile. Ad esempio, è possibile aggiungere a int
s e double
s utilizzando la stessa notazione x += 2
e il compilatore produce:
- istruzioni specifiche per CPU
- un risultato dello stesso tipo.
Il sovraccarico si estende quindi perfettamente ai tipi definiti dall'utente:
std::string x;
int y = 0;
x += 'c';
y += 'c';
I sovraccarichi forniti dal compilatore per i tipi di base sono comuni nei linguaggi informatici di alto livello (3GL +) e la discussione esplicita del polimorfismo implica generalmente qualcosa di più. (2GLs - linguaggi di assemblaggio - spesso richiedono al programmatore di usare esplicitamente mnemonici diversi per tipi diversi.)
> Conversioni standard
La quarta sezione dello standard C ++ descrive le conversioni standard.
Il primo punto riassume bene (da una vecchia bozza - si spera ancora sostanzialmente corretto):
-1- Le conversioni standard sono conversioni implicite definite per i tipi predefiniti. La clausola conv enumera la serie completa di tali conversioni. Una sequenza di conversione standard è una sequenza di conversioni standard nel seguente ordine:
Zero o una conversione dal seguente set: conversione da lvalue a rvalue, conversione da array a puntatore e conversione da funzione a puntatore.
Zero o una conversione dal seguente set: promozioni integrali, promozione in virgola mobile, conversioni integrali, conversioni in virgola mobile, conversioni in virgola mobile integrale, conversioni da puntatore, conversioni da puntatore a membro e conversioni booleane.
Zero o una conversione di qualificazione.
[Nota: una sequenza di conversione standard può essere vuota, ovvero non può consistere in conversioni. ] Se necessario, una sequenza di conversione standard verrà applicata a un'espressione per convertirla in un tipo di destinazione richiesto.
Queste conversioni consentono codice come:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Applicazione del test precedente:
Per essere polimorfico, [ a()
] deve essere in grado di operare con valori di almeno due tipi distinti (ad es. int
E double
), trovando ed eseguendo il codice di tipo appropriato .
a()
si esegue il codice specifico per double
ed è quindi non polimorfica.
Ma, in seconda convocazione per a()
il compilatore sa per generare il codice di tipo appropriato per un "punto di promozione flottante" (Standard § 4) per convertire 42
a 42.0
. Quel codice extra è nella funzione chiamante . Discuteremo il significato di questo nella conclusione.
> Coercizione, cast, costruttori impliciti
Questi meccanismi consentono alle classi definite dall'utente di specificare comportamenti affini alle conversioni standard dei tipi predefiniti. Diamo un'occhiata:
int a, b;
if (std::cin >> a >> b)
f(a, b);
Qui, l'oggetto std::cin
viene valutato in un contesto booleano, con l'aiuto di un operatore di conversione. Questo può essere concettualmente raggruppato con "promozioni integrali" e altri dalle conversioni standard nell'argomento sopra.
I costruttori impliciti fanno effettivamente la stessa cosa, ma sono controllati dal tipo cast-to:
f(const std::string& x);
f("hello"); // invokes `std::string::string(const char*)`
Implicazioni di sovraccarichi forniti dal compilatore, conversioni e coercizione
Tener conto di:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Se vogliamo che l'importo x
venga trattato come un numero reale durante la divisione (ovvero sia 6,5 anziché arrotondato per difetto a 6), dobbiamo solo cambiare typedef double Amount
.
È bello, ma non sarebbe stato troppo lavoro per rendere esplicitamente il codice "tipo corretto":
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;
} }
Ma considera che possiamo trasformare la prima versione in un template
:
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
È a causa di quelle piccole "funzionalità di praticità" che può essere facilmente istanziato per o int
o double
e come previsto. Senza queste funzionalità, avremmo bisogno di cast espliciti, tratti di tipo e / o classi di policy, un pasticcio verboso e soggetto a errori come:
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;
}
Pertanto, sovraccarico dell'operatore fornito dal compilatore per tipi incorporati, conversioni standard, costruttori di casting / coercizione / impliciti - tutti contribuiscono in modo sottile al polimorfismo. Dalla definizione nella parte superiore di questa risposta, affrontano "la ricerca e l'esecuzione del codice appropriato per tipo" mappando:
Essi non stabiliscono contesti polimorfici da soli, ma aiutano empower / code semplificare all'interno di tali contesti.
Potresti sentirti tradito ... non sembra molto. Il significato è che in contesti polimorfici parametrici (cioè all'interno di modelli o macro), stiamo cercando di supportare una gamma arbitrariamente ampia di tipi ma spesso vogliamo esprimere operazioni su di essi in termini di altre funzioni, valori letterali e operazioni progettati per un piccolo set di tipi. Riduce la necessità di creare funzioni o dati quasi identici per tipo, quando l'operazione / valore è logicamente lo stesso. Queste caratteristiche cooperano per aggiungere un atteggiamento di "miglior sforzo", facendo ciò che è intuitivamente previsto utilizzando le funzioni e i dati disponibili limitati e fermandosi con un errore solo quando c'è vera ambiguità.
Questo aiuta a limitare la necessità di un codice polimorfico che supporti il codice polimorfico, disegnando una rete più stretta attorno all'uso del polimorfismo in modo che l'uso localizzato non imponga un uso diffuso e rende disponibili i benefici del polimorfismo secondo necessità senza imporre i costi di dover esporre l'implementazione a tempo di compilazione, disporre di più copie della stessa funzione logica nel codice oggetto per supportare i tipi utilizzati e nel fare invio virtuale invece di chiamate inline o almeno in fase di compilazione risolte. Come è tipico in C ++, al programmatore viene data molta libertà di controllare i confini entro i quali viene utilizzato il polimorfismo.