Perché un T * può essere passato nel registro, ma un unique_ptr <T> non può?


85

Sto guardando il discorso di Chandler Carruth in CppCon 2019:

Non ci sono astrazioni a costo zero

in esso, fornisce l'esempio di come è stato sorpreso da quanto sovraccarico si incorre usando un std::unique_ptr<int>over an int*; quel segmento inizia all'incirca alle 17:25.

Puoi dare un'occhiata ai risultati della compilazione del suo esempio paio di frammenti (godbolt.org) - per testimoniare che, in effetti, sembra che il compilatore non sia disposto a passare il valore unique_ptr - che in effetti è solo un indirizzo - all'interno di un registro, solo nella memoria diretta.

Uno dei punti sollevati da Carruth intorno alle 27:00 è che l'ABI C ++ richiede che i parametri per valore (alcuni ma non tutti; forse - tipi non primitivi? Tipi non banalmente costruibili?) Per essere passati in memoria piuttosto che all'interno di un registro.

Le mie domande:

  1. Questo è effettivamente un requisito ABI su alcune piattaforme? (quale?) O forse è solo un po 'di pessimizzazione in determinati scenari?
  2. Perché l'ABI è così? Cioè, se i campi di una struttura / classe rientrano nei registri o anche in un singolo registro - perché non dovremmo essere in grado di passarlo all'interno di quel registro?
  3. Il comitato per gli standard C ++ ha discusso questo punto negli ultimi anni o mai?

PS - Per non lasciare questa domanda senza codice:

Puntatore normale:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

Puntatore unico:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}

8
Non sono sicuro di quale sia esattamente il requisito ABI, ma non vieta di inserire le strutture nei registri
harold,

6
Se dovessi indovinare, direi che ha a che fare con funzioni membro non banali che richiedono un thispuntatore che punti in una posizione valida. unique_ptrha quelli. Spargere il registro a tale scopo significherebbe negare l'intera ottimizzazione "passa in un registro".
StoryTeller - Unslander Monica,

2
itanium-cxx-abi.github.io/cxx-abi/abi.html#calls . Quindi questo comportamento richiesto. Perché? itanium-cxx-abi.github.io/cxx-abi/cxx-closed.html , cerca il problema C-7. C'è qualche spiegazione lì, ma non è troppo dettagliata. Ma sì, questo comportamento non mi sembra logico. Questi oggetti possono essere passati normalmente attraverso lo stack. Spingerli a impilare e quindi passare il riferimento (solo per oggetti "non banali") sembra uno spreco.
geza,

6
Sembra che C ++ stia violando i suoi principi qui, il che è abbastanza triste. Ero convinto al 140% che qualsiasi unique_ptr scompaia solo dopo la compilazione. Dopotutto è solo una chiamata di distruttore differita che è nota in fase di compilazione.
One Man Monkey Squad,

7
@MaximEgorushkin: se l'avessi scritto a mano, avresti messo il puntatore in un registro e non nello stack.
einpoklum,

Risposte:


49
  1. Si tratta in realtà di un requisito ABI o forse è solo una pessimizzazione in determinati scenari?

Un esempio è System V Application Binary Interface AMD64 architettura del processore Supplement . Questa ABI è per CPU compatibili con x86 a 64 bit (Linux x86_64 architecure). È seguito su Solaris, Linux, FreeBSD, macOS, sottosistema Windows per Linux:

Se un oggetto C ++ ha un costruttore di copie non banale o un distruttore non banale, viene passato per riferimento invisibile (l'oggetto viene sostituito nell'elenco dei parametri da un puntatore con classe INTEGER).

Un oggetto con un costruttore di copie non banale o un distruttore non banale non può essere passato per valore perché tali oggetti devono avere indirizzi ben definiti. Problemi simili si applicano quando si restituisce un oggetto da una funzione.

Si noti che solo 2 registri di uso generale possono essere utilizzati per passare 1 oggetto con un costruttore di copie banale e un distruttore di banale, vale a dire solo i valori di oggetti con sizeofnon più di 16 possono essere passati nei registri. Vedere Convenzioni di chiamata di Agner Fog per un trattamento dettagliato delle convenzioni di chiamata, in particolare §7.1 Passaggio e restituzione di oggetti. Esistono convenzioni di chiamata separate per il passaggio dei tipi SIMD nei registri.

