Sono finiti i giorni di passaggio const std :: string & come parametro?


604

Ho sentito un recente discorso da Herb Sutter che ha suggerito che le ragioni per passare std::vectore std::stringda const &sono in gran parte scomparsi. Ha suggerito che ora è preferibile scrivere una funzione come la seguente:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

Capisco che return_valsarà un valore nel punto in cui la funzione ritorna e può quindi essere restituita usando la semantica di movimento, che è molto economica. Tuttavia, invalè ancora molto più grande della dimensione di un riferimento (che di solito viene implementato come puntatore). Questo perché a std::stringha vari componenti tra cui un puntatore nell'heap e un membro char[]per l'ottimizzazione della stringa breve. Quindi mi sembra che passare per riferimento sia ancora una buona idea.

Qualcuno può spiegare perché Herb avrebbe potuto dirlo?


89
Penso che la migliore risposta alla domanda sia probabilmente quella di leggere l' articolo di Dave Abrahams su C ++ Next . Aggiungo che non vedo nulla di ciò che si qualifica come off-topic o non costruttivo. È una domanda chiara, sulla programmazione, a cui ci sono risposte concrete.
Jerry Coffin,

Affascinante, quindi se devi comunque fare una copia, il valore per passaggio è probabilmente più veloce del riferimento per passaggio.
Benj,

3
@Sz. Sono sensibile alle domande erroneamente classificate come duplicate e chiuse. Non ricordo i dettagli di questo caso e non li ho riesaminati. Invece eliminerò semplicemente il mio commento sull'ipotesi che ho fatto un errore. Grazie per averlo portato alla mia attenzione.
Howard Hinnant,

2
@HowardHinnant, grazie mille, è sempre un momento prezioso in cui ci si imbatte in questo livello di attenzione e sensibilità, è così rinfrescante! (Eliminerò il mio, ovviamente.)
Sz.

Risposte:


393

Il motivo per cui Herb ha detto quello che ha detto è a causa di casi come questo.

Diciamo che ho una funzione Ache chiama funzione B, che chiama funzione C. E Apassa una stringa attraverso Be dentro C. Anon sa né si preoccupa C; tutto ciò che Asa è B. Cioè, Cè un dettaglio di implementazione di B.

Diciamo che A è definito come segue:

void A()
{
  B("value");
}

Se B e C prendono la stringa const&, allora sembra qualcosa del genere:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

Tutto bene. Stai solo passando puntatori, nessuna copia, nessun movimento, tutti sono felici. Caccetta un const&perché non memorizza la stringa. Lo usa semplicemente.

Ora, voglio fare una semplice modifica: è Cnecessario memorizzare la stringa da qualche parte.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

Ciao, copia costruttore e potenziale allocazione di memoria (ignora Short String Optimization (SSO) ). La semantica di spostamento di C ++ 11 dovrebbe consentire di rimuovere inutili costrutti di copia, giusto? E Apassa un temporaneo; non c'è motivo per cui Cdovrebbe essere necessario copiare i dati. Dovrebbe semplicemente fuggire da ciò che gli è stato dato.

Tranne che non può. Perché ci vuole un const&.

Se cambio Cper prendere il suo parametro in base al valore, ciò fa semplicemente Bfare la copia in quel parametro; Non guadagno niente.

Quindi, se avessi appena passato il strvalore attraverso tutte le funzioni, basandomi sul std::moveriordino dei dati, non avremmo questo problema. Se qualcuno vuole tenerlo, può farlo. Se non lo fanno, vabbè.

È più costoso? Sì; lo spostamento in un valore è più costoso rispetto all'utilizzo dei riferimenti. È meno costoso della copia? Non per stringhe piccole con SSO. Vale la pena farlo?

Dipende dal tuo caso d'uso. Quanto odi le allocazioni di memoria?


