Perché C ++ non ti consente di prendere l'indirizzo di un costruttore?


14

C'è una ragione specifica per cui ciò romperebbe concettualmente la lingua o una ragione specifica per cui ciò è tecnicamente impossibile in alcuni casi?

L'uso sarebbe con il nuovo operatore.

Modifica: ho intenzione di rinunciare alla speranza di ottenere il mio "nuovo operatore" e "nuovo operatore" dritto ed essere diretto.

Il punto della domanda è: perché i costruttori sono speciali ? Ricorda ovviamente che le specifiche del linguaggio ci dicono cosa è legale, ma non necessariamente morale. Ciò che è legale è in genere informato da ciò che è logicamente coerente con il resto della lingua, ciò che è semplice e conciso e ciò che è fattibile per i compilatori da implementare. Le possibili motivazioni del comitato per le norme nel valutare questi fattori sono intenzionali e interessanti - da qui la domanda.


Non sarebbe un problema prendere l'indirizzo di un costruttore, ma poter passare il tipo. I modelli possono farlo.
Euforico,

Cosa succede se si dispone di un modello di funzione che si desidera costruire un oggetto utilizzando un costruttore che verrà specificato come argomento per la funzione?
Prassolitico,

1
Ci saranno alternative per qualsiasi esempio a cui riesco a pensare, ma ancora, perché i costruttori dovrebbero essere speciali? Ci sono molte cose che probabilmente non userete nella maggior parte dei linguaggi di programmazione, ma casi speciali come questo di solito hanno una giustificazione.
Prassolitico,

1
@RobertHarvey La domanda che mi è venuta in mente quando stavo per scrivere una classe di fabbrica.
Prassolitico,

1
Mi chiedo se il C ++ 11 std::make_uniquee in std::make_sharedgrado di risolvere adeguatamente la motivazione pratica sottostante per questa domanda. Questi sono metodi modello, il che significa che è necessario acquisire gli argomenti di input per il costruttore e quindi inoltrarli al costruttore effettivo.
rwong

Risposte:


10

Le funzioni da puntatore a membro hanno senso solo se si dispone di più di una funzione membro con la stessa firma, altrimenti ci sarebbe un solo valore possibile per il puntatore. Ma ciò non è possibile per i costruttori, poiché in C ++ costruttori diversi della stessa classe devono avere firme diverse.

L'alternativa a Stroustrup sarebbe stata quella di scegliere una sintassi per C ++ in cui i costruttori potessero avere un nome diverso dal nome della classe, ma ciò avrebbe impedito alcuni aspetti molto eleganti della sintassi ctor esistente e avrebbe reso il linguaggio più complicato. A me sembra un prezzo elevato solo per consentire una caratteristica raramente necessaria che può essere facilmente simulata "esternalizzando" l'inizializzazione di un oggetto dal ctor a una initfunzione diversa (una normale funzione membro per la quale può essere puntatore-membri creato).


2
Tuttavia, perché prevenire memcpy(buffer, (&std::string)(int, char), size)? (Probabilmente estremamente non kosher, ma dopo tutto questo è C ++.)
Thomas Eding,

3
scusa, ma quello che hai scritto non ha senso. Non vedo nulla di sbagliato nell'avere puntatore a membro che punta a un costruttore. inoltre, sembra che tu abbia citato qualcosa, senza collegamento alla fonte.
BЈовић,

1
@ThomasEding: cosa ti aspetti esattamente da quell'affermazione? Copia del codice assembly del string ctor somewgere? Come sarà determinata la "dimensione" (anche se provi qualcosa di equivalente per una funzione membro standard)?
Doc Brown,

Mi aspetto che faccia la stessa cosa che farebbe dato l'indirizzo di un puntatore a funzione libera memcpy(buffer, strlen, size). Presumibilmente avrebbe copiato l'assemblea, ma chi lo sa. Indipendentemente dal fatto che il codice possa essere invocato senza crash, richiederebbe la conoscenza del compilatore che si utilizza. Lo stesso vale per determinare la dimensione. Sarebbe altamente dipendente dalla piattaforma, ma molti codici C ++ non portatili vengono utilizzati nel codice di produzione. Non vedo alcun motivo per vietarlo.
Thomas Eding,

