Qual è il linguaggio copia-e-scambia?


2003

Cos'è questo idioma e quando dovrebbe essere usato? Quali problemi risolve? Il linguaggio cambia quando si usa C ++ 11?

Anche se è stato menzionato in molti luoghi, non abbiamo avuto nessuna domanda e risposta "che cos'è", quindi eccola qui. Ecco un elenco parziale dei luoghi in cui è stato precedentemente menzionato:



2
Fantastico, ho collegato questa domanda dalla mia risposta per spostare la semantica .
Fredoverflow,

4
È una buona idea avere una spiegazione a pieno titolo per questo idioma, è così comune che tutti dovrebbero saperlo.
Matthieu M.,

16
Avvertenza: l'idioma copia / scambio viene utilizzato molto più frequentemente di quanto sia utile. Spesso è dannoso per le prestazioni quando non è necessaria una forte garanzia di sicurezza delle eccezioni dall'assegnazione delle copie. E quando è necessaria un'eccezionale sicurezza delle eccezioni per l'assegnazione delle copie, è facilmente fornita da una breve funzione generica, oltre a un operatore di assegnazione delle copie molto più veloce. Vedi slideshare.net/ripplelabs/howard-hinnant-accu2014 diapositive 43 - 53. Riepilogo: copia / scambia è uno strumento utile nella casella degli strumenti. Ma è stato sopravvalutato e successivamente è stato spesso abusato.
Howard Hinnant,

2
@HowardHinnant: Sì, +1 a quello. Ho scritto questo in un momento in cui quasi tutte le domande su C ++ erano "aiutare la mia classe si blocca quando una copia" e questa è stata la mia risposta. È appropriato quando vuoi semplicemente lavorare sulla semantica copia / spostamento o altro, in modo da poter passare ad altre cose, ma non è davvero ottimale. Sentiti libero di mettere un disclaimer in cima alla mia risposta se pensi che possa aiutarti.
GManNickG

Risposte:


2184

Panoramica

Perché abbiamo bisogno del linguaggio copia-e-scambia?

Qualsiasi classe che gestisce una risorsa (un wrapper , come un puntatore intelligente) deve implementare The Big Three . Mentre gli obiettivi e l'implementazione del costruttore di copie e del distruttore sono semplici, l'operatore di assegnazione delle copie è senza dubbio il più sfumato e difficile. Come dovrebbe essere fatto? Quali insidie ​​devono essere evitate?

Il linguaggio copia-e-scambia è la soluzione e assiste con eleganza l'operatore di assegnazione nel realizzare due cose: evitare la duplicazione del codice e fornire una forte garanzia di eccezione .

Come funziona?

Concettualmente , funziona utilizzando la funzionalità del costruttore di copie per creare una copia locale dei dati, quindi accetta i dati copiati con una swapfunzione, scambiando i vecchi dati con i nuovi dati. La copia temporanea viene quindi distrutta, portando con sé i vecchi dati. Ci resta una copia dei nuovi dati.

Per usare il linguaggio copia-e-scambia, abbiamo bisogno di tre cose: un costruttore di copia funzionante, un distruttore funzionante (entrambi sono la base di qualsiasi wrapper, quindi dovrebbe essere completo comunque) e una swapfunzione.

Una funzione di scambio è una funzione non di lancio che scambia due oggetti di una classe, membro per membro. Potremmo essere tentati di usare std::swapinvece di fornire il nostro, ma ciò sarebbe impossibile; std::swapusa l'operatore di costruzione e copia-incarico all'interno della sua implementazione e alla fine proveremo a definire l'operatore di assegnazione in termini di se stesso!

(Non solo, ma le chiamate non qualificate swaputilizzeranno il nostro operatore di scambio personalizzato, saltando la costruzione e la distruzione non necessarie della nostra classe che std::swapcomporterebbe.)


Una spiegazione approfondita

L'obiettivo. il gol

Consideriamo un caso concreto. Vogliamo gestire, in una classe altrimenti inutile, un array dinamico. Iniziamo con un costruttore funzionante, un costruttore di copie e un distruttore:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Questa classe gestisce quasi correttamente l'array, ma deve operator=funzionare correttamente.

Una soluzione fallita

Ecco come potrebbe apparire un'implementazione ingenua:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