2
Quando dici che spostarsi in un valore è più costoso dell'uso dei riferimenti, è ancora più costoso di un importo costante (indipendentemente dalla lunghezza della stringa che viene spostata), giusto?
Neil G

3
@NeilG: Capisci cosa significa "dipendente dall'implementazione"? Quello che stai dicendo è sbagliato, perché dipende se e come viene implementato SSO.
ildjarn,

17
@ildjarn: nell'analisi dell'ordine, se il caso peggiore di qualcosa è legato da una costante, allora è ancora tempo costante. Non c'è una piccola stringa più lunga? Quella stringa non richiede un certo lasso di tempo costante per essere copiata? Tutte le stringhe più piccole non richiedono meno tempo per essere copiate? Quindi, la copia di stringhe per stringhe di piccole dimensioni è "tempo costante" nell'analisi degli ordini, nonostante le stringhe di piccole dimensioni richiedano quantità variabili di tempo per essere copiate. L'analisi dell'ordine riguarda il comportamento asintotico .
Neil G

8
@NeilG: Certo, ma la tua domanda originale era " che è ancora più costosa di una quantità costante (indipendentemente dalla lunghezza della stringa che viene spostata) giusto? " Il punto che sto cercando di fare è che potrebbe essere più costoso di diverso importi costanti a seconda della lunghezza della stringa, che viene riassunta come "no".
ildjarn,

13
Perché la stringa dovrebbe essere movedda B a C nel caso del valore? Se B è B(std::string b)e C è C(std::string c)allora o dobbiamo chiamare C(std::move(b))in B o dobbiamo brimanere invariati (quindi 'impassibili da') fino all'uscita B. (Forse un compilatore di ottimizzazione sposta la stringa sotto la regola as-if se bnon viene utilizzata dopo la chiamata ma non credo che ci sia una forte garanzia.) Lo stesso vale per la copia di strto m_str. Anche se un parametro di funzione è stato inizializzato con un valore è un valore all'interno della funzione ed std::moveè necessario spostarsi da quel valore.
Pixelchemist,

163

Sono finiti i giorni di passaggio const std :: string & come parametro?

No . Molte persone prendono questo consiglio (tra cui Dave Abrahams) al di là del dominio si applica a, e semplificare per applicare a tutti std::string i parametri - sempre passando std::stringper valore non è una "best practice" per qualsiasi e tutti i parametri e le applicazioni arbitrarie in quanto le ottimizzazioni questi le discussioni / articoli si concentrano solo su un numero limitato di casi .

Se si restituisce un valore, si modifica il parametro o si assume il valore, il passaggio in base al valore potrebbe salvare copie costose e offrire comodità sintattica.

Come sempre, il passaggio per riferimento const consente di risparmiare molta copia quando non è necessaria una copia .

Ora all'esempio specifico:

Tuttavia, inval è ancora molto più grande della dimensione di un riferimento (che di solito viene implementato come puntatore). Questo perché uno std :: string ha vari componenti tra cui un puntatore nell'heap e un membro char [] per l'ottimizzazione della stringa breve. Quindi mi sembra che passare per riferimento sia ancora una buona idea. Qualcuno può spiegare perché Herb avrebbe potuto dirlo?

Se la dimensione dello stack è un problema (e supponendo che questo non sia inline / ottimizzato), return_val+ inval> return_val- IOW, l'utilizzo dello stack di picco può essere ridotto passando qui per valore (nota: semplificazione eccessiva degli ABI). Nel frattempo, passare per riferimento const può disabilitare le ottimizzazioni. Il motivo principale qui non è quello di evitare la crescita dello stack, ma di garantire che l'ottimizzazione possa essere eseguita laddove applicabile .

I giorni di passaggio per riferimento const non sono finiti - le regole sono solo più complicate di quanto lo fossero una volta. Se le prestazioni sono importanti, sarai saggio a considerare come passi questi tipi, in base ai dettagli che usi nelle tue implementazioni.


