Usare le classi di amici per incapsulare le funzioni dei membri privati ​​in C ++ - buone pratiche o abusi?


12

Quindi ho notato che è possibile evitare di inserire funzioni private nelle intestazioni facendo qualcosa del genere:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        friend class PredicateList_HelperFunctions;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList_HelperFunctions
    {
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList_HelperFunctions::fullMatch(*this);
    }

La funzione privata non viene mai dichiarata nell'intestazione e i consumatori della classe che importano l'intestazione non devono mai sapere che esiste. Ciò è necessario se la funzione helper è un modello (l'alternativa è inserire l'intero codice nell'intestazione), ed è così che l'ho "scoperto". Un altro aspetto positivo di non dover ricompilare ogni file che include l'intestazione se si aggiunge / rimuove / si modifica una funzione di membro privato. Tutte le funzioni private sono nel file .cpp.

Così...

  1. È questo un modello di design noto per il quale c'è un nome?
  2. Per me (proveniente da un background Java / C # e apprendimento del C ++ ai miei tempi), questa sembra un'ottima cosa, dato che l'intestazione sta definendo un'interfaccia, mentre il .cpp sta definendo un'implementazione (e il tempo di compilazione migliorato è un bel bonus). Tuttavia, ha anche l'odore di abusare di una funzionalità linguistica che non è stata progettata per essere utilizzata in questo modo. Quindi, che cos'è? È qualcosa che ti accorgeresti di non vedere in un progetto C ++ professionale?
  3. Qualche insidie ​​a cui non sto pensando?

Sono a conoscenza di Pimpl, che è un modo molto più efficace per nascondere l'implementazione a margine della libreria. Questo è più utile per le classi interne, in cui Pimpl potrebbe causare problemi di prestazioni o non funzionare perché la classe deve essere trattata come un valore.


EDIT 2: L'eccellente risposta di Dragon Energy di seguito ha suggerito la seguente soluzione, che non utilizza affatto la friendparola chiave:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        class Private;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList::Private
    {
    public:
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList::Private::fullMatch(*this);
    }

Questo evita il fattore di shock di friend(che sembra essere stato demonizzato come goto) pur mantenendo lo stesso principio di separazione.


2
" Un consumatore potrebbe definire la propria classe PredicateList_HelperFunctions e consentire loro di accedere ai campi privati. " Non sarebbe una violazione ODR ? Sia tu che il consumatore dovreste definire la stessa classe. Se tali definizioni non sono uguali, il codice non è corretto.
Nicol Bolas,

Risposte:


13

È un po 'esoterico dire il minimo come hai già riconosciuto, il che potrebbe farmi grattarmi la testa per un momento quando inizio a incontrare il tuo codice chiedendomi cosa stai facendo e dove vengono implementate queste classi di aiuto fino a quando non inizio a prendere il tuo stile / abitudini (a quel punto potrei abituarmi totalmente).

Mi piace che stai riducendo la quantità di informazioni nelle intestazioni. Soprattutto in basi di codice molto grandi, che possono avere effetti pratici per ridurre le dipendenze in fase di compilazione e in definitiva i tempi di costruzione.

La mia reazione istintiva è che se si sente la necessità di nascondere i dettagli di implementazione in questo modo, per favorire il passaggio di parametri a funzioni indipendenti con collegamento interno nel file di origine. Di solito è possibile implementare funzioni di utilità (o intere classi) utili per l'implementazione di una particolare classe senza avere accesso a tutti gli interni della classe e invece passare solo quelle pertinenti dall'implementazione di un metodo alla funzione (o costruttore). E naturalmente questo ha il vantaggio di ridurre l'accoppiamento tra la tua classe e gli "aiutanti". Ha anche la tendenza a generalizzare ulteriormente quelli che altrimenti avrebbero potuto essere "aiutanti" se scopri che stanno iniziando a servire uno scopo più generalizzato applicabile a più di una implementazione di classe.

A volte anche un po 'rabbrividisco quando vedo un sacco di "aiutanti" nel codice. Non è sempre vero, ma a volte possono essere sintomatici di uno sviluppatore che sta semplicemente decomponendo le funzioni volenti o nolenti per eliminare la duplicazione del codice con enormi blocchi di dati trasferiti a funzioni con nomi / scopi a malapena comprensibili oltre al fatto che riducono la quantità di codice richiesto per implementare alcune altre funzioni. Solo un po 'più di un po' più in anticipo, a volte può portare a una chiarezza molto maggiore in termini di come l'implementazione di una classe è scomposta in ulteriori funzioni e favorire il passaggio di parametri specifici al passaggio di intere istanze del tuo oggetto con pieno accesso agli interni promuovere quello stile di pensiero progettuale. Non sto suggerendo che lo stai facendo, ovviamente (non ne ho idea),

Se diventa ingombrante, prenderei in considerazione una seconda soluzione più idiomatica che è il brufolo (mi rendo conto che hai citato problemi con esso ma penso che puoi generalizzare una soluzione per evitare quelli con il minimo sforzo). Ciò può spostare molte informazioni che la tua classe deve essere implementata, compresi i suoi dati privati, lontano dall'intestazione all'ingrosso. I problemi di prestazioni del pimpl possono essere in gran parte mitigati con un allocatore a tempo costante * economico * sporco come un elenco gratuito preservando la semantica del valore senza dover implementare un ctor di copia definito dall'utente.

  • Per quanto riguarda le prestazioni, il brufolo introduce almeno un overhead puntatore, ma penso che i casi debbano essere piuttosto seri laddove ciò pone una preoccupazione pratica. Se la località spaziale non viene degradata in modo significativo attraverso l'allocatore, i loop stretti che iterano sull'oggetto (il che dovrebbe essere generalmente omogeneo se le prestazioni sono così importanti) tenderà comunque a ridurre al minimo le mancate cache nella pratica, a condizione che tu usi qualcosa come un elenco gratuito per allocare il pimpl, inserendo i campi della classe in blocchi di memoria in gran parte contigui.

Personalmente, solo dopo aver esaurito quelle possibilità, prenderei in considerazione qualcosa del genere. Penso che sia un'idea decente se l'alternativa è come più metodi privati ​​esposti all'intestazione con forse solo la natura esoterica di ciò che è la preoccupazione pratica.

Un'alternativa

Un'alternativa che mi è venuta in mente in questo momento e che realizza ampiamente i tuoi stessi scopi in assenza di amici è questa:

struct PredicateListData
{
     int somePrivateField;
};

class PredicateList
{
    PredicateListData data;
public:
    bool match() const;
};

// In source file:
static bool fullMatch(const PredicateListData& p)
{
     // Can access p.somePrivateField here.
}

bool PredicateList::match() const
{
     return fullMatch(data);
}

Ora potrebbe sembrare una differenza molto discutibile e lo definirei comunque un "aiutante" (in un senso forse dispregiativo dal momento che stiamo ancora passando l'intero stato interno della classe alla funzione se ne ha bisogno o no) tranne che evita il fattore "shock" dell'incontro friend. In generale friendsembra un po 'spaventoso vedere spesso assenti ulteriori controlli, dal momento che dice che i tuoi interni di classe sono accessibili altrove (il che implica che potrebbe non essere in grado di mantenere i propri invarianti). Con il modo in cui friendlo usi diventa piuttosto discutibile se le persone sono consapevoli della pratica dalfriendrisiede semplicemente nello stesso file di origine e aiuta a implementare la funzionalità privata della classe, ma quanto sopra ottiene quasi lo stesso effetto, almeno con l'unico vantaggio probabilmente discutibile che non coinvolge nessun amico che evita quell'intero tipo ("Oh sparare, questa classe ha un amico. In quale altro luogo i suoi privati ​​hanno accesso / mutazione? "). Considerando che la versione immediatamente sopra comunica immediatamente che non c'è modo per i privati ​​di accedere / mutare al di fuori di qualsiasi cosa fatta nell'implementazione di PredicateList.

Questo forse si sta spostando verso territori un po 'dogmatici con questo livello di sfumatura poiché chiunque può rapidamente capire se si nominano in modo uniforme le cose *Helper*e le inseriscono tutte nello stesso file sorgente che è tutto raggruppato insieme come parte dell'implementazione privata di una classe. Ma se diventiamo schizzinosi, forse lo stile immediatamente sopra non causerà una reazione istintiva a prima vista in assenza della friendparola chiave che tende a sembrare un po 'spaventosa.

Per le altre domande:

Un consumatore può definire la propria classe PredicateList_HelperFunctions e consentire loro di accedere ai campi privati. Anche se non lo vedo come un grosso problema (se davvero volessi in quei campi privati ​​potresti fare un casting), forse incoraggerebbe i consumatori ad usarlo in quel modo?

Questa potrebbe essere una possibilità oltre i confini dell'API in cui il client potrebbe definire una seconda classe con lo stesso nome e ottenere l'accesso agli interni in quel modo senza errori di collegamento. Poi di nuovo sono in gran parte un programmatore C che lavora nella grafica in cui i problemi di sicurezza a questo livello di "what if" sono molto bassi nella lista delle priorità, quindi preoccupazioni come queste sono solo quelle su cui tendo agitando le mani e faccio una danza e prova a fingere che non esistano. MrGreen Se stai lavorando in un dominio in cui preoccupazioni come queste sono piuttosto serie, penso che sia una buona considerazione da prendere.

La proposta alternativa di cui sopra evita anche di soffrire di questo problema. Se vuoi comunque continuare a utilizzare friend, puoi anche evitare quel problema trasformando l'helper in una classe nidificata privata.

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

// In source file:
class PredicateList::Helper
{
    ...
};

È questo un modello di design noto per il quale c'è un nome?

Nessuno a mia conoscenza. Dubito che ce ne sarebbe uno dal momento che sta davvero entrando nella minuzia dei dettagli e dello stile di implementazione.

"Helper Hell"

Ho ricevuto una richiesta di ulteriori chiarimenti in merito al modo in cui a volte rabbrividisco quando vedo implementazioni con un sacco di codice "helper", e questo potrebbe essere leggermente controverso con alcuni, ma in realtà è un dato di fatto come ho fatto davvero rabbrividire quando stavo eseguendo il debug di alcuni dell'implementazione di una classe da parte dei miei colleghi solo per trovare un sacco di "aiutanti". MrGreen E non ero l'unico della squadra a grattarmi la testa cercando di capire cosa avrebbero dovuto fare esattamente tutti questi aiutanti. Inoltre non voglio venire dogmatico come "Non usare gli aiutanti", ma vorrei suggerire che potrebbe aiutare a pensare a come implementare le cose assenti da loro quando pratico.

Tutte le funzioni dei membri privati ​​non sono utili per definizione?

E sì, sto includendo metodi privati. Se vedo una classe con un'interfaccia pubblica semplice ma come una serie infinita di metodi privati ​​che sono in qualche modo mal definiti in uno scopo come find_implo find_detailo find_helper, allora anch'io mi rabbrividisco in un modo simile.

Quello che sto suggerendo in alternativa sono funzioni non amichevoli non membro con collegamento interno (dichiarato statico all'interno di uno spazio dei nomi anonimo) per aiutare a implementare la tua classe con almeno uno scopo più generalizzato di "una funzione che aiuta a implementare gli altri". E posso citare Herb Sutter da "Coding Standards" C ++ qui per il motivo per cui ciò può essere preferibile da un punto di vista generale SE:

Evita le quote associative: ove possibile, preferisci rendere le funzioni non amichevoli. [...] Le funzioni non amichevoli non membri migliorano l'incapsulamento minimizzando le dipendenze: il corpo della funzione non può dipendere dai membri non pubblici della classe (vedi Articolo 11). Inoltre, suddividono le classi monolitiche per liberare la funzionalità separabile, riducendo ulteriormente l'accoppiamento (vedi Articolo 33).

Puoi anche comprendere le "quote associative" di cui parla in una certa misura in termini del principio di base della limitazione dell'ambito variabile. Se immagini, come l'esempio più estremo, un oggetto God che ha tutto il codice necessario per l'esecuzione dell'intero programma, favorendo quindi "aiutanti" di questo tipo (funzioni, che siano funzioni membro o amici) che possono accedere a tutti gli interni ( privati) di una classe rendono sostanzialmente tali variabili non meno problematiche delle variabili globali. Hai tutte le difficoltà nel gestire correttamente lo stato e nel thread della sicurezza e nel mantenimento degli invarianti che otterresti con le variabili globali in questo esempio estremo. E, naturalmente, la maggior parte degli esempi reali non si spera in alcun modo vicino a questo estremo, ma nascondere le informazioni è utile solo in quanto limita la portata delle informazioni a cui si accede.

Ora Sutter dà già una bella spiegazione qui, ma aggiungerei inoltre che il disaccoppiamento tende a promuovere come un miglioramento psicologico (almeno se il tuo cervello funziona come il mio) in termini di come funzioni il design. Quando inizi a progettare funzioni che non possono accedere a tutto nella classe tranne i parametri rilevanti che le passi o, se passi l'istanza della classe come parametro, solo i suoi membri pubblici, tende a promuovere una mentalità di progettazione che favorisce funzioni che hanno uno scopo più chiaro, oltre al disaccoppiamento e alla promozione di un migliore incapsulamento, rispetto a ciò che potresti essere tentato di progettare se solo potessi accedere a tutto.

Se torniamo alle estremità, una base di codice piena di variabili globali non induce esattamente gli sviluppatori a progettare funzioni in modo chiaro e generalizzato allo scopo. Molto rapidamente più informazioni puoi accedere a una funzione, più molti di noi mortali affrontano la tentazione di degeneralizzarla e ridurne la chiarezza a favore dell'accesso a tutte queste informazioni extra che abbiamo invece di accettare parametri più specifici e pertinenti a quella funzione restringere il suo accesso allo stato e ampliarne l'applicabilità e migliorare la chiarezza delle intenzioni. Ciò vale (anche se generalmente in misura minore) con funzioni membro o amici.


1
Grazie per l'input! Non capisco perfettamente da dove vieni con questa parte, però: "A volte anche un po 'rabbrividisco quando vedo molti" aiutanti "nel codice." - Tutte le funzioni dei membri privati ​​non aiutano le funzioni per definizione? Questo sembra mettere in discussione le funzioni dei membri privati ​​in generale.
Robert Fraser,

1
Ah, la classe interiore non ha affatto bisogno di "amico", quindi farlo in questo modo evita totalmente la parola chiave "amico"
Robert Fraser,

"Le funzioni di supporto di tutte le funzioni di membro privato non sono definite per definizione? Questo sembra mettere in discussione le funzioni di membro privato in generale." Non è la cosa più grande. Pensavo fosse una necessità pratica che per un'implementazione di classe non banale, tu avessi un numero di funzioni private o aiutanti con accesso a tutti i membri della classe contemporaneamente. Ma ho guardato lo stile di alcuni grandi come Linus Torvalds, John Carmack, e sebbene i primi codici in C, quando codifica l'equivalente analogico di un oggetto, riesce generalmente a codificarlo con nessuno dei due insieme a Carmack.
Dragon Energy,

E naturalmente penso che gli helper nel file sorgente siano preferibili ad alcune enormi intestazioni che includono molte più intestazioni esterne del necessario perché ha usato molte funzioni private per aiutare a implementare la classe. Ma dopo aver studiato lo stile di quelli sopra e di altri, mi sono reso conto che spesso è possibile scrivere funzioni un po 'più generalizzate rispetto ai tipi che hanno bisogno di accedere a tutti i membri interni di una classe anche solo per implementare una classe, e il pensiero in anticipo nominare bene la funzione e passarla ai membri specifici di cui ha bisogno per lavorare spesso fa risparmiare più tempo [...]
Dragon Energy

[...] di quello che serve, producendo un'implementazione più chiara in generale che è più facile da manipolare in seguito. È come invece di scrivere un "predicato di supporto" per "corrispondenza completa" che acceda a tutto ciò che è nel tuo PredicateList, spesso potrebbe essere fattibile passare un membro o due dall'elenco dei predicati a una funzione leggermente più generalizzata che non ha bisogno di accedere a ogni membro privato PredicateListe spesso tenderà a dare anche un nome e uno scopo più chiari e generalizzati a quella funzione interna, nonché maggiori opportunità di "riutilizzo del codice a posteriori".
Dragon Energy,
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.