Os dias de passar const são string e parametrizados?

Eu ouvi uma recente palestra de Herb Sutter, que sugeriu que as razões para passar std::vector e std::string por const & praticamente desapareceram. Ele sugeriu que escrever uma function como a seguinte é agora preferível:

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

Eu entendo que o return_val será um rvalue no ponto que a function retorna e pode, portanto, ser retornado usando a semântica de movimento, que é muito barata. No entanto, o inval ainda é muito maior que o tamanho de uma referência (que geralmente é implementada como um ponteiro). Isso ocorre porque um std::string possui vários componentes, incluindo um ponteiro para o heap e um membro char[] para otimização de string curta. Então, parece-me que passar por referência ainda é uma boa ideia.

Alguém pode explicar por que Herb poderia ter dito isso?

A razão pela qual Herb disse o que ele disse é por causa de casos como este.

Digamos que eu tenha a function A que chama a function B , que chama a function C E A passa uma corda através de B e para C A não sabe nem se importa com C ; tudo que A conhece é B Ou seja, C é um detalhe de implementação de B

Digamos que A seja definido da seguinte forma:

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

Se B e C pegam a string por const& , então é algo como isto:

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

Tudo bem e bem. Você está apenas passando dicas, sem copiar, sem se mexer, todo mundo está feliz. C pega uma const& porque não armazena a string. Ele simplesmente usa isso.

Agora, quero fazer uma alteração simples: C precisa armazenar a string em algum lugar.

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

Olá, copie o construtor e a alocação de memory potencial (ignore a Short String Optimization (SSO) ). A semântica de movimento do C ++ 11 supostamente torna possível remover a construção desnecessária de cópias, certo? E A passa um temporário; Não há razão para que C tenha que copiar os dados. Deve apenas fugir do que foi dado a ela.

Exceto que não pode. Porque é preciso um const& .

Se eu mudar C para tomar seu parâmetro por valor, isso faz com que B faça a cópia nesse parâmetro; Eu não ganho nada.

Então, se eu tivesse acabado de passar str por valor através de todas as funções, contando com std::move para embaralhar os dados, não teríamos esse problema. Se alguém quiser segurá-lo, eles podem. Se não, oh bem.

É mais caro? Sim; mudar para um valor é mais caro do que usar referências. É menos caro que a cópia? Não para strings pequenas com SSO. Vale a pena fazer?

Depende do seu caso de uso. Quanto você odeia alocações de memory?

Os dias de passar const são string e parametrizados?

Não Muitas pessoas aceitam esse conselho (incluindo Dave Abrahams) além do domínio ao qual ele se aplica, e o simplificam para aplicar a todos os parâmetros std::stringSempre passar std::string por valor não é uma “melhor prática” para todo e qualquer parameters arbitrários e aplicativos porque as otimizações que essas conversações / artigos focam aplicam-se apenas a um conjunto restrito de casos .

Se você estiver retornando um valor, alterando o parâmetro ou assumindo o valor, passar por valor poderá economizar uma cópia dispendiosa e oferecer conveniência sintática.

Como sempre, passar por referência const economiza muitas cópias quando você não precisa de uma cópia .

Agora, para o exemplo específico:

No entanto inval ainda é muito maior do que o tamanho de uma referência (que geralmente é implementada como um ponteiro). Isso ocorre porque um std :: string possui vários componentes, incluindo um ponteiro para o heap e um membro char [] para otimização de string curta. Então, parece-me que passar por referência ainda é uma boa ideia. Alguém pode explicar por que Herb poderia ter dito isso?

Se o tamanho da pilha for uma preocupação (e supondo que isso não seja inlined / optimized), return_val + inval > return_val – IOW, o uso da pilha de pico pode ser reduzido passando por valor aqui (note: simplificação excessiva de ABIs). Enquanto isso, passar por referência const pode desabilitar as otimizações. A principal razão aqui não é evitar o crescimento da pilha, mas garantir que a otimização possa ser executada onde for aplicável .

Os dias de passagem pela referência const não acabaram – as regras são apenas mais complicadas do que eram antes. Se o desempenho for importante, será sensato considerar como você passa esses tipos, com base nos detalhes usados ​​em suas implementações.

Isso depende muito da implementação do compilador.

No entanto, também depende do que você usa.