3
In caso di utilizzo dello stack, gli ABI tipici passano un singolo riferimento in un registro senza utilizzo dello stack.
ahcox,

63

Questo dipende fortemente dall'implementazione del compilatore.

Tuttavia, dipende anche da cosa usi.

Consideriamo le prossime funzioni:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

Queste funzioni sono implementate in un'unità di compilazione separata per evitare l'allineamento. Quindi:
1. Se passi letteralmente queste due funzioni, non vedrai molta differenza nelle prestazioni. In entrambi i casi, è necessario creare un oggetto stringa
2. Se si passa un altro oggetto std :: string, foo2sarà più efficace foo1, poiché foo1verrà eseguita una copia approfondita.

Sul mio PC, usando g ++ 4.6.1, ho ottenuto questi risultati:

  • variabile per riferimento: 1000000000 iterazioni -> tempo trascorso: 2.25912 sec
  • variabile per valore: 1000000000 iterazioni -> tempo trascorso: 27.2259 sec
  • letterale per riferimento: 100000000 iterazioni -> tempo trascorso: 9.10319 sec
  • letterale per valore: 100000000 iterazioni -> tempo trascorso: 8,62659 sec

4
Ciò che è più rilevante è ciò che sta accadendo all'interno della funzione: sarebbe, se chiamato con un riferimento, fare una copia internamente che può essere omessa quando si passa per valore ?
leftaroundabout

1
@leftaroundabout Sì, certo. La mia ipotesi che entrambe le funzioni stiano facendo esattamente la stessa cosa.
BЈовић,

5
Non è questo il punto. Se passare per valore o per riferimento è meglio dipende da cosa stai facendo all'interno della funzione. Nel tuo esempio, in realtà non stai utilizzando gran parte dell'oggetto stringa, quindi il riferimento è ovviamente migliore. Ma se il compito della funzione fosse posizionare la stringa in una struttura o eseguire, diciamo, un algoritmo ricorsivo che coinvolge più suddivisioni della stringa, il passaggio per valore potrebbe effettivamente salvare alcune copie, rispetto al passaggio per riferimento. Nicol Bolas lo spiega abbastanza bene.
lasciato circa il

3
Per me "dipende da cosa fai all'interno della funzione" è un cattivo design, poiché stai basando la firma della funzione sugli interni dell'implementazione.
Hans Olsson,

1
Potrebbe essere un refuso, ma gli ultimi due tempi letterali hanno 10 volte meno loop.
TankorSmash,

54

Risposta breve: NO! Risposta lunga:

  • Se non modificherai la stringa (tratta è di sola lettura), passala come const ref&.
    ( const ref&ovviamente deve rimanere all'interno dell'ambito mentre viene eseguita la funzione che lo utilizza)
  • Se si prevede di modificarlo o si sa che uscirà dall'ambito (thread) , passarlo come a value, non copiare l' const ref&interno del corpo della funzione.

C'era un post su cpp-next.com chiamato "Vuoi velocità, passa per valore!" . Il TL; DR:

Linea guida : non copiare gli argomenti della funzione. Invece, passali per valore e lascia che il compilatore esegua la copia.

TRADUZIONE di ^

Non copiare gli argomenti della funzione --- significa: se si prevede di modificare il valore dell'argomento copiandolo in una variabile interna, utilizzare invece un argomento valore .

Quindi, non farlo :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

fai questo :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

Quando è necessario modificare il valore dell'argomento nel corpo della funzione.

Devi solo essere consapevole di come prevedi di utilizzare l'argomento nel corpo della funzione. Sola lettura o NON ... e se si attacca nell'ambito.


2
In alcuni casi si consiglia di passare per riferimento, ma si fa riferimento a una linea guida che consiglia di passare sempre per valore.
Keith Thompson,

3
@KeithThompson Non copiare gli argomenti della funzione. I mezzi non copiano il const ref&in una variabile interna per modificarlo. Se è necessario modificarlo ... impostare un parametro su un valore. È piuttosto chiaro per il mio io che non parla inglese.
CodeAngry,