E diciamo che abbiamo finito; questo ora gestisce un array, senza perdite. Tuttavia, soffre di tre problemi, contrassegnati in sequenza nel codice come (n).

  1. Il primo è il test di autoassegnazione. Questo controllo ha due scopi: è un modo semplice per impedirci di eseguire codice inutile in caso di autoassegnazione e ci protegge da bug sottili (come eliminare l'array solo per provare a copiarlo). Ma in tutti gli altri casi serve semplicemente a rallentare il programma e ad agire come rumore nel codice; l'autoassegnazione si verifica raramente, quindi il più delle volte questo controllo è uno spreco. Sarebbe meglio se l'operatore potesse funzionare correttamente senza di essa.

  2. Il secondo è che fornisce solo una garanzia di eccezione di base. Se new int[mSize]fallisce, *thissarà stato modificato. (Vale a dire, la dimensione è errata e i dati sono spariti!) Per una forte garanzia di eccezione, dovrebbe essere qualcosa di simile a:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
    
  3. Il codice si è espanso! Il che ci porta al terzo problema: la duplicazione del codice. Il nostro operatore di assegnazione duplica in modo efficace tutto il codice che abbiamo già scritto altrove, ed è una cosa terribile.

Nel nostro caso, il nucleo di esso è solo due righe (l'allocazione e la copia), ma con risorse più complesse questo codice gonfio può essere una seccatura. Dovremmo sforzarci di non ripeterci mai.

(Ci si potrebbe chiedere: se questo codice è necessario per gestire correttamente una risorsa, cosa succede se la mia classe ne gestisce più di una? Mentre questa può sembrare una preoccupazione valida e in effetti richiede clausole try/ non banali catch, questa è una non -issue. Questo perché una classe dovrebbe gestire una sola risorsa !)

Una soluzione di successo

Come accennato, il linguaggio copia-e-scambia risolverà tutti questi problemi. Ma in questo momento, abbiamo tutti i requisiti tranne uno: una swapfunzione. Mentre The Rule of Three implica con esistenza l'esistenza del nostro costruttore di copie, operatore di assegnazione e distruttore, in realtà dovrebbe essere chiamato "I tre e mezzo grandi": ogni volta che la tua classe gestisce una risorsa ha anche senso fornire una swapfunzione .

Dobbiamo aggiungere funzionalità di scambio alla nostra classe e lo facciamo come segue †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( Ecco la spiegazione del perché public friend swap.) Ora non solo possiamo scambiare i nostri dumb_array, ma gli swap in generale possono essere più efficienti; scambia semplicemente puntatori e dimensioni, piuttosto che allocare e copiare interi array. A parte questo vantaggio in termini di funzionalità ed efficienza, siamo ora pronti a implementare il linguaggio copia-e-scambia.

Senza ulteriori indugi, il nostro operatore di incarichi è:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

E questo è tutto! Con un colpo solo, tutti e tre i problemi vengono elegantemente affrontati contemporaneamente.

Perché funziona

Per prima cosa notiamo una scelta importante: l'argomento parametro è preso per valore . Mentre uno potrebbe altrettanto facilmente fare quanto segue (e in effetti, molte implementazioni ingenue dell'idioma lo fanno):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Perdiamo un'importante opportunità di ottimizzazione . Non solo, ma questa scelta è fondamentale in C ++ 11, che verrà discussa più avanti. (In linea generale, una linea guida notevolmente utile è la seguente: se hai intenzione di fare una copia di qualcosa in una funzione, lascia che il compilatore lo faccia nell'elenco dei parametri. ‡)

In entrambi i casi, questo metodo per ottenere la nostra risorsa è la chiave per eliminare la duplicazione del codice: possiamo usare il codice dal costruttore di copie per fare la copia, e non abbiamo mai bisogno di ripeterlo. Ora che la copia è stata fatta, siamo pronti per lo scambio.

Osservare che inserendo la funzione tutti i nuovi dati sono già allocati, copiati e pronti per essere utilizzati. Questo è ciò che ci dà una forte garanzia eccezionale gratuitamente: non entreremo nemmeno nella funzione se la costruzione della copia fallisce, e quindi non è possibile modificare lo stato di *this. (Quello che abbiamo fatto manualmente prima per una forte garanzia di eccezione, il compilatore sta facendo per noi ora; che gentile.)