Vamos considerar as próximas funções:

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

Essas funções são implementadas em uma unidade de compilation separada para evitar inlining. Então :
1. Se você passar um literal para essas duas funções, você não verá muita diferença nas performances. Em ambos os casos, um object string deve ser criado
2. Se você passar outro object std :: string, foo2 irá superar foo1 , porque foo1 fará uma cópia profunda.

No meu PC, usando o g ++ 4.6.1, obtive estes resultados:

  • variável por referência: 1000000000 iterações -> tempo decorrido: 2.25912 seg
  • variável por valor: 1000000000 iterações -> tempo decorrido: 27.2259 seg
  • literal por referência: 100000000 iterações -> tempo decorrido: 9.10319 seg
  • literal por valor: 100000000 iterações -> tempo decorrido: 8.62659 segundos

A menos que você realmente precise de uma cópia, ainda é razoável levar const & . Por exemplo:

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

Se você alterar isso para obter a string por valor, você acabará movendo ou copiando o parâmetro, e não há necessidade disso. Não apenas copiar / mover é provavelmente mais caro, mas também introduz uma nova falha potencial; a cópia / movimentação pode lançar uma exceção (por exemplo, a alocação durante a cópia pode falhar), enquanto não é possível fazer uma referência a um valor existente.

Se você precisa de uma cópia, em seguida, passando e retornando por valor é geralmente (sempre?) A melhor opção. Na verdade, eu geralmente não me preocuparia com isso em C ++ 03, a menos que você ache que cópias extras realmente causam um problema de desempenho. Copiar elision parece bastante confiável em compiladores modernos. Eu acho que o ceticismo das pessoas e a insistência de que você tenha que checar sua tabela de suporte a compiladores para o RVO estão praticamente obsoletas hoje em dia.


Em suma, o C ++ 11 realmente não muda nada a este respeito, exceto para pessoas que não confiam em elisão de cópia.

Resposta curta: NÃO! Resposta longa:

  • Se você não modificar a string (tratar é como somente leitura), passe-a como const ref& .
    (o const ref& obviamente precisa ficar dentro do escopo enquanto a function que o usa executa)
  • Se você planeja modificá-lo ou sabe que ele ficará fora do escopo (threads) , passe-o como um value , não copie o const ref& dentro do seu corpo da function.

Havia um post no cpp-next.com chamado “Quer velocidade, passe por valor!” . O TL; DR:

Diretriz : não copie os argumentos da sua function. Em vez disso, passe-os por valor e deixe o compilador fazer a cópia.

TRADUÇÃO DE ^

Não copie os argumentos da function — significa: se você planeja modificar o valor do argumento copiando-o para uma variável interna, use apenas um argumento de valor .

Então, não faça isso :

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

faça isso :

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

Quando você precisa modificar o valor do argumento no corpo da function.

Você só precisa estar ciente de como planeja usar o argumento no corpo da function. Somente leitura ou NÃO … e se ficar dentro do escopo.

Quase.

Em C ++ 17, temos basic_string_view , O que nos leva basicamente a um caso de uso restrito para std::string const& parâmetros std::string const& .

A existência da semântica de movimento eliminou um caso de uso para a std::string const& – se você está planejando armazenar o parâmetro, tomar um valor std::string por é mais ideal, já que você pode sair do parâmetro.

Se alguém chamou sua function com uma "string" C bruta, isso significa que apenas um buffer std::string será alocado, ao contrário de dois no std::string const& case.

No entanto, se você não pretende fazer uma cópia, tomar por std::string const& ainda é útil em C ++ 14.

Com std::string_view , contanto que você não esteja passando a cadeia para uma API que espera buffers de caractere std::string_view em estilo '\0' , você pode obter mais eficientemente a funcionalidade std::string sem arriscar qualquer alocação. Uma string C bruta pode até ser transformada em std::string_view sem nenhuma cópia de alocação ou caractere.

Nesse ponto, o uso de std::string const& é quando você não está copiando o atacado de dados e vai passá-lo para uma API de estilo C que espera um buffer terminado nulo, e você precisa da cadeia de nível superior funções que std::string fornece. Na prática, esse é um conjunto raro de requisitos.