5
@KeithThompson Il preventivo delle Linee guida (non copiare gli argomenti della funzione. Passali invece per valore e lascia che il compilatore esegua la copia) è COPIATO da quella pagina. Se questo non è abbastanza chiaro, non posso fare a meno. Non mi fido completamente dei compilatori per fare le scelte migliori. Preferirei essere molto chiaro sulle mie intenzioni nel modo in cui definisco gli argomenti di funzione. # 1 Se è di sola lettura, è a const ref&. # 2 Se ho bisogno di scriverlo o so che esce dal campo di applicazione ... Uso un valore. # 3 Se devo modificare il valore originale, passo ref&. # 4 Uso pointers *un argomento facoltativo, quindi posso nullptrfarlo.
CodeAngry,

11
Non mi sto schierando dalla parte della questione se passare per valore o per riferimento. Il mio punto è che in alcuni casi sostenete il passaggio per riferimento, ma poi citate (apparentemente per supportare la vostra posizione) una linea guida che raccomanda di passare sempre per valore. Se non sei d'accordo con le linee guida, potresti volerlo dire e spiegare perché. (I collegamenti a cpp-next.com non funzionano per me.)
Keith Thompson il

4
@KeithThompson: stai parafrasando male la linea guida. Non è "sempre" passare per valore. Riassumendo, era "Se avessi creato una copia locale, usa pass by value per fare in modo che il compilatore esegua quella copia per te." Non sta dicendo di usare il pass-by-value quando non hai intenzione di fare una copia.
Ben Voigt,

43

A meno che tu non abbia effettivamente bisogno di una copia, è comunque ragionevole prenderlo const &. Per esempio:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

Se lo cambi per prendere la stringa in base al valore, finirai per spostare o copiare il parametro e non è necessario. La copia / spostamento non solo è probabilmente più costosa, ma introduce anche un nuovo potenziale fallimento; la copia / spostamento potrebbe generare un'eccezione (ad esempio, l'assegnazione durante la copia potrebbe non riuscire) mentre non è possibile prendere un riferimento a un valore esistente.

Se non bisogno di una copia poi passaggio e la restituzione per valore è di solito (sempre?) L'opzione migliore. In effetti, in genere non me ne preoccuperei in C ++ 03 a meno che non trovi che copie extra causano effettivamente un problema di prestazioni. Copia elision sembra abbastanza affidabile su compilatori moderni. Penso che lo scetticismo delle persone e l'insistenza sul fatto che si debba controllare la tabella del supporto del compilatore per RVO al giorno d'oggi sia per lo più obsoleta.


In breve, C ++ 11 in realtà non cambia nulla in questo senso, tranne che per le persone che non si fidano delle copie elise.


2
I costruttori di spostamento sono generalmente implementati con noexcept, ma ovviamente i costruttori di copie non lo sono.
lasciato circa il

25

Quasi.

In C ++ 17, abbiamo basic_string_view<?>, il che ci porta sostanzialmente a un caso d'uso ristretto per i std::string const&parametri.

L'esistenza della semantica di spostamento ha eliminato un caso d'uso per std::string const&- se si sta pianificando di memorizzare il parametro, prendere un std::stringvalore per è più ottimale, poiché è possibile moveuscire dal parametro.

Se qualcuno ha chiamato la tua funzione con una C non elaborata, "string"ciò significa che std::stringviene allocato solo un buffer, a differenza di due nel std::string const&caso.

Tuttavia, se non si intende fare una copia, prendere in considerazione std::string const&è ancora utile in C ++ 14.

Con std::string_view, fino a quando non si passa detta stringa a un'API che prevede '\0'buffer di carattere con terminazione in stile C, è possibile ottenere in modo più efficiente std::stringcome funzionalità senza rischiare alcuna allocazione. Una stringa C grezza può anche essere trasformata in una std::string_viewsenza allocazione o copia dei caratteri.

