funzione membro scambio pubblico amico


169

Nella bella risposta al linguaggio copia-e-scambia c'è un pezzo di codice che mi serve un po 'di aiuto:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        using std::swap; 
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
    // ...
};

e aggiunge una nota

Ci sono altre affermazioni che dovremmo specializzare std :: swap per il nostro tipo, fornire uno swap in classe insieme a uno swap a funzione libera, ecc. Ma tutto ciò non è necessario: qualsiasi uso corretto dello swap avverrà attraverso una chiamata non qualificata e la nostra funzione sarà rilevata tramite ADL. Una funzione farà.

Con friendun po 'di termini "ostili", devo ammetterlo. Quindi, le mie domande principali sono:

  • sembra una funzione libera , ma è all'interno del corpo della classe?
  • perché non è swapstatico ? Ovviamente non utilizza alcuna variabile membro.
  • "Qualsiasi uso corretto dello swap lo scoprirà tramite ADL" ? ADL cercherà negli spazi dei nomi, giusto? Ma guarda anche dentro le classi? O è qui che friendentra?

Side-domande:

  • Con C ++ 11, dovrei segnare i miei swaps con noexcept?
  • Con C ++ 11 e la sua gamma-per , dovrei mettere friend iter begin()e friend iter end()allo stesso modo all'interno della classe? Penso che friendnon sia necessario qui, giusto?

Considerando la domanda secondaria su range-based per: è meglio scrivere funzioni membro e lasciare l'accesso range su begin () e end () nello spazio dei nomi std (§24.6.5), basato su range per usarli internamente da globale o spazio dei nomi standard (vedere §6.5.4). Tuttavia, il lato negativo è che queste funzioni fanno parte dell'intestazione <iteratore>, se non la si include, è possibile scriverle da soli.
Vitus,

2
perché non è statico - perché una friendfunzione non è affatto una funzione membro.
aschepler

Risposte:


175

Esistono diversi modi per scrivere swap, alcuni meglio di altri. Nel tempo, tuttavia, è stato scoperto che una singola definizione funziona meglio. Consideriamo come potremmo pensare di scrivere una swapfunzione.


Innanzitutto vediamo che contenitori come std::vector<>hanno una funzione membro a argomento singolo swap, come ad esempio:

struct vector
{
    void swap(vector&) { /* swap members */ }
};

Naturalmente, quindi, anche la nostra classe dovrebbe, giusto? Beh, non proprio. La libreria standard ha ogni sorta di cose inutili e un membro swapè una di queste. Perché? Andiamo avanti


Ciò che dovremmo fare è identificare ciò che è canonico e ciò che la nostra classe deve fare per lavorare con esso. E il metodo canonico di scambio è con std::swap. Questo è il motivo per cui le funzioni membro non sono utili: non sono il modo in cui dovremmo scambiare le cose, in generale, e non influire sul comportamento di std::swap.

Bene, quindi, per rendere il std::swaplavoro dovremmo fornire (e avremmo dovuto fornire std::vector<>) una specializzazione std::swap, giusto?

namespace std
{
    template <> // important! specialization in std is OK, overloading is UB
    void swap(myclass&, myclass&)
    {
        // swap
    }
}

Bene, sicuramente funzionerebbe in questo caso, ma ha un evidente problema: le specializzazioni di funzioni non possono essere parziali. Cioè, non possiamo specializzare le classi di template con questo, solo particolari istanze:

namespace std
{
    template <typename T>
    void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
    {
        // swap
    }
}

Questo metodo funziona qualche volta, ma non sempre. Deve esserci un modo migliore.


C'è! Possiamo usare una friendfunzione e trovarla tramite ADL :

namespace xyz
{
    struct myclass
    {
        friend void swap(myclass&, myclass&);
    };
}

Quando vogliamo scambiare qualcosa, associamo std::swap e quindi facciamo una chiamata non qualificata:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first

// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

Cos'è una friendfunzione? C'è confusione in quest'area.

Prima che il C ++ fosse standardizzato, le friendfunzioni facevano qualcosa chiamato "iniezione del nome amico", in cui il codice si comportava come se la funzione fosse stata scritta nello spazio dei nomi circostante. Ad esempio, questi erano pre-standard equivalenti:

struct foo
{
    friend void bar()
    {
        // baz
    }
};

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Tuttavia, quando è stato inventato ADL questo è stato rimosso. La friendfunzione può quindi essere trovata solo tramite ADL; se lo volevi come funzione gratuita, doveva essere dichiarato come tale ( vedi questo , per esempio). Ma ecco! C'era un problema.

Se lo usi std::swap(x, y), il tuo sovraccarico non sarà mai trovato, perché hai esplicitamente detto "guarda dentro stde nessun altro"! Questo è il motivo per cui alcune persone hanno suggerito di scrivere due funzioni: una come funzione che si trova tramite ADL e l'altra per gestire std::qualifiche esplicite .

Ma come abbiamo visto, questo non può funzionare in tutti i casi e finiamo con un brutto pasticcio. Invece, lo scambio idiomatico è andato dall'altra parte: invece di renderlo il compito delle classi da fornire std::swap, è compito degli scambiatori assicurarsi che non usino qualificati swap, come sopra. E questo tende a funzionare abbastanza bene, purché le persone lo sappiano. Ma qui sta il problema: non è intuitivo dover usare una chiamata non qualificata!

Per rendere ciò più semplice, alcune librerie come Boost hanno fornito la funzione boost::swap, che esegue semplicemente una chiamata non qualificata swap, con std::swapuno spazio dei nomi associato. Questo aiuta a rendere le cose di nuovo succinte, ma è ancora un peccato.

Si noti che non vi è alcun cambiamento nel comportamento di C ++ 11 std::swap, cosa che io e altri abbiamo erroneamente pensato fosse il caso. Se sei stato morso da questo, leggi qui .


In breve: la funzione membro è solo rumore, la specializzazione è brutta e incompleta, ma la friendfunzione è completa e funziona. E quando si scambia, utilizzare boost::swapo un non qualificato swapcon std::swapassociati.


† Informalmente, viene associato un nome se verrà preso in considerazione durante una chiamata di funzione. Per i dettagli, leggere §3.4.2. In questo caso, std::swapnormalmente non viene considerato; ma possiamo associarlo (aggiungerlo all'insieme di sovraccarichi considerati da non qualificati swap), consentendone la ricerca.


10
Non sono d'accordo sul fatto che la funzione membro sia solo rumore. Una funzione membro consente, ad esempio std::vector<std::string>().swap(someVecWithData);, che non è possibile con una swapfunzione libera poiché entrambi gli argomenti sono passati da un riferimento non const.
ildjarn,

3
@ildjarn: puoi farlo su due righe. Avere la funzione membro viola il principio DRY.
GManNickG,

4
@GMan: il principio DRY non si applica se uno è implementato in termini di altro. Altrimenti nessuno sosterrebbe una classe con implementazioni di operator=, operator+e operator+=, chiaramente, quegli operatori di classi pertinenti sono accettati / previsti per simmetria. Lo stesso vale per member swap+ namespace scoped swapsecondo me.
ildjarn,

3
@GMan Penso che stia considerando troppe funzioni. Poco conosciuto, ma anche un function<void(A*)> f; if(!f) { }può fallire solo perché Adichiara un operator!che accetta faltrettanto bene come fproprio operator!(improbabile, ma può accadere). Se l function<>'autore pensasse "ohh ho un' operatore bool ', perché dovrei implementare' operatore! '? Sarebbe una violazione del DRY!", Sarebbe fatale. Devi solo avere un operator!implementato per A, e Aavere un costruttore per un function<...>, e le cose si romperanno, perché entrambi i candidati richiederanno conversioni definite dall'utente.
Johannes Schaub - litb

1
Consideriamo come potremmo pensare di scrivere una funzione di swap [membro]. Naturalmente, quindi, anche la nostra classe dovrebbe, giusto? Beh, non proprio. La libreria standard ha ogni sorta di cose inutili e uno swap membro è uno di questi. I sostenitori GotW collegati per la funzione di scambio dei membri.
Xeverous,

7

Tale codice è equivalente (in quasi tutti i modi) a:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second);
    // ...
};

inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
    using std::swap; 
    swap(first.mSize, second.mSize); 
    swap(first.mArray, second.mArray);
}

Una funzione di amico definita all'interno di una classe è:

  • collocato nello spazio dei nomi allegato
  • automaticamente inline
  • in grado di fare riferimento a membri statici della classe senza ulteriori qualifiche

Le regole esatte sono nella sezione [class.friend](cito i paragrafi 6 e 7 della bozza C ++ 0x):

Una funzione può essere definita in una dichiarazione di amicizia di una classe se e solo se la classe è una classe non locale (9.8), il nome della funzione non è qualificato e la funzione ha un ambito dello spazio dei nomi.

Tale funzione è implicitamente in linea. Una funzione di un amico definita in una classe è nell'ambito (lessicale) della classe in cui è definita. Una funzione amico definita al di fuori della classe non lo è.


2
In realtà, le funzioni degli amici non vengono inserite nello spazio dei nomi allegato, in C ++ standard. Il vecchio comportamento era chiamato "iniezione del nome dell'amico", ma fu sostituito da ADL, sostituito nel primo standard. Vedi la parte superiore di questo . (Il comportamento è abbastanza simile, però.)
GManNickG

1
Non proprio equivalente. Il codice nella domanda lo rende swapvisibile solo ad ADL. È un membro dello spazio dei nomi racchiuso, ma il suo nome non è visibile agli altri moduli di ricerca dei nomi. EDIT: Vedo che @GMan è stato di nuovo più veloce :) @Ben è sempre stato così nell'ISO C ++ :)
Johannes Schaub - litb

2
@Ben: No, l'iniezione di amici non è mai esistita in uno standard, ma è stata ampiamente utilizzata prima del quale l'idea (e il supporto del compilatore) tendevano a continuare, ma tecnicamente non c'è. friendle funzioni si trovano solo da ADL e se devono essere solo funzioni libere con friendaccesso, devono essere dichiarate come friendall'interno della classe e come normali dichiarazioni di funzioni libere al di fuori della classe. Puoi vedere quella necessità in questa risposta , per esempio.
GManNickG,

2
@towi: Poiché la funzione di amico è nell'ambito di spazio dei nomi, le risposte a tutte e tre le tue domande dovrebbero diventare chiare: (1) È una funzione gratuita, oltre ad avere un amico accesso ai membri privati ​​e protetti della classe. (2) Non è affatto un membro, né istanza né statica. (3) ADL non esegue la ricerca all'interno delle classi, ma questo va bene perché la funzione di amico ha uno spazio dei nomi.
Ben Voigt,

1
@ Ben. Nella specifica, la funzione è un membro dello spazio dei nomi e la frase "la funzione ha un ambito dello spazio dei nomi" può essere interpretata per dire che la funzione è un membro dello spazio dei nomi (praticamente dipende dal contesto di tale affermazione). E aggiunge un nome a quello spazio dei nomi che è visibile solo ad ADL (in realtà, IIRC alcune parti contraddicono altre parti nelle specifiche se viene aggiunto o meno un nome. Ma l'aggiunta di un nome è necessaria per rilevare dichiarazioni incompatibili aggiunte a quella spazio dei nomi, quindi in effetti viene aggiunto un nome invisibile . Vedi la nota in 3.3.1p4).
Johannes Schaub - litb
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.