Perché inseriamo le funzioni dei membri privati ​​nelle intestazioni?


16

La risposta al motivo per cui inseriamo le variabili dei membri privati ​​nelle intestazioni C ++ è che la dimensione della classe deve essere nota nei punti in cui vengono dichiarate le istanze in modo che il compilatore possa generare codice che si sposta in modo appropriato nello stack.

Perché dobbiamo inserire i membri privati ​​nelle intestazioni?

Ma c'è qualche motivo per dichiarare funzioni private nella definizione della classe?

L'alternativa sarebbe essenzialmente l'idioma del brufolo, ma senza il superfluo indiretto.

Questa funzionalità linguistica è più di un errore storico?

Risposte:


11

Le funzioni dei membri privati ​​possono essere virtuale nelle implementazioni comuni di C ++ (che usano una vtable) è necessario che l'ordine e il numero specifici delle funzioni virtuali siano conosciuti da tutti i client della classe. Questo vale anche se una o più funzioni dei membri virtuali lo sono private.

Potrebbe sembrare che sia come "mettere il carrello davanti al cavallo", perché le scelte di implementazione del compilatore non dovrebbero influenzare le specifiche del linguaggio. Tuttavia, in realtà il linguaggio C ++ stesso è stato sviluppato contemporaneamente a un'implementazione funzionante ( Cfront ), che utilizzava vtables.


4
"Le scelte di implementazione non dovrebbero influenzare il linguaggio" è quasi l'esatto contrario del mantra C ++. La specifica non impone alcuna implementazione particolare, ma ci sono molte regole appositamente create per soddisfare i requisiti di una scelta di implementazione particolarmente efficiente.
Ben Voigt,

Sembra molto plausibile. C'è una fonte su questo?
Prassolitico,

@Praxeolitic: puoi leggere la tabella dei metodi virtuali .
Greg Hewgill

1
@Praxeolitic - trova una vecchia copia del "Manuale di riferimento C ++ annotato" ( stroustrup.com/arm.html ) - gli autori parlano di vari dettagli di implementazione e di come ha modellato il linguaggio.
Michael Kohne,

Non sono sicuro che questo risponda davvero alla domanda. Per me si rivolge solo a un aspetto specifico - anche se bene - e lascia il resto: perché dobbiamo inserire le funzioni dei membri privati non virtuali nelle intestazioni? Certo, solo per coerenza / semplicità è un argomento - ma idealmente avremmo anche una logica meccanicistica e / o filosofica. Quindi, ho aggiunto una risposta che credo spieghi come una scelta progettuale molto deliberata con effetti molto benefici.
underscore_d,

13

Se hai consentito l'aggiunta di metodi a una classe al di fuori della sua definizione, potrebbero essere aggiunti ovunque , in qualsiasi file, da chiunque.

Ciò darebbe immediatamente a tutto il codice client un accesso banale ai membri di dati privati ​​e protetti.

Una volta terminata la definizione della classe, non c'è modo di contrassegnare alcuni file come appositamente benedetti dall'autore per estenderla: ci sono solo unità di traduzione piatte. Quindi, l'unico modo ragionevole per dire al compilatore che un determinato insieme di metodi è ufficiale, o benedetto dall'autore della classe, è dichiararli all'interno della classe.


Nota che abbiamo accesso diretto alla memoria in C ++, il che significa che in genere è banale creare un tipo di ombra con lo stesso layout di memoria della tua classe, aggiungere i miei metodi (o semplicemente rendere pubblici tutti i dati) e reinterpret_cast. Oppure posso trovare il codice della tua funzione privata o smontarlo. Oppure cerca l'indirizzo della funzione nella tabella dei simboli e chiama o direttamente.

Questi identificatori di accesso non tentano di prevenire questi attacchi, perché ciò non è possibile. Indicano solo come dovrebbe essere usata una classe.


1
La definizione di classe potrebbe fare riferimento a un'entità contenitore di funzioni nascosta dal codice client. In alternativa, questo potrebbe essere invertito e una definizione di classe tradizionale completa nascosta dal codice client potrebbe designare un'interfaccia visibile che descrive solo i membri pubblici e può rivelarne le dimensioni.
Prassolitico,

1
Certo, ma il modello di compilazione non fornisce un modo ragionevole per fermare il codice client interponendo la propria versione del contenitore nascosto o una diversa interfaccia visibile con accessori aggiuntivi.
Inutile

Questo è un buon punto, ma non è vero per le definizioni di tutte le funzioni membro non nell'intestazione?
Prassolitico,