A quel punto, l'uso std::string const&è quando non si copiano i dati all'ingrosso e li si passerà a un'API di tipo C che prevede un buffer con terminazione null e sono necessarie le funzioni di stringa di livello superiore che std::stringforniscono. In pratica, si tratta di una rara serie di requisiti.


2
Apprezzo questa risposta, ma voglio sottolineare che soffre (come fanno molte risposte di qualità) di un po 'di pregiudizio specifico del dominio. Vale a dire: "In pratica, questo è un raro insieme di requisiti" ... nella mia esperienza di sviluppo, questi vincoli - che sembrano anormalmente stretti all'autore - sono soddisfatti, letteralmente, sempre. Vale la pena sottolineare questo.
fish2000,

1
@ fish2000 Per essere chiari, per std::stringdominare non hai solo bisogno di alcuni di questi requisiti, ma tutti . Qualcuno o anche due di questi è, lo ammetto, comune. Forse di solito hai bisogno di tutti e 3 (tipo, stai facendo un po 'di analisi di un argomento di stringa per scegliere a quale API C lo passerai all'ingrosso?)
Yakk - Adam Nevraumont,

@ Yakk-AdamNevraumont È una cosa YMMV - ma è un caso d'uso frequente se (diciamo) stai programmando contro POSIX o altre API in cui la semantica della stringa C è, come, il minimo comune denominatore. Dovrei dire davvero che adoro std::string_view- come dici tu, "Una stringa C grezza può persino essere trasformata in una stringa std :: string_view senza allocazione o copia dei caratteri" che è qualcosa che vale la pena ricordare a coloro che usano C ++ nel contesto di tale utilizzo delle API, infatti.
fish2000,

1
@ fish2000 "'Una stringa C grezza può anche essere trasformata in una std :: string_view senza alcuna allocazione o copia dei caratteri' che è qualcosa che vale la pena ricordare". Anzi, ma tralascia la parte migliore: nel caso in cui la stringa non elaborata sia letterale, non richiede nemmeno un runtime strlen () !
Don Hatch,

17

std::stringnon è Plain Old Data (POD) e le sue dimensioni non sono la cosa più rilevante di sempre. Ad esempio, se si passa una stringa che supera la lunghezza di SSO e allocata sull'heap, mi aspetto che il costruttore della copia non copi l'archiviazione SSO.

Il motivo per cui questo è raccomandato è perché invalè costruito dall'espressione dell'argomento, e quindi viene sempre spostato o copiato come appropriato - non vi è alcuna perdita di prestazioni, supponendo che sia necessario il possesso dell'argomento. In caso contrario, un constriferimento potrebbe comunque essere il modo migliore di procedere.


2
Punto interessante sul fatto che il costruttore di copie sia abbastanza intelligente da non preoccuparsi dell'SSO se non lo utilizza. Probabilmente corretto, dovrò verificare se è vero ;-)
Benj

3
@Benj: Vecchio commento lo so, ma se SSO è abbastanza piccolo copiarlo incondizionatamente è più veloce che fare un ramo condizionale. Ad esempio, 64 byte è una riga della cache e può essere copiata in un tempo davvero banale. Probabilmente 8 cicli o meno su x86_64.
Zan Lynx,

Anche se il SSO non viene copiato dal costruttore della copia, an std::string<>è 32 byte allocati dallo stack, 16 dei quali devono essere inizializzati. Confronta questo con solo 8 byte allocati e inizializzati per un riferimento: è il doppio della quantità di lavoro della CPU e occupa quattro volte più spazio nella cache che non sarà disponibile per altri dati.
cmaster - reintegrare monica il

Oh, e ho dimenticato di parlare di passare argomenti di funzione nei registri; che porterebbe a zero l'utilizzo dello stack del riferimento per l'ultima chiamata ...
cmaster - ripristina monica

16