@ThomasEding: un compilatore C ++ conforme dovrebbe fornire una diagnostica quando tenta di accedere a un puntatore a funzione come se fosse un puntatore a dati. Un compilatore C ++ non conforme può fare qualsiasi cosa, ma può anche fornire un modo non c ++ per accedere anche a un costruttore. Questo non è un motivo per aggiungere una funzionalità a C ++ che non ha usi nel codice conforme.
Bart van Ingen Schenau,

5

Un costruttore è una funzione che si chiama quando l'oggetto non esiste ancora, quindi non può essere una funzione membro. Potrebbe essere statico.

Un costruttore viene effettivamente chiamato con questo puntatore, dopo che la memoria è stata allocata ma prima che sia stata completamente inizializzata. Di conseguenza un costruttore ha una serie di caratteristiche privilegiate.

Se avessi un puntatore a un costruttore, dovrebbe essere un puntatore statico, qualcosa come una funzione di fabbrica o un puntatore speciale a qualcosa che verrebbe chiamato immediatamente dopo l'allocazione della memoria. Non potrebbe essere una normale funzione membro e ancora funzionare come costruttore.

L'unico scopo utile che viene in mente è un tipo speciale di puntatore che potrebbe essere passato al nuovo operatore per consentirgli di indirizzare su quale costruttore utilizzare. Immagino che potrebbe essere utile, ma richiederebbe una nuova sintassi significativa e presumibilmente la risposta è: ci hanno pensato e non ne è valsa la pena.

Se vuoi solo riformattare il codice di inizializzazione comune, una normale funzione di memoria è di solito una risposta sufficiente e puoi ottenere un puntatore a uno di questi.


Questa sembra la risposta più corretta. Ricordo un articolo di molti (molti) anni fa sull'operatore nuovo e sul funzionamento interno del "nuovo operatore". l'operatore new () alloca spazio. Il nuovo operatore chiama il costruttore con quello spazio assegnato. Prendere l'indirizzo di un costruttore è "speciale" perché chiamare il costruttore richiede spazio. L'accesso per chiamare un costruttore come questo è con il posizionamento nuovo.
Bill Door,

1
La parola "esiste" oscura i dettagli secondo cui un oggetto può avere un indirizzo e avere memoria allocata ma non essere inizializzato. Sulla funzione membro o no, penso che ottenere questo puntatore faccia una funzione una funzione membro perché è chiaramente associata a un'istanza di oggetto (anche se non inizializzata). Detto questo, la risposta solleva un buon punto: il costruttore è l'unica funzione membro che può essere chiamata su un oggetto non inizializzato.
Prassolitico,

Non importa, a quanto pare hanno la designazione di "funzioni speciali dei membri". Clausola 12 della norma C ++ 11: "Il costruttore predefinito (12.1), il costruttore di copie e l'operatore di assegnazione delle copie (12.8), il costruttore di spostamenti e l'operatore di assegnazione dei movimenti (12.8) e il distruttore (12.4) sono funzioni speciali dei membri ".
Prassolitico,

E 12.1: "Un costruttore non deve essere virtuale (10.3) o statico (9.4)." (la mia enfasi)
Prassolitico

1
Il fatto è che se si compila con simboli di debug e si cerca una traccia dello stack, in realtà c'è un puntatore al costruttore. Quello che non sono mai stato in grado di trovare la sintassi per ottenere questo puntatore ( &A::Anon funziona in nessuno dei compilatori che ho provato.)
alfC

-3

Questo perché il loro non è un tipo di costruttore di ritorno e non stai riservando spazio per il costruttore in memoria. Come in caso di variabile durante la dichiarazione. Ad esempio: se scrivi la semplice variabile X, il compilatore genererà un errore perché il compilatore non capirà il significato di questo. Ma quando scrivi Int x; Quindi il compilatore viene a sapere che è una variabile di dati di tipo int, quindi riserverà dello spazio per la variabile.