Esistono diverse ABI per altre architetture di CPU.


  1. Perché l'ABI è così? Cioè, se i campi di una struttura / classe rientrano nei registri o anche in un singolo registro - perché non dovremmo essere in grado di passarlo all'interno di quel registro?

Si tratta di un dettaglio di implementazione, ma quando viene gestita un'eccezione, durante lo svolgimento dello stack, gli oggetti con durata di memorizzazione automatica in fase di distruzione devono essere indirizzabili rispetto al frame dello stack delle funzioni perché i registri sono stati bloccati da quel momento. Lo stack svolgendo codice ha bisogno degli indirizzi degli oggetti per invocare i loro distruttori, ma gli oggetti nei registri non hanno un indirizzo.

Pedanticamente, i distruttori operano su oggetti :

Un oggetto occupa una regione di stoccaggio nel suo periodo di costruzione ([class.cdtor]), per tutta la sua vita e nel suo periodo di distruzione.

e un oggetto non può esistere in C ++ se non viene allocato alcun archivio indirizzabile perché l'identità dell'oggetto è il suo indirizzo .

Quando è necessario un indirizzo di un oggetto con un banale costruttore di copie tenuto nei registri, il compilatore può semplicemente memorizzare l'oggetto in memoria e ottenere l'indirizzo. Se il costruttore di copie non è banale, d'altra parte, il compilatore non può semplicemente memorizzarlo in memoria, ma piuttosto deve chiamare il costruttore di copie che accetta un riferimento e quindi richiede l'indirizzo dell'oggetto nei registri. La convenzione di chiamata probabilmente non può dipendere dal fatto che il costruttore della copia sia stato inserito nella chiamata o meno.

Un altro modo di pensare a questo, è che per tipi banalmente copiabili il compilatore trasferisce il valore di un oggetto nei registri, da cui un oggetto può essere recuperato da semplici archivi di memoria, se necessario. Per esempio:

void f(long*);
void g(long a) { f(&a); }

su x86_64 con System V ABI viene compilato in:

g(long):                             // Argument a is in rdi.
        push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

Nel suo discorso stimolante Chandler Carruth menziona che potrebbe essere necessario (tra le altre cose) un cambiamento di ABI per attuare la mossa distruttiva che potrebbe migliorare le cose. IMO, la modifica dell'ABI potrebbe non interrompersi se le funzioni che utilizzano la nuova ABI optano esplicitamente per avere un nuovo collegamento diverso, ad esempio dichiararle in extern "C++20" {}blocco (possibilmente, in un nuovo spazio dei nomi in linea per la migrazione delle API esistenti). In modo che solo il codice compilato in base alle nuove dichiarazioni di funzione con il nuovo collegamento possa utilizzare la nuova ABI.

Si noti che l'ABI non si applica quando la funzione chiamata è stata incorporata. Oltre alla generazione di codici time-link, il compilatore può incorporare funzioni definite in altre unità di traduzione o utilizzare convenzioni di chiamata personalizzate.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

8

Con ABI comuni, distruttore non banale -> non può passare nei registri

(Un'illustrazione di un punto nella risposta di @ MaximEgorushkin usando l'esempio di @ harold in un commento; corretto secondo il commento di @ Yakk.)

Se compili:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

ottieni:

test(Foo):
        mov     eax, edi
        ret

cioè l' Foooggetto viene passato a testin register ( edi) e restituito anche in register ( eax).