Ho copiato / incollato la risposta da questa domanda qui e ho cambiato i nomi e l'ortografia per adattarli a questa domanda.

Ecco il codice per misurare ciò che viene chiesto:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

Per me questo produce:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

La tabella seguente riassume i miei risultati (usando clang -std = c ++ 11). Il primo numero è il numero di costruzioni di copia e il secondo numero è il numero di costruzioni di movimento:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

La soluzione pass-by-value richiede solo un sovraccarico ma costa una costruzione di mossa extra quando si superano valori e valori x. Ciò può essere o non essere accettabile per una determinata situazione. Entrambe le soluzioni presentano vantaggi e svantaggi.


1
std :: string è una classe di libreria standard. È già sia mobile che copiabile. Non vedo quanto sia rilevante. L'OP chiede di più sull'esecuzione della mossa rispetto ai riferimenti , non sull'esecuzione della mossa rispetto alla copia.
Nicol Bolas,

3
Questa risposta conta il numero di mosse e copia che subirà una stringa std :: string con il progetto di passaggio per valore descritto da Herb e Dave, rispetto al passaggio per riferimento con una coppia di funzioni sovraccaricate. Uso il codice OP nella demo, tranne che per la sostituzione in una stringa fittizia per gridare quando viene copiato / spostato.
Howard Hinnant,

Probabilmente dovresti ottimizzare il codice prima di eseguire i test ...
The Paramagnetic Croissant

3
@TheParamagneticCroissant: hai ottenuto risultati diversi? In tal caso, utilizzando quale compilatore con quali argomenti della riga di comando?
Howard Hinnant,

14

Herb Sutter è ancora registrato, insieme a Bjarne Stroustroup, nel raccomandare const std::string&come tipo di parametro; vedi https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .

C'è una trappola non menzionata in nessuna delle altre risposte qui: se si passa una stringa letterale a un const std::string&parametro, passerà un riferimento a una stringa temporanea, creata al volo per contenere i caratteri del letterale. Se si salva quindi quel riferimento, non sarà valido una volta che la stringa temporanea è stata assegnata. Per sicurezza, è necessario salvare una copia , non il riferimento. Il problema deriva dal fatto che i letterali stringa sono const char[N]tipi che richiedono promozione std::string.

Il codice seguente illustra la trappola e la soluzione alternativa, insieme a un'opzione di efficienza minore - sovraccarico con un const char*metodo, come descritto in Esiste un modo per passare una stringa letterale come riferimento in C ++ .

(Nota: Sutter & Stroustroup avvisano che se conservate una copia della stringa, fornite anche una funzione sovraccaricata con un parametro && e std :: move ().)

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

PRODUZIONE:

const char * constructor
const std::string& constructor

Second string
Second string

Questo è un problema diverso e WidgetBadRef non ha bisogno di avere una const e un parametro per andare storto. La domanda è se WidgetSafeCopy ha appena preso un parametro stringa sarebbe più lento? (Penso che la copia temporanea per membro sia sicuramente più facile da individuare)
Superfly Jon

Nota che T&&non è sempre un riferimento universale; infatti, std::string&&sarà sempre un riferimento al valore e mai un riferimento universale, poiché non viene eseguita alcuna detrazione del tipo . Pertanto, i consigli di Stroustroup & Sutter non sono in conflitto con quelli di Meyers.
Justin Time - Ripristina Monica il

@JustinTime: grazie; Ho rimosso la frase finale errata sostenendo, in effetti, che std :: string && sarebbe un riferimento universale.
circlepi314,

@ circlepi314 Prego. È un miscuglio facile da fare, a volte può confondere se un dato T&&è un riferimento universale dedotto o un riferimento di valore non dedotto; probabilmente sarebbe stato più chiaro se avessero introdotto un simbolo diverso per riferimenti universali (come &&&, come una combinazione di &e &&), ma probabilmente sarebbe solo sciocco.
Justin Time - Ripristina Monica il