Conclusione: - quindi la conclusione è che a causa dell'esclusione del tipo restituito non otterrà l'indirizzo in memoria.


1
Il codice nel costruttore deve avere un indirizzo in memoria perché deve essere da qualche parte. Non è necessario riservare spazio per esso nello stack, ma deve trovarsi da qualche parte nella memoria. È possibile prendere l'indirizzo di funzioni che non restituiscono valori. (void)(*fptr)()dichiara un puntatore a una funzione senza valore di ritorno.
Prassolitico

2
Hai perso il punto della domanda: il post originale chiedeva di prendere l'indirizzo del codice per il costruttore, non il risultato fornito dal costruttore. Inoltre, su questa scheda, usa parole piene: "u" non è un sostituto accettabile per "tu".
BobDalgleish,

Signor praxeolitic, penso che se non menzioniamo alcun tipo di ritorno, il compilatore non imposterà una posizione di memoria particolare per ctor e la posizione è impostata internamente .... Possiamo recuperare l'indirizzo di qualsiasi cosa in c ++ che non è data da compilatore? Se sbaglio, per favore correggimi con la risposta corretta
amorevole Goyal

E parlami anche della variabile di riferimento. Possiamo recuperare l'indirizzo della variabile di riferimento? In caso contrario, quale indirizzo printf ("% u", & (& (j))); sta stampando se & j = x dove x = 10? Perché l'indirizzo stampato da printf e l'indirizzo di x non sono gli stessi
amorevole Goyal

-4

Prenderò un'ipotesi selvaggia:

Il costruttore e il distruttore C ++ non sono affatto funzioni: sono macro. Vengono allineati all'ambito in cui viene creato l'oggetto e all'ambito in cui l'oggetto viene distrutto. A sua volta, non vi è alcun costruttore né distruttore, l'oggetto è solo IS.

In realtà, penso che le altre funzioni della classe non siano né funzioni, ma funzioni incorporate che NON vengono allineate perché ne prendi l'indirizzo (il compilatore si rende conto che ci sei sopra e non incorpora o incorpora il codice nella funzione e ottimizza quella funzione) e, a sua volta, la funzione sembra "essere ancora lì", anche se non lo sarebbe se non l'avessi presa in considerazione.

La tabella virtuale dell '"oggetto" C ++ non è come un oggetto JavaScript, dove è possibile ottenere il suo "costruttore" e crearne oggetti in fase di runtime tramite new XMLHttpRequest.constructor, ma piuttosto una raccolta di puntatori a funzioni anonime che agiscono come mezzi per interfacciarsi con questo oggetto , esclusa la possibilità di creare l'oggetto. E non ha nemmeno senso "eliminare" l'oggetto, perché è come provare a eliminare una struttura, non puoi: è solo un'etichetta di pila, basta scriverci come ti pare sotto un'altra etichetta: sei libero di usa una classe come 4 numeri interi:

/* i imagine this string gets compiled into a struct, one of which's members happens to be a const char * which is initialized to exactly your string: no function calls are made during construction. */
std::string a = "hello, world";
int *myInt = (int *)(*((void **)&a));
myInt[0] = 3;
myInt[1] = 9;
myInt[2] = 20;
myInt[3] = 300;

Non c'è perdita di memoria, non ci sono problemi, tranne per il fatto che hai effettivamente sprecato un sacco di spazio dello stack riservato all'interfaccia degli oggetti e alla stringa, ma non distruggerà il tuo programma (finché non provi a usarlo come una stringa mai più).

In realtà, se le mie assunzioni precedenti sono corrette: il costo completo della stringa è solo il costo di memorizzazione di questi 32 byte e lo spazio della stringa costante: le funzioni vengono utilizzate solo al momento della compilazione e possono anche essere incorporate e eliminate dopo l'oggetto viene creato e utilizzato (come se si stesse lavorando con una struttura e si riferisse ad esso solo direttamente senza alcuna chiamata di funzione, sicuramente ci sono chiamate duplicate invece di salti di funzione, ma di solito è più veloce e utilizza meno spazio). In sostanza, ogni volta che chiamate una funzione, il compilatore sostituisce semplicemente quella chiamata con le istruzioni per farlo letteralmente, con l'eccezione che i progettisti del linguaggio hanno impostato.