1
Tuttavia, tutte le funzioni membro sono dichiarate all'interno della classe. Il punto è che non è possibile aggiungere nuove funzioni membro che non sono state almeno dichiarate nella definizione della classe (e la regola di una definizione impedisce più definizioni).
Inutile

E che dire di un unico contenitore di regole di funzioni private?
Prassolitico,

8

La risposta accettata spiega questo per le funzioni private virtuali , ma risponde solo a un aspetto specifico della domanda, che è considerevolmente più limitato di quello che l'OP ha chiesto. Quindi, dobbiamo riformulare: perché siamo tenuti a dichiarare le funzioni private non virtuali nelle intestazioni?

Un'altra risposta invoca il fatto che le classi devono essere dichiarate in un blocco, dopo di che sono sigillate e non possono essere aggiunte. Questo è quello che faresti omettendo di dichiarare un metodo privato nell'intestazione e cercando di definirlo altrove. Bel punto. Perché alcuni utenti della classe dovrebbero essere in grado di aumentarlo in modo che altri utenti non possano osservare? I metodi privati ​​ne fanno parte e non ne sono esclusi. Ma poi chiedi perché sono inclusi e sembra un po 'tautologico. Perché gli utenti della classe devono saperlo? Se non fossero visibili, gli utenti non potrebbero aggiungerne nessuno, e hey presto.

Quindi, volevo fornire una risposta che, anziché includere semplicemente i metodi privati ​​per impostazione predefinita, fornisca punti specifici a favore della loro visibilità agli utenti. Un motivo meccanicistico per funzioni private non virtuali che richiedono una dichiarazione pubblica è riportato nel GotW # 100 di Herb Sutter sull'idioma di Pimpl come parte della sua logica. Non parlerò di Pimpl qui, poiché sono sicuro che lo sappiamo tutti. Ma ecco il bit rilevante:

In C ++, quando cambia qualcosa nella definizione della classe di un file di intestazione, tutti gli utenti di quella classe devono essere ricompilati, anche se l'unica modifica è stata ai membri della classe privata a cui gli utenti della classe non possono nemmeno accedere. Questo perché il modello di build di C ++ si basa sull'inclusione testuale e perché C ++ presuppone che i chiamanti conoscano due cose principali su una classe che possono essere influenzate dai membri privati:

  • Dimensioni e layout : [dei membri e funzioni virtuali - autoesplicativo e ottimo per le prestazioni, ma non per questo che siamo qui]
  • Funzioni : il codice chiamante deve essere in grado di risolvere le chiamate alle funzioni membro della classe, comprese le funzioni private inaccessibili che si sovraccaricano di funzioni non private - se la funzione privata è una corrispondenza migliore, il codice chiamante non verrà compilato. (Il C ++ ha preso la deliberata decisione di progettazione per eseguire la risoluzione del sovraccarico prima del controllo dell'accessibilità per motivi di sicurezza. Ad esempio, si è ritenuto che cambiare l'accessibilità di una funzione da privato a pubblico non dovrebbe cambiare il significato del codice di chiamata legale.)

Sutter è, ovviamente, una fonte estremamente affidabile come membro del Comitato, quindi conosce "una decisione progettuale deliberata" quando ne vede una. E l'idea di richiedere una dichiarazione pubblica dei metodi privati ​​come un modo per evitare la semantica alterata o l'accessibilità accidentale rotta in seguito è probabilmente la logica più convincente. Per fortuna, dato che il tutto sembrava piuttosto inutile prima d'ora!


" Ma poi chiedi perché, e quella risposta sembra tautologica. Ancora una volta, perché gli utenti della classe devono conoscerli? " No, non è tautologico. Gli utenti non devono necessariamente "conoscerli". Ciò che deve accadere è che devi essere in grado di impedire alle persone di estendere le lezioni senza il consenso dello scrittore di classe. Quindi tutti questi membri devono essere dichiarati in anticipo. Che ciò induca gli utenti a conoscere i membri privati ​​è semplicemente una conseguenza necessaria.
Nicol Bolas

1
@NicolBolas Di sicuro. Probabilmente non era una formulazione molto buona da parte mia. Intendevo dire che la risposta spiega solo la visibilità dei metodi privati ​​come conseguenza di una regola (molto valida) che copre molte cose, piuttosto che fornire una logica sulla visibilità specifica dei metodi privati. Naturalmente, la vera risposta è una combinazione di inutili, gnasher e miei. Voglio solo fornire un'altra prospettiva che non era ancora qui.
underscore_d