A questo punto siamo liberi da casa, perché swapnon lancia. Scambiamo i nostri dati attuali con i dati copiati, alterando in sicurezza il nostro stato e i vecchi dati vengono inseriti nel temporaneo. I vecchi dati vengono quindi rilasciati quando la funzione ritorna. (Dove finisce l'ambito del parametro e viene chiamato il suo distruttore.)

Poiché il linguaggio non ripete nessun codice, non possiamo introdurre bug all'interno dell'operatore. Si noti che questo significa che ci siamo liberati della necessità di un controllo di auto-assegnazione, consentendo un'unica implementazione uniforme di operator=. (Inoltre, non prevediamo più una penalità per prestazioni non assegnabili).

E questo è l'idioma copia-e-scambia.

Che dire di C ++ 11?

La prossima versione di C ++, C ++ 11, apporta una modifica molto importante al modo in cui gestiamo le risorse: la Regola del Tre è ora La Regola del Quattro (e mezzo). Perché? Perché non solo dobbiamo essere in grado di costruire-copia la nostra risorsa, ma dobbiamo anche spostarla-costruirla .

Fortunatamente per noi, questo è facile:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

Cosa sta succedendo qui? Ricorda l'obiettivo della costruzione di mosse: prendere le risorse da un'altra istanza della classe, lasciandole in uno stato garantito per essere assegnabile e distruttibile.

Quindi quello che abbiamo fatto è semplice: inizializzare tramite il costruttore predefinito (una funzionalità C ++ 11), quindi scambiare con other; sappiamo che un'istanza predefinita della nostra classe può essere assegnata e distrutta in modo sicuro, quindi sappiamo che othersarà in grado di fare lo stesso dopo lo scambio.

(Si noti che alcuni compilatori non supportano la delega del costruttore; in questo caso, è necessario creare manualmente manualmente la classe predefinita. Si tratta di un'attività sfortunata ma fortunatamente banale.)

Perché funziona?

Questo è l'unico cambiamento che dobbiamo apportare alla nostra classe, quindi perché funziona? Ricorda la decisione sempre importante che abbiamo preso per rendere il parametro un valore e non un riferimento:

dumb_array& operator=(dumb_array other); // (1)

Ora, se otherviene inizializzato con un valore, verrà costruito in modo movimento . Perfetto. Allo stesso modo C ++ 03 riutilizziamo la nostra funzionalità di costruzione di copia prendendo l'argomento per valore, C ++ 11 sceglierà automaticamente anche il costruttore di mosse quando appropriato. (E, naturalmente, come menzionato nell'articolo precedentemente collegato, la copia / spostamento del valore può essere semplicemente elusa del tutto.)

E così conclude il linguaggio copia-e-scambia.


Le note

* Perché impostiamo mArraysu null? Perché se viene lanciato un ulteriore codice nell'operatore, il distruttore di dumb_arraypotrebbe essere chiamato; e se ciò accade senza impostarlo su null, proviamo a eliminare la memoria che è già stata eliminata! Lo evitiamo impostandolo su null, poiché l'eliminazione di null è un'operazione nulla.

† Ci sono altre affermazioni che dovremmo specializzarci std::swapper il nostro tipo, fornire un servizio in classe swapa fianco di una funzione libera swap, ecc. Ma tutto ciò non è necessario: qualsiasi uso corretto swapavverrà attraverso una chiamata non qualificata e la nostra funzione sarà trovato attraverso ADL . Una funzione farà.

‡ Il motivo è semplice: una volta che hai la risorsa per te, puoi scambiarla e / o spostarla (C ++ 11) ovunque sia necessario. E facendo la copia nell'elenco dei parametri, massimizzi l'ottimizzazione.

†† Il costruttore di mosse dovrebbe essere generalmente noexcept, altrimenti un codice (ad es. std::vectorLogica di ridimensionamento) utilizzerà il costruttore di copie anche quando una mossa avrebbe senso. Naturalmente, contrassegnalo solo se il codice all'interno non genera eccezioni.


17
@GMan: Direi che una classe che gestisce più risorse contemporaneamente è destinata a fallire (la sicurezza delle eccezioni diventa da incubo) e raccomanderei caldamente che una classe gestisca UNA risorsa OPPURE abbia funzionalità aziendali e gestori di utilizzo.
Matthieu M.,

22
Non capisco perché il metodo di scambio sia dichiarato amico qui?
szx,

9
@asd: per consentirne l'individuazione tramite ADL.
GManNickG,

8
@neuviemeporte: con la parentesi, gli elementi degli array vengono inizializzati di default. Senza, non sono inizializzati. Dato che nel costruttore di copie sovrascriveremo comunque i valori, possiamo saltare l'inizializzazione.
GManNickG

10
@neuviemeporte: Devi swapessere trovato durante ADL se vuoi che funzioni con la maggior parte del codice generico che troverai, come boost::swape altre varie istanze di swap. Lo scambio è un problema complicato in C ++ e in generale siamo tutti d'accordo sul fatto che un singolo punto di accesso è il migliore (per coerenza) e l'unico modo per farlo in generale è una funzione gratuita ( intnon può avere un membro di scambio, per esempio). Vedi la mia domanda per alcuni retroscena.
GManNickG

274

L'assegnazione, in sostanza, prevede due passaggi: abbattere il vecchio stato dell'oggetto e costruirne uno nuovo come copia dello stato di qualche altro oggetto.

Fondamentalmente, è quello che fanno il distruttore e il costruttore di copie , quindi la prima idea sarebbe quella di delegare il lavoro a loro. Tuttavia, poiché la distruzione non può fallire, mentre la costruzione potrebbe, in realtà vogliamo farlo al contrario : prima esegui la parte costruttiva e, se ciò ha avuto successo, quindi fai la parte distruttiva . L'idioma copia-e-scambia è un modo per fare proprio questo: prima chiama il costruttore di copie di una classe per creare un oggetto temporaneo, quindi scambia i suoi dati con quelli del temporaneo e quindi lascia che il distruttore del temporaneo distrugga il vecchio stato.
Daswap()si suppone che non fallisca mai, l'unica parte che potrebbe fallire è la costruzione della copia. Ciò viene eseguito per primo e, se fallisce, nulla verrà modificato nell'oggetto target.

Nella sua forma raffinata, il copia-e-scambia viene implementato facendo eseguire la copia inizializzando il parametro (senza riferimento) dell'operatore di assegnazione:

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

1
Penso che menzionare il brufolo sia importante quanto menzionare la copia, lo scambio e la distruzione. Lo swap non è magicamente sicuro per le eccezioni. È sicuro dalle eccezioni perché lo scambio di puntatori è sicuro dalle eccezioni. Non è necessario utilizzare un brufolo, ma in caso contrario è necessario assicurarsi che ogni scambio di un membro sia sicuro dalle eccezioni. Questo può essere un incubo quando questi membri possono cambiare ed è banale quando sono nascosti dietro un brufolo. E poi, arriva il costo del brufolo. Il che ci porta alla conclusione che spesso la sicurezza delle eccezioni comporta un costo in termini di prestazioni.
Wilhelmtell,

7
std::swap(this_string, that)non fornisce una garanzia di non lancio. Fornisce una forte sicurezza delle eccezioni, ma non una garanzia di non lancio.
Wilhelmtell,

11
@wilhelmtell: in C ++ 03 non si fa menzione delle eccezioni potenzialmente generate da std::string::swap(che viene chiamato da std::swap). In C ++ 0x, std::string::swapè noexcepte non deve generare eccezioni.
James McNellis,

2
@sbi @JamesMcNellis ok, ma il punto è ancora valido: se hai membri di tipo di classe devi assicurarti di scambiarli senza un tiro. Se hai un singolo membro che è un puntatore, allora è banale. Altrimenti non lo è.
Wilhelmtell,

2
@wilhelmtell: ho pensato che fosse questo il punto di scambiare: non getta mai ed è sempre O (1) (sì, lo so, std::array...)
sbi

44

Ci sono già alcune buone risposte. Mi concentrerò principalmente su ciò che ritengo manchi - una spiegazione dei "contro" con il linguaggio copia-e-scambia ...

Qual è il linguaggio copia-e-scambia?

Un modo di implementare l'operatore di assegnazione in termini di una funzione di scambio:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

L'idea fondamentale è che:

  • la parte più soggetta a errori nell'assegnare a un oggetto è garantire che siano acquisite tutte le risorse di cui il nuovo stato ha bisogno (ad es. memoria, descrittori)

  • tale acquisizione può essere tentata prima di modificare lo stato corrente dell'oggetto (ovvero *this) se viene effettuata una copia del nuovo valore, motivo per cui rhsviene accettato dal valore (ovvero copiato) anziché dal riferimento

  • scambiare lo stato della copia locale rhsed *thisè generalmente relativamente facile da fare senza potenziali guasti / eccezioni, dato che la copia locale non ha bisogno di alcuno stato particolare in seguito (ha solo bisogno dello stato adatto all'esecuzione del distruttore, tanto quanto a un oggetto che viene spostato da in> = C ++ 11)

Quando dovrebbe essere usato? (Quali problemi risolve [/ create] ?)

  • Quando si desidera che l'oggetto assegnato venga obiettato inalterato da un compito che genera un'eccezione, supponendo che si abbia o si possa scrivere una swapgaranzia con eccezionale eccezione, e idealmente uno che non può fallire / throw.. †

  • Quando si desidera un modo pulito, facile da capire e robusto per definire l'operatore di assegnazione in termini di (più semplice) funzione di costruzione copie swape distruttore.

    • L'autoassegnazione eseguita come copia e scambia evita casi limite spesso trascurati. ‡

  • Quando qualsiasi penalità prestazionale o un utilizzo delle risorse momentaneamente maggiore creato dall'avere un oggetto temporaneo aggiuntivo durante l'assegnazione non è importante per la tua applicazione. ⁂

swaplancio: in genere è possibile scambiare in modo affidabile membri di dati che gli oggetti tracciano per puntatore, ma membri di dati senza puntatore che non hanno uno scambio privo di lancio o per i quali è necessario implementare lo scambio X tmp = lhs; lhs = rhs; rhs = tmp;e la costruzione della copia o l'assegnazione potrebbe lanciare, potrebbe comunque non riuscire a lasciare alcuni membri dei dati scambiati e altri no. Questo potenziale si applica anche al C ++ 03 std::stringquando James commenta un'altra risposta:

@wilhelmtell: in C ++ 03 non si fa menzione delle eccezioni potenzialmente generate da std :: string :: swap (che viene chiamato da std :: swap). In C ++ 0x, std :: string :: swap è noexcept e non deve generare eccezioni. - James McNellis, 22 dicembre 10 alle 15:24


‡ L'implementazione dell'operatore di assegnazione che sembra sana quando si assegna da un oggetto distinto può facilmente fallire per l'autoassegnazione. Sebbene possa sembrare inimmaginabile che il codice client tenti persino di autoassegnarsi, può accadere relativamente facilmente durante le operazioni algo su container, con x = f(x);codice in cui f(forse solo per alcuni #ifdeframi) è una macro ala #define f(x) xo una funzione che restituisce un riferimento a x, o addirittura (probabilmente codice inefficiente ma conciso) come x = c1 ? x * 2 : c2 ? x / 2 : x;). Per esempio:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

In caso di auto-assegnazione, il codice sopra riportato cancella x.p_;, punta p_a una nuova regione di heap allocata, quindi tenta di leggere i dati non inizializzati in essa contenuti (comportamento indefinito), se ciò non fa nulla di strano, copytenta un auto-assegnazione a ogni giusto- 'T' distrutto!


⁂ Il linguaggio copia-e-scambia può introdurre inefficienze o limitazioni dovute all'uso di un temporaneo extra (quando il parametro dell'operatore è costruito su copia):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Qui, un scritto a mano Client::operator=potrebbe verificare se *thisè già connesso allo stesso server di rhs(magari inviando un codice "reset" se utile), mentre l'approccio copia-e-scambia invocherebbe il costruttore di copia che probabilmente verrebbe scritto per aprire una connessione socket distinta quindi chiudere quella originale. Ciò non solo potrebbe significare un'interazione di rete remota anziché una semplice copia variabile in-process, ma potrebbe anche ostacolare i limiti del client o del server su risorse socket o connessioni. (Naturalmente questa classe ha un'interfaccia piuttosto orribile, ma questa è un'altra questione ;-P).


4
Detto questo, una connessione socket era solo un esempio: lo stesso principio si applica a qualsiasi inizializzazione potenzialmente costosa, come sondaggio hardware / inizializzazione / calibrazione, generazione di un pool di thread o numeri casuali, alcune attività di crittografia, cache, scansioni del file system, database connessioni ecc.
Tony Delroy,

C'è un altro (enorme) truffatore. A partire dalle specifiche attuali tecnicamente l'oggetto non avrà un operatore di assegnazione di movimenti! Se successivamente utilizzata come membro di una classe, la nuova classe non sarà generata automaticamente da move-ctor! Fonte: youtu.be/mYrbivnruYw?t=43m14s
user362515

3
Il problema principale con l'operatore di assegnazione delle copie Clientè che l'assegnazione non è vietata.
sabato

Nell'esempio del client, la classe deve essere resa non copiabile.
John Z. Li

25

Questa risposta è più simile a un'aggiunta e una leggera modifica alle risposte sopra.

In alcune versioni di Visual Studio (e forse di altri compilatori) c'è un bug che è davvero fastidioso e non ha senso. Quindi se dichiari / definisci la tua swapfunzione in questo modo:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... il compilatore ti urlerà quando chiami la swapfunzione:

inserisci qui la descrizione dell'immagine

Ciò ha a che fare con una friendfunzione chiamata e l' thisoggetto passato come parametro.


Un modo per aggirare questo è di non usare la friendparola chiave e ridefinire la swapfunzione:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Questa volta, puoi semplicemente chiamare swape passare other, rendendo così felice il compilatore:

inserisci qui la descrizione dell'immagine


Dopotutto, non è necessario utilizzare una friendfunzione per scambiare 2 oggetti. Ha lo stesso senso rendere swapuna funzione membro che ha un otheroggetto come parametro.

Hai già accesso thisall'oggetto, quindi passarlo come parametro è tecnicamente ridondante.


1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg . Questa è una versione semplificata. Sembra che si verifichi un errore ogni volta che friendviene chiamata una funzione con il *thisparametro
Oleksiy

1
@GManNickG come ho detto, è un bug e potrebbe funzionare bene per altre persone. Volevo solo aiutare alcune persone che potrebbero avere lo stesso problema. Ho provato questo con Visual Studio 2012 Express e 2013 Preview e l'unica cosa che l'ha fatto sparire è stata la mia modifica
Oleksiy,

8
@GManNickG non si adatterebbe in un commento con tutte le immagini e gli esempi di codice. Ed è ok se le persone votano male, sono sicuro che c'è qualcuno là fuori che sta ottenendo lo stesso bug; le informazioni in questo post potrebbero essere proprio ciò di cui hanno bisogno.
Oleksiy,

14
si noti che questo è solo un bug nell'evidenziazione del codice IDE (IntelliSense) ... Verrà compilato correttamente senza avvisi / errori.
Amro,

3
Si prega di segnalare qui il bug VS se non lo si è già fatto (e se non è stato corretto) connect.microsoft.com/VisualStudio
Matt

15

Vorrei aggiungere un avvertimento quando si ha a che fare con contenitori compatibili con l'allocatore in stile C ++ 11. Lo scambio e l'assegnazione hanno una semantica leggermente diversa.

Per concretezza, consideriamo un contenitore std::vector<T, A>, in cui Aè presente un tipo di allocatore con stato, e confronteremo le seguenti funzioni:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

Lo scopo di entrambe le funzioni fsed fmè di dare alo stato che baveva inizialmente. Tuttavia, c'è una domanda nascosta: cosa succede se a.get_allocator() != b.get_allocator()? La risposta è, dipende. Scrittura di Let AT = std::allocator_traits<A>.

  • Se AT::propagate_on_container_move_assignmentè std::true_type, quindi fmriassegna l'allocatore di acon il valore di b.get_allocator(), altrimenti non lo fa e acontinua a utilizzare l'allocatore originale. In tal caso, gli elementi di dati devono essere scambiati singolarmente, poiché la memorizzazione ae bnon è compatibile.

  • Se AT::propagate_on_container_swapè std::true_type, quindi fsswap sia i dati e allocatori al modo previsto.

  • Se lo AT::propagate_on_container_swapè std::false_type, allora abbiamo bisogno di un controllo dinamico.

    • Se a.get_allocator() == b.get_allocator(), quindi i due contenitori utilizzano una memoria compatibile e lo scambio procede nel modo consueto.
    • Tuttavia, se a.get_allocator() != b.get_allocator(), il programma ha un comportamento indefinito (cfr. [Container.requirements.general / 8].

Il risultato è che lo scambio è diventato un'operazione non banale in C ++ 11 non appena il container inizia a supportare allocatori con stato. Questo è un po 'un "caso d'uso avanzato", ma non è del tutto improbabile, poiché le ottimizzazioni di spostamento di solito diventano interessanti solo quando la classe gestisce una risorsa e la memoria è una delle risorse più popolari.

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.