Riepilogo: gli oggetti C ++ non hanno idea di cosa siano; tutti gli strumenti per interfacciarsi con essi sono integrati staticamente e persi in fase di esecuzione. Ciò rende il lavoro con le classi efficiente quanto il riempimento di strutture con i dati e il lavoro diretto con tali dati senza chiamare alcuna funzione (queste funzioni sono incorporate).

Questo è completamente diverso dagli approcci di COM / ObjectiveC e javascript, che mantengono dinamicamente le informazioni sul tipo, a scapito del sovraccarico di runtime, della gestione della memoria, delle chiamate di costruzioni, poiché il compilatore non può buttare via queste informazioni: è necessario per spedizione dinamica. Questo a sua volta ci dà la possibilità di "parlare" con il nostro programma in fase di esecuzione e svilupparlo mentre è in esecuzione con componenti riflettenti.


2
scusate, ma alcune parti di questa "risposta" sono sbagliate o pericolosamente fuorvianti. Purtroppo, lo spazio dei commenti è troppo piccolo per elencarli tutti (la maggior parte dei metodi non verrà incorporata, questo impedirebbe l'invio virtuale e gonfiare il file binario; anche se in linea, potrebbe esserci una copia indirizzabile accessibile da qualche parte; esempio di codice irrilevante che in il caso peggiore corrompe il tuo stack e nel migliore dei casi non si adatta ai tuoi presupposti; ...)
hoffmale

La risposta è ingenua, volevo solo esprimere la mia ipotesi sul perché non si possa fare riferimento a costruttore / distruttore. Concordo nel caso delle classi virtuali, la vtable deve persistere e il codice indirizzabile deve essere in memoria in modo che la vtable possa fare riferimento a essa. Tuttavia, le classi che non implementano una classe virtuale sembrano inline, come nel caso di std :: string. Non tutto viene sottolineato, ma le cose che non sembrano essere minimamente inserite in un blocco di codice "anonimo" da qualche parte nella memoria. Inoltre, in che modo il codice corrompe lo stack? Certo abbiamo perso la corda, ma per il resto tutto ciò che abbiamo fatto è reinterpretare.
Dmitry

Il danneggiamento della memoria si verifica in un programma per computer quando il contenuto di una posizione di memoria viene modificato involontariamente. Questo programma lo fa intenzionalmente e non tenta più di usare quella stringa, quindi non c'è corruzione, ma solo uno spreco di spazio nello stack. Ma sì, l'invariante della stringa non viene più mantenuto, ingombra l'ambito (al termine del quale viene recuperato lo stack).
Dmitry

a seconda dell'implementazione della stringa, è possibile scrivere su byte che non si desidera. Se la stringa è qualcosa di simile struct { int size; const char * data; };(come sembra che tu supponga) scrivi 4 * 4 byte = 16 byte su un indirizzo di memoria in cui hai prenotato solo 8 byte su una macchina x86, quindi 8 byte vengono scritti su altri dati (che possono danneggiare il tuo stack ). Fortunatamente, std::stringnormalmente ha alcune ottimizzazioni sul posto per stringhe brevi, quindi dovrebbe essere abbastanza grande per il tuo esempio quando usi un'implementazione std principale.
hoffmale,

@hoffmale hai perfettamente ragione, potrebbe essere 4 byte potrebbe essere 8, o anche 1 byte. Tuttavia, una volta che conosci la dimensione della stringa, sai anche che quella memoria è nello stack nell'ambito corrente e puoi usarla come preferisci. Il mio punto era che se conosci la struttura, è impacchettata in modo indipendente da qualsiasi informazione sulla classe, a differenza degli oggetti COM che hanno un uuido che identifica la loro classe come parte della vtable di IUnknown. A sua volta, il compilatore accede a questi dati direttamente tramite funzioni statiche incorporate o alterate.
Dmitry
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.