Quando il distruttore non è banale (come std::unique_ptrnell'esempio degli OP), gli ABI comuni richiedono il posizionamento nello stack. Questo è vero anche se il distruttore non usa affatto l'indirizzo dell'oggetto.

Quindi, anche nel caso estremo di un distruttore che non fa nulla, se si compila:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

ottieni:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

con caricamento e memorizzazione inutili.


Non sono convinto da questo argomento. Il distruttore non banale non fa nulla per proibire la regola as-if. Se l'indirizzo non viene osservato, non vi è assolutamente alcun motivo per cui debba essercene uno. Quindi un compilatore conforme potrebbe tranquillamente metterlo in un registro, se così facendo non cambia il comportamento osservabile (e gli attuali compilatori lo faranno se i chiamanti sono noti ).
ComicSansMS,

1
Sfortunatamente, è il contrario (sono d'accordo che parte di questo è già oltre la ragione). Per essere precisi: non sono convinto che i motivi che hai fornito renderebbero necessariamente inaccettabile qualsiasi ABI che consentisse il passaggio della corrente std::unique_ptrin un registro.
ComicSansMS,

3
"banale distruttore [NECESSITÀ DELLA CITAZIONE]" chiaramente falso; se in realtà nessun codice dipende dall'indirizzo, allora come se significa che l'indirizzo non deve necessariamente esistere sulla macchina reale . L'indirizzo deve esistere nella macchina astratta , ma le cose nella macchina astratta che non hanno alcun impatto sulla macchina reale sono cose che potrebbero essere eliminate.
Yakk - Adam Nevraumont,

2
@einpoklum Non esiste nulla nello standard in base al quale esistono registri. La parola chiave register indica semplicemente "non puoi prendere l'indirizzo". C'è solo una macchina astratta per quanto riguarda lo standard. "come se" significa che qualsiasi implementazione della macchina reale deve comportarsi solo "come se" la macchina astratta si comporti, fino a comportamenti non definiti dallo standard. Ora, ci sono problemi molto impegnativi nell'avere un oggetto in un registro, di cui tutti hanno parlato ampiamente. Inoltre, le convenzioni di convocazione, che anche la norma non discute, hanno esigenze pratiche.
Yakk - Adam Nevraumont,

1
@einpoklum No, in quella macchina astratta, tutte le cose hanno indirizzi; ma gli indirizzi sono osservabili solo in determinate circostanze. La registerparola chiave aveva lo scopo di rendere banale per la macchina fisica archiviare qualcosa in un registro bloccando cose che praticamente rendono più difficile "non avere indirizzo" nella macchina fisica.
Yakk - Adam Nevraumont,

2

Questo è effettivamente un requisito ABI su alcune piattaforme? (quale?) O forse è solo un po 'di pessimizzazione in determinati scenari?

Se qualcosa è visibile nell'unità di calcolo, allora se è definito implicitamente o esplicitamente, diventa parte dell'ABI.

Perché l'ABI è così?

Il problema fondamentale è che i registri vengono salvati e ripristinati continuamente mentre si sposta verso l'alto e verso l'alto lo stack di chiamate. Quindi non è pratico avere un riferimento o un puntatore ad essi.

In-lining e le ottimizzazioni che ne derivano sono belle quando succede, ma un designer ABI non può fare affidamento sul fatto che accada. Devono progettare l'ABI ipotizzando il caso peggiore. Non penso che i programmatori sarebbero molto contenti di un compilatore in cui l'ABI è cambiato a seconda del livello di ottimizzazione.

Un tipo banalmente copiabile può essere passato nei registri perché l'operazione di copia logica può essere suddivisa in due parti. I parametri vengono copiati nei registri utilizzati per il passaggio dei parametri dal chiamante e quindi copiati nella variabile locale dalla chiamata. Il fatto che la variabile locale abbia o meno una posizione di memoria è quindi solo il problema della chiamata.

Un tipo in cui una copia o sposta costruttore deve essere utilizzato d'altra parte non può essere suddiviso in questo modo l'operazione di copia, quindi deve essere passato in memoria.

Il comitato per gli standard C ++ ha discusso questo punto negli ultimi anni o mai?

Non ho idea se gli enti normativi lo abbiano preso in considerazione.

La soluzione ovvia per me sarebbe quella di aggiungere le mosse distruttive appropriate (piuttosto che l'attuale casa a metà strada di uno "stato valido ma altrimenti non specificato") alla lingua, quindi introdurre un modo per contrassegnare un tipo che consenta "mosse distruttive banali "anche se non consente copie banali.

ma una soluzione del genere richiederebbe la rottura dell'ABI del codice esistente da implementare per i tipi esistenti, il che può portare un bel po 'di resistenza (anche se le interruzioni dell'ABI come risultato delle nuove versioni standard C ++ non sono senza precedenti, ad esempio le modifiche std :: string in C ++ 11 ha provocato un'interruzione ABI.


Puoi approfondire come le mosse distruttive appropriate consentirebbero il passaggio di un unique_ptr in un registro? Sarebbe perché consentirebbe di eliminare il requisito di archiviazione indirizzabile?
einpoklum,

Mosse distruttive appropriate consentirebbero di introdurre un concetto di mosse distruttive banali. Ciò consentirebbe a tale mossa banale di essere suddivisa dall'ABI nello stesso modo in cui oggi possono essere copie banali.
lavaggio:

Anche se si vorrebbe anche aggiungere una regola secondo la quale un compilatore potrebbe implementare un passaggio di parametri come mossa o copia regolare seguita da una "mossa distruttiva banale" per garantire che sia sempre possibile passare nei registri, indipendentemente da dove provenga il parametro.
lavaggio:

Perché la dimensione del registro può contenere un puntatore, ma una struttura unique_ptr? Qual è la dimensione di (unique_ptr <T>)?
Mel Viso Martinez,

@MelVisoMartinez Potresti essere confuso unique_ptre shared_ptrsemantico: shared_ptr<T>ti consente di fornire al ctor 1) un ptr x all'oggetto derivato U da eliminare con il tipo statico U con l'espressione delete x;(quindi non hai bisogno di un dtor virtuale qui) 2) o anche una funzione di pulizia personalizzata. Ciò significa che lo stato di runtime viene utilizzato all'interno del shared_ptrblocco di controllo per codificare tali informazioni. OTOH unique_ptrnon ha tale funzionalità e non codifica il comportamento di eliminazione nello stato; l'unico modo per personalizzare la pulizia è creare un'altra istanza del modello (un altro tipo di classe).
curioso

-1

Innanzitutto dobbiamo tornare a cosa significa passare per valore e per riferimento.

Per linguaggi come Java e SML, il passaggio per valore è semplice (e non esiste un passaggio per riferimento), così come lo è la copia di un valore di variabile, poiché tutte le variabili sono solo scalari e hanno una copia semantica incorporata: sono entrambi ciò che conta come aritmetica digitare C ++ o "riferimenti" (puntatori con nome e sintassi diversi).

In C abbiamo tipi scalari e definiti dall'utente:

  • Gli scalari hanno un valore numerico o astratto (i puntatori non sono numeri, hanno un valore astratto) che viene copiato.
  • Ai tipi aggregati vengono copiati tutti i membri eventualmente inizializzati:
    • per tipi di prodotto (matrici e strutture): ricorsivamente, tutti i membri delle strutture e gli elementi delle matrici vengono copiati (la sintassi della funzione C non consente di passare direttamente le matrici per valore, ma solo i membri delle matrici di una struttura, ma questo è un dettaglio ).
    • per i tipi di somma (sindacati): viene conservato il valore del "membro attivo"; ovviamente, copia membro per membro non è in ordine in quanto non tutti i membri possono essere inizializzati.

In C ++ i tipi definiti dall'utente possono avere una copia semantica definita dall'utente, che consente una programmazione veramente "orientata agli oggetti" con oggetti con proprietà delle loro risorse e operazioni di "copia profonda". In tal caso, un'operazione di copia è in realtà una chiamata a una funzione che può quasi eseguire operazioni arbitrarie.

Per le strutture C compilate come C ++, la "copia" è ancora definita come chiamata all'operazione di copia definita dall'utente (costruttore o operatore di assegnazione), che sono generati implicitamente dal compilatore. Significa che la semantica di un programma di sottoinsieme comune C / C ++ è diversa in C e C ++: in C viene copiato un intero tipo di aggregato, in C ++ viene chiamata una funzione di copia generata implicitamente per copiare ciascun membro; il risultato finale è che in entrambi i casi ogni membro viene copiato.

(C'è un'eccezione, penso, quando viene copiata una struttura all'interno di un'unione.)

Quindi, per un tipo di classe, l'unico modo (copie esterne al sindacato) per creare una nuova istanza è tramite un costruttore (anche per quelli con banali costruttori generati dal compilatore).

Non è possibile prendere l'indirizzo di un valore tramite operatore unario, &ma ciò non significa che non vi sia alcun oggetto valore; e un oggetto, per definizione, ha un indirizzo ; e quell'indirizzo è persino rappresentato da un costrutto di sintassi: un oggetto di tipo classe può essere creato solo da un costruttore e ha un thispuntatore; ma per tipi banali, non esiste un costruttore scritto dall'utente, quindi non c'è posto dove mettere thisfino a dopo che la copia è stata costruita e denominata.

Per il tipo scalare, il valore di un oggetto è il valore dell'oggetto, il valore matematico puro memorizzato nell'oggetto.

Per un tipo di classe, l'unica nozione di un valore dell'oggetto è un'altra copia dell'oggetto, che può essere fatta solo da un costruttore di copie, una funzione reale (anche se per tipi banali tale funzione è così particolarmente banale, a volte possono essere creato senza chiamare il costruttore). Ciò significa che il valore dell'oggetto è il risultato del cambiamento dello stato del programma globale da parte di un'esecuzione . Non accede matematicamente.

Quindi passare per valore in realtà non è una cosa: è passare per copia la chiamata del costruttore , che è meno carina. Il costruttore di copie dovrebbe eseguire un'operazione di "copia" ragionevole in base alla semantica corretta del tipo di oggetto, rispettando le sue invarianti interne (che sono proprietà utente astratte, non proprietà intrinseche C ++).

Passare per valore di un oggetto classe significa:

  • crea un'altra istanza
  • quindi attiva la funzione chiamata su quell'istanza.

Si noti che il problema non ha nulla a che fare con il fatto che la copia stessa sia un oggetto con un indirizzo: tutti i parametri delle funzioni sono oggetti e hanno un indirizzo (a livello semantico della lingua).

Il problema è se:

  • la copia è un nuovo oggetto inizializzato con il puro valore matematico (vero valore puro) dell'oggetto originale, come con gli scalari;
  • oppure la copia è il valore dell'oggetto originale , come con le classi.

Nel caso di un tipo di classe banale, è ancora possibile definire il membro della copia del membro dell'originale, in modo da poter definire il valore puro dell'originale a causa della banalità delle operazioni di copia (costruttore della copia e assegnazione). Non così con le funzioni utente speciali arbitrarie: un valore dell'originale deve essere una copia costruita.

Gli oggetti di classe devono essere costruiti dal chiamante; un costruttore ha formalmente un thispuntatore ma qui il formalismo non è rilevante: tutti gli oggetti hanno formalmente un indirizzo ma solo quelli che effettivamente usano il loro indirizzo in modi non puramente locali (a differenza *&i = 1;dell'uso puramente locale dell'indirizzo) devono avere un ben definito indirizzo.

Un oggetto deve assolutamente essere passato per indirizzo se deve avere un indirizzo in entrambe queste due funzioni compilate separatamente:

void callee(int &i) {
  something(&i);
}

void caller() {
  int i;
  callee(i);
  something(&i);
}

Qui anche se something(address)è una funzione o macro pura o qualsiasi altra cosa (come printf("%p",arg)) che non può memorizzare l'indirizzo o comunicare con un'altra entità, abbiamo il requisito di passare per indirizzo perché l'indirizzo deve essere ben definito per un oggetto unico intche ha un unico identità.

Non sappiamo se una funzione esterna sarà "pura" in termini di indirizzi passati ad essa.

Qui il potenziale per un uso reale dell'indirizzo in un costruttore o in un distruttore non banale dal lato del chiamante è probabilmente la ragione per prendere il percorso sicuro e semplicistico e dare all'oggetto un'identità nel chiamante e passare il suo indirizzo, mentre fa certo che qualsiasi uso non banale del suo indirizzo nel costruttore, dopo la costruzione e nel distruttore sia coerente : thisdeve apparire come lo stesso sull'esistenza dell'oggetto.

Un costruttore o distruttore non banale come qualsiasi altra funzione può usare il thispuntatore in un modo che richiede coerenza sul suo valore anche se alcuni oggetti con oggetti non banali potrebbero non:

struct file_handler { // don't use that class!
    file_handler () { this->fileno = -1; }
    file_handler (int f) { this->fileno = f; }
    file_handler (const file_handler& rhs) {
        if (this->fileno != -1)
            this->fileno = dup(rhs.fileno);
        else
            this->fileno = -1;
    }
    ~file_handler () {
        if (this->fileno != -1)
            close(this->fileno); 
    }
    file_handler &operator= (const file_handler& rhs);
};

Si noti che in quel caso, nonostante l'uso esplicito di un puntatore (sintassi esplicita this->), l'identità dell'oggetto è irrilevante: il compilatore potrebbe usare l'oggetto per copiare bit a bit per spostarlo e fare "copia elisione". Questo si basa sul livello di "purezza" dell'uso di thisfunzioni membro speciali (l'indirizzo non scappa).

Ma la purezza non è un attributo disponibile a livello di dichiarazione standard (esistono estensioni del compilatore che aggiungono una descrizione di purezza nella dichiarazione di funzione non inline), quindi non è possibile definire un ABI basato sulla purezza di codice che potrebbe non essere disponibile (il codice può o potrebbe non essere in linea e disponibile per l'analisi).

La purezza è misurata come "certamente pura" o "impura o sconosciuta". Il terreno comune, o limite superiore della semantica (in realtà massimo) o LCM (minimo comune multiplo) è "sconosciuto". Quindi l'ABI si deposita su sconosciuto.

Sommario:

  • Alcuni costrutti richiedono che il compilatore definisca l'identità dell'oggetto.
  • L'ABI è definito in termini di classi di programmi e non di casi specifici che potrebbero essere ottimizzati.

Possibili lavori futuri:

L'annotazione di purezza è abbastanza utile per essere generalizzata e standardizzata?


1
Il tuo primo esempio appare fuorviante. Penso che tu stia solo sollevando un punto in generale, ma all'inizio pensavo che stavi facendo un'analogia con il codice nella domanda. Ma void foo(unique_ptr<int> ptr)prende l'oggetto della classe per valore . Quell'oggetto ha un membro puntatore, ma stiamo parlando dell'oggetto di classe stesso che viene passato per riferimento. (Perché non è banalmente copiabile, quindi il suo costruttore / distruttore ha bisogno di un coerente this.) Questo è il vero argomento e non collegato al primo esempio di passaggio esplicito per riferimento ; in tal caso il puntatore viene passato in un registro.
Peter Cordes,

@PeterCordes " stavi facendo un'analogia con il codice nella domanda. " Ho fatto esattamente questo. " l'oggetto di classe in base al valore " Sì, probabilmente dovrei spiegare che in generale non esiste un "valore" di un oggetto di classe, quindi per valore per un tipo non matematico non è "per valore". " Quell'oggetto ha un membro puntatore " La natura simile a ptr di un "ptr intelligente" è irrilevante; e così è il membro ptr di "smart ptr". Un ptr è solo uno scalare come un int: ho scritto un esempio di "fileno intelligente" che illustra che la "proprietà" non ha nulla a che fare con "il trasporto di un ptr".
curioso

1
Il valore di un oggetto classe è la sua rappresentazione dell'oggetto. Per unique_ptr<T*>, questa è la stessa dimensione e layout di T*e si inserisce in un registro. Gli oggetti di classe copiabili in modo banale possono essere passati per valore nei registri nel sistema V x86-64, come la maggior parte delle convenzioni di chiamata. Ciò fa un copia del unique_ptroggetto, a differenza nel vostro intesempio in cui il chiamato del &i è l'indirizzo del chiamante è iperché è stato superato con riferimento al livello di C ++ , non solo come un dettaglio di implementazione asm.
Peter Cordes,

1
Err, correzione al mio ultimo commento. Non è solo fare una copia del unique_ptrbene; lo sta usando, std::movequindi è sicuro copiarlo perché ciò non comporterà 2 copie dello stesso unique_ptr. Ma per un tipo banalmente copiabile, sì, copia l'intero oggetto aggregato. Se si tratta di un singolo membro, le buone convenzioni di chiamata lo trattano come uno scalare di quel tipo.
Peter Cordes,

1
Sembra migliore. Note: Per le strutture C compilate come C ++ - Questo non è un modo utile per introdurre la differenza tra C ++. In C ++ struct{}è una struttura C ++. Forse dovresti dire "semplici strutture" o "diversamente dalla C". Perché sì, c'è una differenza. Se lo usi atomic_intcome membro struct, C lo copierà in modo non atomico, errore C ++ sul costruttore di copie cancellate. Dimentico ciò che fa C ++ sulle strutture con i volatilemembri. C ti consentirà struct tmp = volatile_struct;di copiare tutto (utile per un SeqLock); C ++ no.
Peter Cordes,
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.