8

IMO che utilizza il riferimento C ++ per std::stringè un'ottimizzazione locale rapida e breve, mentre l'utilizzo del passaggio per valore potrebbe essere (o meno) una migliore ottimizzazione globale.

Quindi la risposta è: dipende dalle circostanze:

  1. Se scrivi tutto il codice dall'esterno alle funzioni interne, sai cosa fa il codice, puoi usare il riferimento const std::string &.
  2. Se si scrive il codice della libreria o si utilizza fortemente il codice della libreria in cui vengono passate le stringhe, è probabile che si guadagni di più in senso globale fidandosi del std::stringcomportamento del costruttore di copie.

6

Vedi "Herb Sutter" Back to the Basics! Essentials of Modern C ++ Style " . Tra gli altri argomenti, rivede il parametro che passa consigli che sono stati dati in passato, e nuove idee che arrivano con C ++ 11 e guarda specificamente al idea di passare stringhe per valore.

diapositiva 24

I benchmark mostrano che passare std::strings per valore, nei casi in cui la funzione lo copi comunque, può essere significativamente più lento!

Questo perché lo stai forzando a fare sempre una copia completa (e quindi a spostarti in posizione), mentre la const&versione aggiornerà la vecchia stringa che potrebbe riutilizzare il buffer già allocato.

Vedi la sua diapositiva 27: Per le funzioni "set", l'opzione 1 è la stessa di sempre. L'opzione 2 aggiunge un sovraccarico per il riferimento al valore, ma ciò provoca un'esplosione combinatoria se ci sono più parametri.

È solo per i parametri "sink" in cui è necessario creare una stringa (senza modificarne il valore esistente) che il trucco pass-by-value è valido. Ossia, costruttori in cui il parametro inizializza direttamente il membro del tipo corrispondente.

Se vuoi vedere quanto in profondità puoi preoccuparti di questo, guarda la presentazione di Nicolai Josuttis e buona fortuna con quello ( "Perfetto - Fatto!" N volte dopo aver trovato un difetto con la versione precedente. Ci sei mai stato?)


Questo è anche sintetizzato come ⧺F.15 nelle Linee guida standard.


3

Come sottolinea @ JDługosz nei commenti, Herb fornisce altri consigli in un altro (più tardi?) Discorso, vedi più o meno da qui: https://youtu.be/xnqTKD8uD64?t=54m50s .

Il suo consiglio si riduce all'uso dei parametri di valore solo per una funzione fche accetta i cosiddetti argomenti sink, supponendo che si sposterà il costrutto da questi argomenti sink.

Questo approccio generale aggiunge solo il sovraccarico di un costruttore di spostamenti per gli argomenti sia lvalue che rvalue rispetto a un'implementazione ottimale degli fargomenti su misura rispettivamente lvalue e rvalue. Per capire perché questo è il caso, supponiamo che fprenda un parametro value, dove Tè un tipo di copia e sposta costruibile:

void f(T x) {
  T y{std::move(x)};
}

La chiamata fcon un argomento lvalue comporterà il richiamo di un costruttore di copie xe di un costruttore di spostamenti y. D'altra parte, la chiamata fcon un argomento rvalue farà sì che un costruttore di mosse venga chiamato per costruire xe un altro costruttore di mosse venga chiamato per costruire y.

In generale, l'implementazione ottimale degli fargomenti for lvalue è la seguente:

void f(const T& x) {
  T y{x};
}

In questo caso, solo un costruttore di copie viene chiamato per costruire y. L'implementazione ottimale degli fargomenti for rvalue è, sempre in generale, come segue:

void f(T&& x) {
  T y{std::move(x)};
}

In questo caso, solo un costruttore di mosse viene chiamato per costruire y.

Quindi un ragionevole compromesso è prendere un parametro value e avere una chiamata in più per il costruttore di mosse per argomenti lvalue o rvalue rispetto all'implementazione ottimale, che è anche il consiglio dato nel discorso di Herb.

