Come sovraccaricare std :: swap ()


115

std::swap()viene utilizzato da molti contenitori std (come std::liste std::vector) durante l'ordinamento e anche l'assegnazione.

Ma l'implementazione standard di swap()è molto generalizzata e piuttosto inefficiente per i tipi personalizzati.

Pertanto l'efficienza può essere ottenuta sovraccaricando std::swap()con un'implementazione specifica del tipo personalizzata. Ma come puoi implementarlo in modo che venga utilizzato dai contenitori std?



Il Swappable pagina si trasferisce a en.cppreference.com/w/cpp/named_req/Swappable
Andrew Keeton

Risposte:


135

Il modo giusto per sovraccaricare lo scambio è scriverlo nello stesso spazio dei nomi di quello che stai scambiando, in modo che possa essere trovato tramite la ricerca dipendente dagli argomenti (ADL) . Una cosa particolarmente facile da fare è:

class X
{
    // ...
    friend void swap(X& a, X& b)
    {
        using std::swap; // bring in swap for built-in types

        swap(a.base1, b.base1);
        swap(a.base2, b.base2);
        // ...
        swap(a.member1, b.member1);
        swap(a.member2, b.member2);
        // ...
    }
};

11
In C ++ 2003 è nella migliore delle ipotesi sotto specificato. La maggior parte delle implementazioni utilizza ADL per trovare lo scambio, ma no non è obbligatorio, quindi non puoi contare su di esso. È possibile specializzarsi std :: swap per un determinato tipo concreto, come mostrato dal PO; semplicemente non aspettarti che quella specializzazione venga utilizzata, ad esempio per classi derivate di quel tipo.
Dave Abrahams

15
Sarei sorpreso di scoprire che le implementazioni ancora non usano ADL per trovare lo swap corretta. Questa è una vecchia questione in commissione. Se la tua implementazione non usa ADL per trovare lo scambio, invia una segnalazione di bug.
Howard Hinnant

3
@Sascha: in primo luogo, sto definendo la funzione nell'ambito dello spazio dei nomi perché è l'unico tipo di definizione che conta per il codice generico. Perché int et. al. non / non posso avere funzioni membro, std :: sort et. al. utilizzare una funzione libera; stabiliscono il protocollo. Secondo, non so perché ti opponi ad avere due implementazioni, ma la maggior parte delle classi è destinata a essere ordinata in modo inefficiente se non puoi accettare di avere uno scambio di non membri. Le regole di sovraccarico assicurano che se vengono visualizzate entrambe le dichiarazioni, verrà scelta quella più specifica (questa) quando lo scambio viene chiamato senza qualificazione.
Dave Abrahams

5
@ Mozza314: dipende. Un std::sortcodice che utilizza ADL per scambiare elementi non è conforme a C ++ 03 ma conforme a C ++ 11. Inoltre, perché -1 una risposta basata sul fatto che i client potrebbero utilizzare codice non idiomatico?
JoeG

4
@curiousguy: Se leggere lo standard fosse solo una semplice questione di leggere lo standard, avresti ragione :-). Purtroppo, l'intento degli autori conta. Quindi, se l'intento originale era che ADL potesse o dovrebbe essere usato, è sottospecificato. In caso contrario, è solo un semplice cambiamento di rottura per C ++ 0x, motivo per cui ho scritto "nella migliore delle ipotesi" sotto specificato.
Dave Abrahams

70

Attenzione Mozza314

Ecco una simulazione degli effetti di una std::algorithmchiamata generica std::swape che l'utente fornisce il proprio scambio nello spazio dei nomi std. Poiché si tratta di un esperimento, questa simulazione utilizza namespace expinvece di namespace std.

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            exp::swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

namespace exp
{
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Per me questo stampa:

generic exp::swap

Se il tuo compilatore stampa qualcosa di diverso, non sta implementando correttamente la "ricerca in due fasi" per i modelli.

Se il tuo compilatore è conforme (a uno qualsiasi di C ++ 98/03/11), allora darà lo stesso output che mostro. E in quel caso accade esattamente quello che temi che accada. E mettere il tuo swapin namespace std( exp) non ha impedito che accadesse.

Dave ed io siamo entrambi membri del comitato e lavoriamo in quest'area dello standard da un decennio (e non sempre in accordo l'uno con l'altro). Ma la questione è stata risolta da molto tempo e siamo entrambi d'accordo su come è stata risolta. Ignora l'opinione / risposta degli esperti di Dave in questo settore a tuo rischio e pericolo.

Questo problema è emerso dopo la pubblicazione di C ++ 98. A partire dal 2001, Dave e io abbiamo iniziato a lavorare in quest'area . E questa è la soluzione moderna:

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

void swap(A&, A&)
{
    printf("swap(A, A)\n");
}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

L'output è:

swap(A, A)

Aggiornare

È stata fatta un'osservazione che:

namespace exp
{    
    template <>
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

lavori! Allora perché non usarlo?

Considera il caso in cui il tuo Aè un modello di classe:

// simulate user code which includes <algorithm>

template <class T>
struct A
{
};

namespace exp
{