std::string não é Plain Old Data (POD) , e seu tamanho bruto não é a coisa mais relevante de todas. Por exemplo, se você passar uma cadeia de caracteres acima do tamanho do SSO e alocada no heap, esperaria que o construtor de cópia não copiasse o armazenamento SSO.

A razão pela qual isso é recomendado é porque o inval é construído a partir da expressão de argumento e, portanto, é sempre movido ou copiado conforme apropriado – não há perda de desempenho, supondo que você precise da propriedade do argumento. Se você não fizer isso, uma referência const ainda pode ser o melhor caminho a percorrer.

Eu copiei / colei a resposta desta questão aqui e mudei os nomes e a ortografia para ajustar essa questão.

Aqui está o código para medir o que está sendo perguntado:

 #include  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()); } 

Para mim, isso gera:

 $ 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&&) 

A tabela abaixo resume meus resultados (usando clang-std = c ++ 11). O primeiro número é o número de construções de cópias e o segundo número é o número de construções de movimento:

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

A solução de passagem por valor requer apenas uma sobrecarga, mas custa uma construção de movimentação extra ao passar lvalores e xvalores. Isso pode ou não ser aceitável para qualquer situação. Ambas as soluções têm vantagens e desvantagens.

Herb Sutter ainda está no registro, junto com Bjarne Stroustroup, em recomendar const std::string& como um tipo de parâmetro; veja https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .

Há uma armadilha não mencionada em nenhuma das outras respostas aqui: se você passar uma string literal para um parâmetro const std::string& , ela passará uma referência para uma string temporária, criada na hora para conter os caracteres de o literal. Se você salvar essa referência, ela será inválida assim que a sequência temporária for desalocada. Por segurança, você deve salvar uma cópia , não a referência. O problema decorre do fato de que literais de string são tipos const char[N] , exigindo promoção para std::string .

O código abaixo ilustra a armadilha e a solução alternativa, juntamente com uma opção de eficiência menor – sobrecarregando com um método const char* , conforme descrito em Existe uma maneira de passar uma string literal como referência em C ++ .

(Nota: Sutter & Stroustroup avisam que se você mantém uma cópia da string, também fornece uma function sobrecarregada com um parâmetro && e std :: move ().)

 #include  #include  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 } 

SAÍDA:

 const char * constructor const std::string& constructor Second string Second string 

O IMO usando a referência C ++ para std::string é uma otimização local rápida e curta, enquanto usar valor de passagem pode ser (ou não) uma otimização global melhor.

Então a resposta é: depende das circunstâncias:

  1. Se você escrever todo o código do lado de fora para as funções internas, você sabe o que o código faz, você pode usar a referência const std::string & .
  2. Se você escrever o código da biblioteca ou usar intensamente o código da biblioteca onde as strings são passadas, você provavelmente ganhará mais no sentido global, confiando no comportamento do construtor de cópia std::string .

Veja “Herb Sutter” Back to the Basics! Essenciais do Modern C ++ Style ” . Entre outros tópicos, ele revisa o conselho de passagem de parâmetro que foi dado no passado, e novas idéias que vêm com o C ++ 11 e especificamente olha para o ideia de passar strings por valor.

slide 24

Os benchmarks mostram que passar std::string s por valor, nos casos em que a function irá copiá-lo de qualquer maneira, pode ser significativamente mais lento!

Isso ocorre porque você está forçando-o a sempre fazer uma cópia completa (e depois se mover), enquanto a const& version atualizará a string antiga que pode reutilizar o buffer já alocado.

Veja o slide dele 27: Para funções “set”, a opção 1 é a mesma que sempre foi. A opção 2 adiciona uma sobrecarga para referência de valor, mas isso gera uma explosão combinatória se houver vários parâmetros.

É somente para os parâmetros “sink”, onde uma string deve ser criada (não ter seu valor existente alterado) que o truque de passagem por valor é válido. Ou seja, construtores nos quais o parâmetro inicializa diretamente o membro do tipo correspondente.

Se você quiser ver o quão fundo você pode se preocupar com isso, assista a apresentação de Nicolai Josuttis e boa sorte com isso ( “Perfeito – Feito!” N vezes depois de encontrar a falha na versão anterior. Já esteve lá?)


Isso também é resumido como .15F.15 nas Diretrizes Padrão.