Come ha sottolineato @ JDługosz nei commenti, passare per valore ha senso solo per le funzioni che costruiranno un oggetto dall'argomento sink. Quando hai una funzione fche copia il suo argomento, l'approccio pass-by-value avrà un overhead maggiore di un approccio pass-by-const-reference generale. L'approccio pass-by-value per una funzione fche conserva una copia del suo parametro avrà la forma:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

In questo caso, esiste una costruzione di copia e un'assegnazione di spostamento per un argomento lvalue, nonché una costruzione di spostamento e un'assegnazione di spostamento per un argomento rvalue. Il caso più ottimale per un argomento lvalue è:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

Questo si riduce a un solo compito, che è potenzialmente molto più economico del costruttore di copie più l'assegnazione di spostamento richiesta per l'approccio pass-by-value. La ragione di ciò è che l'assegnazione potrebbe riutilizzare la memoria allocata esistente ye quindi impedire (de) allocazioni, mentre il costruttore della copia di solito allocerà la memoria.

Per un argomento rvalue l'implementazione più ottimale per fquella conserva una copia ha la forma:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

Quindi, solo un'assegnazione di mosse in questo caso. Passare un valore alla versione di fquello richiede un riferimento const costa solo un compito invece di un compito di spostamento. Quindi relativamente parlando, la versione di fprendere un riferimento const in questo caso è preferibile l'implementazione generale.

Quindi, in generale, per l'implementazione più ottimale, dovrai sovraccaricare o fare una sorta di inoltro perfetto, come mostrato nel discorso. Lo svantaggio è un'esplosione combinatoria del numero di sovraccarichi richiesti, a seconda del numero di parametri fnel caso in cui si scelga di sovraccaricare la categoria di valore dell'argomento. L'inoltro perfetto ha lo svantaggio che fdiventa una funzione modello, che impedisce di renderlo virtuale, e si traduce in un codice significativamente più complesso se si desidera farlo correttamente al 100% (vedere la discussione per i dettagli cruenti).


Vedi i risultati di Herb Sutter nella mia nuova risposta: fallo solo quando sposti il costrutto , non muovi l'assegnazione.
JDługosz,

1
@JDługosz, grazie per il puntatore al discorso di Herb, l'ho appena visto e ho completamente rivisto la mia risposta. Non ero a conoscenza del consiglio (sposta) di assegnare.
Ton van den Heuvel

quella cifra e i consigli sono nel documento Linee guida standard , ora.
JDługosz,

1

Il problema è che "const" è un qualificatore non granulare. Ciò che di solito si intende con "ref string const" è "non modificare questa stringa", non "non modificare il conteggio dei riferimenti". Semplicemente non c'è modo, in C ++, di dire quali membri sono "const". O tutti lo sono o nessuno di loro lo è.

Al fine di incidere a questo problema di lingua, STL potrebbe consentire "C ()" nel tuo esempio per effettuare una copia mossa-semantica in ogni caso , e doverosamente ignorare il "const" per quanto riguarda il conteggio dei riferimenti (mutabile). Finché fosse ben specificato, questo andrebbe bene.

Dato che STL no, ho una versione di una stringa che const_casts <> elimina il contatore di riferimento (non c'è modo di rendere retroattivamente qualcosa di mutabile in una gerarchia di classi), e - ecco ed ecco - puoi passare liberamente i cmstring come riferimenti const, e farne copie con funzioni profonde, tutto il giorno, senza perdite o problemi.

Dato che C ++ non offre qui "granularità const di classe derivata", scrivere una buona specifica e creare un nuovo oggetto "string movable string" (cmstring) brillante è la soluzione migliore che abbia mai visto.


@BenVoigt yep ... invece di lanciarlo, dovrebbe essere mutabile ... ma non puoi cambiare un membro STL in mutabile in una classe derivata.
Erik Aronesty,
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.