4

Ci sono due ragioni per farlo.

Innanzitutto, rendersi conto che l'identificatore di accesso è per il compilatore e non è rilevante in fase di esecuzione. L'accesso a un membro privato al di fuori dell'ambito è un errore di compilazione .

conciseness

Considera una funzione che è breve, una o due righe. Esiste per ridurre la replica del codice altrove, il che ha anche il vantaggio di poter cambiare il modo in cui un algoritmo o qualsiasi altra cosa funzioni in un posto anziché in molti (ad esempio cambiando un algoritmo di ordinamento).

Preferiresti avere una o due righe veloci nell'intestazione o avere il prototipo della funzione lì più un'implementazione da qualche parte? È più facile da trovare nell'intestazione e, per funzioni brevi, è molto più dettagliato avere un'implementazione separata.

C'è un altro grande vantaggio, che è ...

Funzioni integrate

Una funzione privata può essere inline e ciò richiede necessariamente che sia nell'intestazione. Considera questo:

class A {
  private:
    inline void myPrivateFunction() {
      ...
    }

  public:
    inline void somePublicFunction() {
      myPrivateFunction();
      ...
    }
};

È possibile che la funzione privata possa essere incorporata insieme alla funzione pubblica. Ciò viene fatto a discrezione del compilatore, poiché la inlineparola chiave è tecnicamente un suggerimento , non un requisito.


Tutte le funzioni definite all'interno del corpo della classe vengono automaticamente contrassegnate inline, non c'è motivo di utilizzare la parola chiave lì.
Ben Voigt,

@BenVoigt così è. Non me ne ero reso conto e utilizzo C ++ part-time da un po 'di tempo. Quella lingua non smette mai di sorprendermi con piccole pepite come quella.

Non credo che un metodo debba essere nell'intestazione per essere integrato. Deve solo essere nella stessa unità di compilazione. C'è un caso in cui ha senso avere unità di compilazione separate per una classe?
Samuel Danielson,

@SamuelDanielson è corretto, ma una funzione privata deve essere nella definizione della classe. La stessa nozione di "privato" implica che fa parte della classe. Sarebbe possibile avere una funzione non .cppmembro nel file che è sottolineata da funzioni membro che sono definite al di fuori della definizione di classe, ma tale funzione non sarebbe privata.

Il corpo della domanda era "c'è qualche motivo per dichiarare le funzioni private nella definizione della classe [sic, per cui il contesto mostra che OP significava davvero dichiarazione di classe]". Stai parlando di definire le funzioni private. Questo non risponde alla domanda. @SamuelDanielson LTO al giorno d'oggi significa che le funzioni possono essere ovunque in un progetto e conservare le stesse possibilità di essere incorporate. Per quanto riguarda la suddivisione di una classe in più unità di traduzione, il caso più semplice è che la classe è semplicemente grande e si desidera dividerla semanticamente in più file di origine. Sono sicuro che classi così grandi sono scoraggiate, ma comunque
underscore_d

2

Un altro motivo per avere metodi privati ​​nel file di intestazione: ci sono casi in cui un metodo inline pubblico non fa molto di più che chiamare uno o più metodi privati. Avere i metodi privati ​​nell'intestazione significa che una chiamata al metodo pubblico può essere completamente incorporata nel codice effettivo dei metodi privati ​​e l'allineamento non si interrompe con una chiamata al metodo privato. Anche da una diversa unità di compilazione (e i metodi pubblici vengono normalmente chiamati da diverse unità di compilazione).

Naturalmente c'è anche la ragione per cui il compilatore non può rilevare problemi con la risoluzione del sovraccarico se non conosce tutti i metodi, inclusi quelli privati.


Ottimo punto sull'allineamento! Immagino che questo sia particolarmente rilevante per lo stdlib e altre librerie con metodi definiti in linea. (se non tanto per il codice interno, dove LTO può fare quasi tutto oltre i confini della TU)
underscore_d

0

Consentire a tali funzioni di accedere ai membri privati. Altrimenti avresti comunque bisogno di friendloro nell'intestazione.

Se una qualsiasi funzione potesse accedere ai membri privati ​​della classe, allora privato sarebbe inutile.


1
Forse avrei dovuto essere più esplicito nella domanda - intendevo perché il linguaggio è progettato in questo modo? Perché deve essere o perché questo design è buono?
Prassolitico,
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.