Innanzitutto dobbiamo tornare a cosa significa passare per valore e per riferimento.
Per linguaggi come Java e SML, il passaggio per valore è semplice (e non esiste un passaggio per riferimento), così come lo è la copia di un valore di variabile, poiché tutte le variabili sono solo scalari e hanno una copia semantica incorporata: sono entrambi ciò che conta come aritmetica digitare C ++ o "riferimenti" (puntatori con nome e sintassi diversi).
In C abbiamo tipi scalari e definiti dall'utente:
- Gli scalari hanno un valore numerico o astratto (i puntatori non sono numeri, hanno un valore astratto) che viene copiato.
- Ai tipi aggregati vengono copiati tutti i membri eventualmente inizializzati:
- per tipi di prodotto (matrici e strutture): ricorsivamente, tutti i membri delle strutture e gli elementi delle matrici vengono copiati (la sintassi della funzione C non consente di passare direttamente le matrici per valore, ma solo i membri delle matrici di una struttura, ma questo è un dettaglio ).
- per i tipi di somma (sindacati): viene conservato il valore del "membro attivo"; ovviamente, copia membro per membro non è in ordine in quanto non tutti i membri possono essere inizializzati.
In C ++ i tipi definiti dall'utente possono avere una copia semantica definita dall'utente, che consente una programmazione veramente "orientata agli oggetti" con oggetti con proprietà delle loro risorse e operazioni di "copia profonda". In tal caso, un'operazione di copia è in realtà una chiamata a una funzione che può quasi eseguire operazioni arbitrarie.
Per le strutture C compilate come C ++, la "copia" è ancora definita come chiamata all'operazione di copia definita dall'utente (costruttore o operatore di assegnazione), che sono generati implicitamente dal compilatore. Significa che la semantica di un programma di sottoinsieme comune C / C ++ è diversa in C e C ++: in C viene copiato un intero tipo di aggregato, in C ++ viene chiamata una funzione di copia generata implicitamente per copiare ciascun membro; il risultato finale è che in entrambi i casi ogni membro viene copiato.
(C'è un'eccezione, penso, quando viene copiata una struttura all'interno di un'unione.)
Quindi, per un tipo di classe, l'unico modo (copie esterne al sindacato) per creare una nuova istanza è tramite un costruttore (anche per quelli con banali costruttori generati dal compilatore).
Non è possibile prendere l'indirizzo di un valore tramite operatore unario, &
ma ciò non significa che non vi sia alcun oggetto valore; e un oggetto, per definizione, ha un indirizzo ; e quell'indirizzo è persino rappresentato da un costrutto di sintassi: un oggetto di tipo classe può essere creato solo da un costruttore e ha un this
puntatore; ma per tipi banali, non esiste un costruttore scritto dall'utente, quindi non c'è posto dove mettere this
fino a dopo che la copia è stata costruita e denominata.
Per il tipo scalare, il valore di un oggetto è il valore dell'oggetto, il valore matematico puro memorizzato nell'oggetto.
Per un tipo di classe, l'unica nozione di un valore dell'oggetto è un'altra copia dell'oggetto, che può essere fatta solo da un costruttore di copie, una funzione reale (anche se per tipi banali tale funzione è così particolarmente banale, a volte possono essere creato senza chiamare il costruttore). Ciò significa che il valore dell'oggetto è il risultato del cambiamento dello stato del programma globale da parte di un'esecuzione . Non accede matematicamente.
Quindi passare per valore in realtà non è una cosa: è passare per copia la chiamata del costruttore , che è meno carina. Il costruttore di copie dovrebbe eseguire un'operazione di "copia" ragionevole in base alla semantica corretta del tipo di oggetto, rispettando le sue invarianti interne (che sono proprietà utente astratte, non proprietà intrinseche C ++).
Passare per valore di un oggetto classe significa:
- crea un'altra istanza
- quindi attiva la funzione chiamata su quell'istanza.
Si noti che il problema non ha nulla a che fare con il fatto che la copia stessa sia un oggetto con un indirizzo: tutti i parametri delle funzioni sono oggetti e hanno un indirizzo (a livello semantico della lingua).
Il problema è se:
- la copia è un nuovo oggetto inizializzato con il puro valore matematico (vero valore puro) dell'oggetto originale, come con gli scalari;
- oppure la copia è il valore dell'oggetto originale , come con le classi.
Nel caso di un tipo di classe banale, è ancora possibile definire il membro della copia del membro dell'originale, in modo da poter definire il valore puro dell'originale a causa della banalità delle operazioni di copia (costruttore della copia e assegnazione). Non così con le funzioni utente speciali arbitrarie: un valore dell'originale deve essere una copia costruita.
Gli oggetti di classe devono essere costruiti dal chiamante; un costruttore ha formalmente un this
puntatore ma qui il formalismo non è rilevante: tutti gli oggetti hanno formalmente un indirizzo ma solo quelli che effettivamente usano il loro indirizzo in modi non puramente locali (a differenza *&i = 1;
dell'uso puramente locale dell'indirizzo) devono avere un ben definito indirizzo.
Un oggetto deve assolutamente essere passato per indirizzo se deve avere un indirizzo in entrambe queste due funzioni compilate separatamente:
void callee(int &i) {
something(&i);
}
void caller() {
int i;
callee(i);
something(&i);
}
Qui anche se something(address)
è una funzione o macro pura o qualsiasi altra cosa (come printf("%p",arg)
) che non può memorizzare l'indirizzo o comunicare con un'altra entità, abbiamo il requisito di passare per indirizzo perché l'indirizzo deve essere ben definito per un oggetto unico int
che ha un unico identità.
Non sappiamo se una funzione esterna sarà "pura" in termini di indirizzi passati ad essa.
Qui il potenziale per un uso reale dell'indirizzo in un costruttore o in un distruttore non banale dal lato del chiamante è probabilmente la ragione per prendere il percorso sicuro e semplicistico e dare all'oggetto un'identità nel chiamante e passare il suo indirizzo, mentre fa certo che qualsiasi uso non banale del suo indirizzo nel costruttore, dopo la costruzione e nel distruttore sia coerente : this
deve apparire come lo stesso sull'esistenza dell'oggetto.
Un costruttore o distruttore non banale come qualsiasi altra funzione può usare il this
puntatore in un modo che richiede coerenza sul suo valore anche se alcuni oggetti con oggetti non banali potrebbero non:
struct file_handler { // don't use that class!
file_handler () { this->fileno = -1; }
file_handler (int f) { this->fileno = f; }
file_handler (const file_handler& rhs) {
if (this->fileno != -1)
this->fileno = dup(rhs.fileno);
else
this->fileno = -1;
}
~file_handler () {
if (this->fileno != -1)
close(this->fileno);
}
file_handler &operator= (const file_handler& rhs);
};
Si noti che in quel caso, nonostante l'uso esplicito di un puntatore (sintassi esplicita this->
), l'identità dell'oggetto è irrilevante: il compilatore potrebbe usare l'oggetto per copiare bit a bit per spostarlo e fare "copia elisione". Questo si basa sul livello di "purezza" dell'uso di this
funzioni membro speciali (l'indirizzo non scappa).
Ma la purezza non è un attributo disponibile a livello di dichiarazione standard (esistono estensioni del compilatore che aggiungono una descrizione di purezza nella dichiarazione di funzione non inline), quindi non è possibile definire un ABI basato sulla purezza di codice che potrebbe non essere disponibile (il codice può o potrebbe non essere in linea e disponibile per l'analisi).
La purezza è misurata come "certamente pura" o "impura o sconosciuta". Il terreno comune, o limite superiore della semantica (in realtà massimo) o LCM (minimo comune multiplo) è "sconosciuto". Quindi l'ABI si deposita su sconosciuto.
Sommario:
- Alcuni costrutti richiedono che il compilatore definisca l'identità dell'oggetto.
- L'ABI è definito in termini di classi di programmi e non di casi specifici che potrebbero essere ottimizzati.
Possibili lavori futuri:
L'annotazione di purezza è abbastanza utile per essere generalizzata e standardizzata?