Como @ JDługosz aponta nos comentários, Herb dá outros conselhos em outra (mais tarde?) Conversa, veja mais ou menos a partir daqui: https://youtu.be/xnqTKD8uD64?t=54m50s .

Seu conselho resume-se apenas ao uso de parâmetros de valor para uma function f que usa os chamados argumentos sink, supondo que você irá mover a construção a partir desses argumentos do coletor.

Essa abordagem geral apenas adiciona a sobrecarga de um construtor de movimento para os argumentos lvalue e rvalue, em comparação com uma implementação ideal de f adaptada aos argumentos lvalue e rvalue, respectivamente. Para ver por que esse é o caso, suponha que f tome um parâmetro de valor, em que T é um tipo construtível de copiar e mover:

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

Chamar f com um argumento lvalue resultará em um construtor de cópia sendo chamado para construir x e um construtor de movimento sendo chamado para construir y . Por outro lado, chamar f com um argumento rvalue fará com que um construtor de movimento seja chamado para construir x , e outro construtor de movimento seja chamado para construir y .

Em geral, a implementação ótima de f para argumentos lvalue é a seguinte:

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

Nesse caso, apenas um construtor de cópia é chamado para construir y . A implementação ótima de f para argumentos de valor é, novamente, em geral, como segue:

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

Nesse caso, apenas um construtor de movimento é chamado para construir y .

Portanto, um compromisso sensato é pegar um parâmetro de valor e ter uma chamada extra de construtor de movimento para argumentos lvalue ou rvalue com relação à implementação ótima, que é também o conselho dado na palestra de Herb.

Como @ JDługosz apontou nos comentários, passar por valor só faz sentido para funções que irão construir algum object a partir do argumento do coletor. Quando você tem uma function f que copia seu argumento, a abordagem de passagem por valor terá mais sobrecarga do que uma abordagem geral de passagem por referência const. A abordagem de passagem por valor para uma function f que retém uma cópia de seu parâmetro terá o formato:

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

Neste caso, há uma construção de cópia e uma atribuição de movimento para um argumento de lvalue, e uma construção de movimento e uma atribuição de movimento para um argumento de valor. O caso mais ideal para um argumento lvalue é:

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

Isso se resume a apenas uma atribuição, que é potencialmente muito mais barata que o construtor de cópias, além da atribuição de movimento necessária para a abordagem de passagem por valor. A razão para isso é que a atribuição pode reutilizar a memory alocada existente em y e, portanto, impedir (de) alocações, enquanto o construtor de cópia geralmente aloca memory.

Para um argumento rvalue, a implementação mais ótima para f que retém uma cópia tem o formato:

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

Portanto, apenas uma atribuição de movimento neste caso. Passar um rvalue para a versão de f que leva uma referência const apenas custa uma atribuição em vez de uma atribuição de movimento. Então, relativamente falando, a versão de f tomar uma referência const neste caso como a implementação geral é preferível.

Então, em geral, para a implementação mais ideal, você precisará sobrecarregar ou fazer algum tipo de redirecionamento perfeito, como mostrado na palestra. A desvantagem é uma explosão combinatória no número de sobrecargas requeridas, dependendo do número de parâmetros para f no caso de você optar por sobrecarregar a categoria de valor do argumento. O encaminhamento perfeito tem a desvantagem de que f se torna uma function de modelo, o que evita torná-lo virtual e resulta em código significativamente mais complexo, se você quiser acertar 100% (consulte a palestra para obter detalhes).

The problem is that “const” is a non-granular qualifier. What is usually meant by “const string ref” is “don’t modify this string”, not “don’t modify the reference count”. There is simply no way, in C++, to say which members are “const”. They either all are, or none of them are.

In order to hack around this language issue, STL could allow “C()” in your example to make a move-semantic copy anyway , and dutifully ignore the “const” with regard to the reference count (mutable). As long as it was well-specified, this would be fine.

Since STL doesn’t, I have a version of a string that const_casts<> away the reference counter (no way to retroactively make something mutable in a class hierarchy), and – lo and behold – you can freely pass cmstring’s as const references, and make copies of them in deep functions, all day long, with no leaks or issues.

Since C++ offers no “derived class const granularity” here, writing up a good specification and making a shiny new “const movable string” (cmstring) object is the best solution I’ve seen.