tutorial - c++14




I giorni di passare const std:: string e come parametro sopra? (9)

I giorni di passare const std :: string e come parametro sopra?

No Molte persone accettano questo consiglio (incluso Dave Abrahams) oltre il dominio a cui si applica e lo semplificano per applicarlo a tutti i parametri std::string - Il passaggio sempre di std::string per value non è una "best practice" per nessuno parametri e applicazioni arbitrari perché le ottimizzazioni a cui questi articoli / articoli si concentrano si applicano solo a una serie ristretta di casi .

Se si restituisce un valore, si modifica il parametro o si acquisisce il valore, quindi il passaggio per valore potrebbe risparmiare una copia costosa e offrire la convenienza sintattica.

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

Ora per l'esempio specifico:

Tuttavia inval è ancora molto più grande della dimensione di un riferimento (che di solito è implementato come un puntatore). Questo perché una std :: string ha vari componenti tra cui un puntatore nell'heap e un membro char [] per l'ottimizzazione della stringa corta. 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 in linea / ottimizzato), return_val + return_val > return_val - IOW, l'utilizzo dello stack di picco può essere ridotto passando per valore qui (nota: semplificazione eccessiva degli ABI). Nel frattempo, passando per riferimento const è possibile disabilitare le ottimizzazioni. Il motivo principale qui non è evitare la crescita dello stack, ma assicurarsi che l'ottimizzazione possa essere eseguita dove è applicabile .

I giorni di passaggio per riferimento const non sono finiti - le regole sono solo più complicate di una volta. Se le prestazioni sono importanti, sarà saggio considerare come si passano questi tipi, in base ai dettagli che si utilizzano nelle implementazioni.

Ho sentito un recente discorso di Herb Sutter che ha suggerito che i motivi per passare std::vector e std::string di const & sono in gran parte spariti. Ha suggerito che è 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_val sarà un rvalore nel punto in cui la funzione ritorna e può quindi essere restituito usando la semantica del movimento, che è molto economica. Tuttavia, inval è ancora molto più grande della dimensione di un riferimento (che di solito è implementato come un puntatore). Questo perché una std::string ha vari componenti tra cui un puntatore nell'heap e un membro char[] per l'ottimizzazione della stringa corta. Quindi mi sembra che passare per riferimento sia ancora una buona idea.

Qualcuno può spiegare perché Herb avrebbe potuto dirlo?


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

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

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

Se hai bisogno di una copia, il passaggio e la restituzione in base al valore sono solitamente (sempre?) L'opzione migliore. Di fatto, in generale, non mi preoccuperei di ciò in C ++ 03 a meno che non trovi che copie extra causano effettivamente un problema di prestazioni. Copia elision sembra abbastanza affidabile sui compilatori moderni. Penso che lo scetticismo e l'insistenza della gente a dover controllare la tabella del supporto del compilatore per RVO sia al giorno d'oggi obsoleta.

In breve, C ++ 11 non cambia davvero nulla a questo riguardo, tranne per le persone che non si fidano di copia elision.


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

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

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

(Nota: Sutter & Stroustroup consigliano che se si mantiene una copia della stringa, fornire anche una funzione sovraccaricata con un parametro && e std :: move () it.)

#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

Ho copiato / incollato la risposta da questa domanda qui, e ho cambiato i nomi e l'ortografia per adattarla alla 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 questa uscita:

$ 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 spostamento extra quando si passano lvalues ​​e xvalues. Questo può o non può essere accettabile per una determinata situazione. Entrambe le soluzioni presentano vantaggi e svantaggi.


La ragione per cui Herb ha detto quello che ha detto è a causa di casi come questo.

Diciamo che ho la funzione A che chiama la funzione B , che chiama la funzione C E A passa una stringa attraverso B e in C A non sa o si preoccupa di C ; tutto ciò che A sa è 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 da 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 i puntatori in giro, senza copiare, senza muoversi, tutti sono felici. C prende un const& perché non memorizza la stringa. Lo usa semplicemente.

Ora, voglio fare una semplice modifica: C bisogno di memorizzare la stringa da qualche parte.

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

Ciao, costruttore di copia e potenziale allocazione di memoria (ignora Short String Optimization (SSO) ). La semantica del movimento di C ++ 11 dovrebbe consentire di rimuovere inutili copia-costruzione, giusto? E A passa un temporaneo; non c'è motivo per cui C dovrebbe copiare i dati. Dovrebbe solo sfuggire a ciò che gli è stato dato.

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