    template <class T>
    void swap(A<T>&, A<T>&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A<int> a[2];
    exp::algorithm(a, a+2);
}

Ora non funziona più. :-(

Quindi potresti inserire lo swapspazio dei nomi std e farlo funzionare. Ma è necessario ricordarsi di mettere swapin A's spazio dei nomi per il caso in cui si dispone di un modello: A<T>. E dal momento che entrambi i casi, funzionano se si mette swapin A's spazio dei nomi, è solo più facile da ricordare (e ad altri insegnano) per farlo solo che un modo.


4
Grazie mille per la risposta dettagliata. Sono chiaramente meno informato su questo e in realtà mi chiedevo come il sovraccarico e la specializzazione possano produrre comportamenti diversi. Tuttavia, non sto suggerendo il sovraccarico ma la specializzazione. Quando inserisco il template <>tuo primo esempio ottengo l'output exp::swap(A, A)da gcc. Allora, perché non preferire la specializzazione?
voltrevo

1
Wow! Questo è davvero illuminante. Mi hai decisamente convinto. Penso che modificherò leggermente il tuo suggerimento e userò la sintassi degli amici in classe di Dave Abrahams (ehi, posso usarla anche per l'operatore <<! :-)), a meno che tu non abbia un motivo per evitarlo (a parte la compilazione separatamente). Inoltre, alla luce di ciò, pensi che using std::swapsia un'eccezione alla regola "non inserire mai istruzioni using nei file di intestazione"? In effetti, perché non metterlo using std::swapdentro <algorithm>? Suppongo che potrebbe infrangere una piccola minoranza del codice delle persone. Forse deprecare il supporto e alla fine inserirlo?
voltrevo

3
la sintassi degli amici in classe dovrebbe andare bene. Proverei a limitare using std::swapl'ambito della funzione all'interno delle intestazioni. Sì, swapè quasi una parola chiave. Ma no, non è proprio una parola chiave. Quindi è meglio non esportarlo in tutti gli spazi dei nomi finché non è necessario. swapè molto simile operator==. La differenza più grande è che nessuno pensa nemmeno di chiamare operator==con la sintassi dello spazio dei nomi qualificata (sarebbe semplicemente troppo brutto).
Howard Hinnant

15
@NielKirk: Quello che vedi come una complicazione sono semplicemente troppe risposte sbagliate. Non c'è niente di complicato nella risposta corretta di Dave Abrahams: "Il modo giusto per sovraccaricare lo swap è scriverlo nello stesso spazio dei nomi di quello che stai scambiando, in modo che possa essere trovato tramite la ricerca dipendente dall'argomento (ADL)."
Howard Hinnant

2
@ codeshot: Scusa. Herb ha cercato di far passare questo messaggio dal 1998: gotw.ca/publications/mill02.htm Non menziona lo scambio in questo articolo. Ma questa è solo un'altra applicazione del Principio dell'interfaccia di Herb.
Howard Hinnant

53

Non sei autorizzato (dallo standard C ++) a sovraccaricare std :: swap, tuttavia sei specificamente autorizzato ad aggiungere specializzazioni di modello per i tuoi tipi allo spazio dei nomi std. Per esempio

namespace std
{
    template<>
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}

quindi gli usi nei contenitori std (e in qualsiasi altro luogo) sceglieranno la tua specializzazione invece di quella generale.

Si noti inoltre che fornire un'implementazione della classe base di swap non è sufficiente per i tipi derivati. Ad esempio, se hai

class Base
{
    // ... stuff ...
}
class Derived : public Base
{
    // ... stuff ...
}

namespace std
{
    template<>
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}

questo funzionerà per le classi Base, ma se provi a scambiare due oggetti derivati ​​userà la versione generica da std perché lo scambio basato su modellièuna corrispondenza esatta (ed evita il problema di scambiare solo le parti 'base' dei tuoi oggetti derivati ).

NOTA: l'ho aggiornato per rimuovere i bit sbagliati dalla mia ultima risposta. D'oh! (grazie puetzk e j_random_hacker per averlo fatto notare)


1
Per lo più un buon consiglio, ma devo -1 a causa della sottile distinzione notata da puetzk tra specializzare un modello nello spazio dei nomi std (che è consentito dallo standard C ++) e il sovraccarico (che non lo è).
j_random_hacker

11
Downvoted perché il modo corretto per personalizzare lo scambio è farlo nel tuo spazio dei nomi (come sottolinea Dave Abrahams in un'altra risposta).
Howard Hinnant

2
Le mie ragioni per il downvoting sono le stesse di Howard
Dave Abrahams

13
@ HowardHinnant, Dave Abrahams: non sono d'accordo. Su quale base affermi che la tua alternativa è il modo "corretto"? Come puetzk ha citato dallo standard, ciò è specificamente consentito. Anche se sono nuovo in questo problema, non mi piace davvero il metodo che sostenete perché se definisco Foo e scambio in questo modo è probabile che qualcun altro che usa il mio codice utilizzi std :: swap (a, b) anziché swap ( a, b) su Foo, che utilizza silenziosamente la versione predefinita inefficiente.
voltrevo

5
@ Mozza314: I vincoli di spazio e formattazione dell'area commenti non mi hanno permesso di risponderti completamente. Si prega di consultare la risposta che ho aggiunto intitolata "Attenzione Mozza314".
Howard Hinnant

29

Sebbene sia corretto che non si debba generalmente aggiungere cose allo spazio dei nomi std ::, l'aggiunta di specializzazioni di template per i tipi definiti dall'utente è specificamente consentita. Il sovraccarico delle funzioni non lo è. Questa è una sottile differenza :-)

17.4.3.1/1 Non è definito per un programma C ++ aggiungere dichiarazioni o definizioni allo spazio dei nomi std o spazi dei nomi con spazio dei nomi std se non diversamente specificato. Un programma può aggiungere specializzazioni di modello per qualsiasi modello di libreria standard allo spazio dei nomi std. Una tale specializzazione (completa o parziale) di una libreria standard risulta in un comportamento indefinito a meno che la dichiarazione non dipenda da un nome definito dall'utente di collegamento esterno e a meno che la specializzazione del modello non soddisfi i requisiti della libreria standard per il modello originale.

Una specializzazione di std :: swap sarebbe simile a:

namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

Senza il bit template <> sarebbe un sovraccarico, che non è definito, piuttosto che una specializzazione, che è consentita. L'approccio suggerito da @ Wilka per modificare lo spazio dei nomi predefinito potrebbe funzionare con il codice utente (a causa della ricerca di Koenig che preferisce la versione senza spazio dei nomi) ma non è garantito, e in effetti non dovrebbe farlo (l'implementazione STL dovrebbe usare completamente -qualified std :: swap).

C'è un thread su comp.lang.c ++. Moderato con una lunga discussione sull'argomento. Tuttavia, la maggior parte riguarda la specializzazione parziale (cosa che al momento non è possibile fare).


7
Uno dei motivi per cui è sbagliato usare la specializzazione dei modelli di funzione per questo (o altro): interagisce in modi negativi con i sovraccarichi, di cui ce ne sono molti per lo scambio. Ad esempio, se specializzi il normale std :: swap per std :: vector <mytype> &, la tua specializzazione non verrà scelta rispetto allo scambio specifico del vettore dello standard, perché le specializzazioni non vengono considerate durante la risoluzione dell'overload.
Dave Abrahams

4
Questo è anche ciò che Meyers raccomanda in Effective C ++ 3ed (Item 25, pp 106-112).
jww

2
@DaveAbrahams: Se siete specializzati (senza argomenti espliciti template), ordinamento parziale causerà che sia una specializzazione della la vectorversione e sarà utilizzato .
Davis Herring

1
@DavisHerring in realtà, no, quando fai l'ordinamento parziale non gioca alcun ruolo. Il problema non è che non puoi chiamarlo affatto; è ciò che accade in presenza di sovraccarichi di swap apparentemente meno specifici: wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams

2
@DaveAbrahams: l'ordinamento parziale consiste nel selezionare il modello di funzione da specializzare quando la specializzazione esplicita corrisponde a più di uno. Il ::swapsovraccarico che hai aggiunto è più specializzato rispetto al std::swapsovraccarico per vector, quindi cattura la chiamata e nessuna specializzazione di quest'ultimo è rilevante. Non sono sicuro di come sia un problema pratico (ma non sto nemmeno affermando che questa sia una buona idea!).
Davis Herring
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.