Se cambio C per prendere il suo parametro in base al valore, ciò fa sì che B faccia la copia in quel parametro; Non guadagno nulla

Quindi, se avessi appena passato str value attraverso tutte le funzioni, basandomi su std::move per mescolare i dati in giro, non avremmo questo problema. Se qualcuno vuole tenerlo, possono farlo. Se non lo fanno, vabbè.

È più costoso? Sì; passare a un valore è più costoso rispetto all'utilizzo di 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?


Quasi.

In C ++ 17, abbiamo basic_string_view<?> , Che ci porta fondamentalmente in uno stretto caso d'uso per std::string const& parametri.

L'esistenza della semantica del movimento ha eliminato un caso d'uso per std::string const& - se si sta pianificando di memorizzare il parametro, prendere uno std::string per valore è più ottimale, dato che si può uscire dal parametro.

Se qualcuno ha chiamato la tua funzione con una "string" C non "string" ciò significa che viene assegnato un solo buffer std::string , invece di due nella std::string const& caso std::string const& .

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

Con std::string_view , fintanto che non passi la suddetta stringa a un'API che si aspetta buffer di caratteri '\0' stile C, puoi ottenere in modo più efficiente funzionalità come std::string senza rischiare alcuna allocazione. Una stringa C grezza può anche essere trasformata in una std::string_view senza alcuna allocazione o copia di caratteri.

A quel punto, l'uso per std::string const& è quando non si copiano i dati all'ingrosso e si passa a un'API in stile C che si aspetta un buffer con terminazione nullo e serve la stringa di livello superiore funzioni fornite da std::string . In pratica, questa è una rara serie di requisiti.


Risposta breve: NO! Risposta lunga:

  • Se non modifichi la stringa (il trattamento è di sola lettura), const ref& come const ref& .
    (il const ref& ovviamente deve rimanere all'interno della portata mentre viene eseguita la funzione che lo usa)
  • Se si prevede di modificarlo o si sa che uscirà dall'ambito (thread) , passarlo come value , non copiare il riferimento const ref& all'interno del proprio corpo funzione.

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

Linea guida : non copiare i tuoi argomenti di funzione. Invece, passali per valore e lascia che il compilatore faccia la copia.

TRADUZIONE DI ^

Non copiare i tuoi argomenti di funzione --- significa: se hai intenzione di modificare il valore dell'argomento copiandolo su una variabile interna, usa invece un argomento di 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 proprio corpo della funzione.

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


Vedi "Herb Sutter" Ritorna alle nozioni di base! Essentials of Modern C ++ Style " . Tra gli altri argomenti, passa in rassegna il parametro che passa i consigli che sono stati dati in passato e le nuove idee che arrivano con C ++ 11 e in particolare guarda al idea di passare stringhe di valore.

I benchmark mostrano che passare std::string s per valore, nei casi in cui la funzione lo copierà in ogni caso, può essere molto più lento!

Questo perché si sta costringendo a fare sempre una copia completa (e quindi a spostarsi in posizione), mentre la versione const& la const& aggiorneranno 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 di rvalue, ma ciò provoca un'esplosione combinatoria se ci sono più parametri.

È solo per i parametri "sink" in cui una stringa deve essere creata (non avere il suo valore esistente modificato) che il trucco pass-by-value è valido. Cioè costruttori in cui il parametro inizializza direttamente il membro del tipo corrispondente.

Se vuoi vedere quanto in profondità puoi andare a preoccuparti di questo, guarda la presentazione di Nicolai Josuttis e buona fortuna ( "Perfetto - Fatto!" N volte dopo aver trovato il difetto con la versione precedente.

Questo è anche riassunto come ⧺F.15 nelle linee guida standard.


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

Per aggirare questo problema linguistico, STL potrebbe consentire a "C ()" nel tuo esempio di fare una mossa - copia semantica in ogni caso , e doverosamente ignorare il "const" per quanto riguarda il conteggio dei riferimenti (mutabile). Finché è stato ben specificato, questo andrebbe bene.

Dal momento che STL non lo fa, ho una versione di una stringa che const_casts <> allontana il contatore di riferimento (non c'è modo di creare qualcosa di mutabile in una gerarchia di classi), e - ed ecco - puoi passare liberamente cmstring come riferimenti const, e fare copie di loro in funzioni profonde, tutto il giorno, senza perdite o problemi.

Dal momento che il C ++ non offre alcuna "granularità della classe derivata const", scrivere una buona specifica e creare un nuovo oggetto "const mobileable string" (cmstring) è la soluzione migliore che abbia mai visto.





c++11