Os dias da passagem const std :: string e como parâmetro terminaram?


604

Eu ouvi uma conversa recente de Herb Sutter, que sugeriu que as razões para passar por std::vectore desapareceram amplamente. Ele sugeriu que escrever uma função como a seguinte agora é preferível:std::stringconst &

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

Eu entendo que o valor return_valserá um rvalue no ponto em que a função retorna e, portanto, pode ser retornada usando a semântica de movimentação, que é muito barata. No entanto, invalainda é muito maior que o tamanho de uma referência (que geralmente é implementada como um ponteiro). Isso ocorre porque um std::stringpossui vários componentes, incluindo um ponteiro no heap e um membro char[]para otimização de cadeia curta. Portanto, parece-me que passar por referência ainda é uma boa ideia.

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


89
Acho que a melhor resposta para a pergunta é provavelmente ler o artigo de Dave Abrahams sobre isso no C ++ Next . Eu acrescentaria que não vejo nada sobre isso que se qualifique como off-topic ou não construtivo. É uma pergunta clara, sobre programação, para a qual existem respostas factuais.
perfil completo de Jerry Coffin

Fascinante, portanto, se você precisar fazer uma cópia de qualquer maneira, a passagem por valor provavelmente será mais rápida que a passagem por referência.
21412 Benj

3
@Sz. Sou sensível a perguntas falsamente categorizadas como duplicadas e fechadas. Não me lembro dos detalhes deste caso e não os revi. Em vez disso, vou simplesmente excluir meu comentário, supondo que cometi um erro. Obrigado por trazer isso para a minha atenção.
Howard Hinnant

2
@ HowardHinnant, muito obrigado, é sempre um momento precioso quando se depara com esse nível de atenção e sensibilidade, é tão refrescante! (Vou excluir o meu, é claro.)
Sz.

Respostas:


393

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

Digamos que eu tenho uma função Aque chama função B, que chama função C. E Apassa uma corda através Be dentro C. Anão conhece ou se importa C; tudo o que Asabemos é B. Ou seja, Cé um detalhe de implementação de B.

Digamos que A é definido da seguinte maneira:

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

Se B e C pegam a string const&, é 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 dando dicas, sem copiar, sem se mexer, todos estão felizes. Cleva um const&porque não armazena a string. Simplesmente o usa.

Agora, quero fazer uma alteração simples: Cprecisa 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 memória em potencial (ignore o SSO ). A semântica de movimentação do C ++ 11 deve permitir remover desnecessariamente a construção de cópias, certo? E Apassa um temporário; não há razão para Cter que copiar os dados. Deveria simplesmente fugir do que foi dado a ela.

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

Se eu mudar Cpara pegar seu parâmetro por valor, isso apenas Bfará a cópia nesse parâmetro; Eu não ganho nada.

Portanto, se eu tivesse passado o strvalor por todas as funções, confiando em std::moveembaralhar os dados, não teríamos esse problema. Se alguém quiser segurá-lo, pode. Se não, tudo bem.

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

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


2
Quando você diz que mudar para um valor é mais caro do que usar referências, ainda é mais caro por uma quantidade constante (independente do comprimento da string que está sendo movida), certo?
19412 Neil G

3
@ NeilG: Você entende o que significa "dependente de implementação"? O que você está dizendo está errado, porque depende de como e como o SSO é implementado.
Ildjarn

17
@ildjarn: Na análise de pedidos, se o pior caso de algo é vinculado por uma constante, então ainda é tempo constante. Não existe uma corda pequena mais longa? Essa cadeia não leva um tempo constante para copiar? Todas as seqüências menores não levam menos tempo para copiar? Em seguida, a cópia de seqüências de caracteres para seqüências pequenas é "tempo constante" na análise de pedidos - apesar das sequências pequenas levarem várias quantidades de tempo para copiar. A análise da ordem está relacionada ao comportamento assintótico .
Neil G.

8
@ NeilG: Claro, mas sua pergunta original era " que ainda é mais cara por uma quantidade constante (independente do comprimento da corda que está sendo movida), certo? " O ponto que estou tentando fazer é que poderia ser mais caro por diferentes quantidades constantes, dependendo do comprimento da string, que é resumida como "não".
Ildjarn

13
Por que a string seria movedde B a C no caso de valor? Se B é B(std::string b)e C é, C(std::string c)então temos que chamar C(std::move(b))B ou devemos bpermanecer inalterados (portanto, 'inalterados') até sair B. (Talvez um compilador de otimização mova a string sob a regra como se bnão for usada após a chamada, mas acho que não há uma garantia forte.) O mesmo vale para a cópia de strto m_str. Mesmo que um parâmetro de função tenha sido inicializado com um rvalue, ele é um lvalue dentro da função e std::moveé necessário mover-se desse lvalue.
9118 Pixelchemist

163

Os dias da passagem const std :: string e como parâmetro terminaram?

Não . Muitas pessoas seguem esse conselho (incluindo Dave Abrahams) além do domínio ao qual se aplicam e simplificam a aplicação a todos os std::string parâmetros - sempre passar std::stringpor valor não é uma "prática recomendada" para todos e quaisquer parâmetros e aplicativos arbitrários, porque as otimizações as palestras / artigos focados se aplicam apenas a um conjunto restrito de casos .

Se você estiver retornando um valor, alterando o parâmetro ou assumindo o valor, passar esse valor pode economizar cópias caras e oferecer conveniência sintática.

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

Agora, para o exemplo específico:

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 no heap e um membro char [] para otimização de string curta. Portanto, parece-me que passar por referência ainda é uma boa ideia. Alguém pode explicar por que Herb pode ter dito isso?

Se o tamanho da pilha for uma preocupação (e supondo que isso não esteja embutido / otimizado), return_val+ inval> return_val- IOW, o pico de uso da pilha pode ser reduzido passando por valor aqui (nota: simplificação excessiva das ABIs). Enquanto isso, passar pela referência const pode desativar as otimizações. O principal motivo 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 ainda não terminaram - as regras são mais complicadas do que eram antes. Se o desempenho for importante, você deve considerar como passa esses tipos, com base nos detalhes usados ​​em suas implementações.


3
No uso da pilha, as ABIs típicas passarão uma única referência em um registro sem uso da pilha.
21915 Ahcox

63

Isso depende muito da implementação do compilador.

No entanto, isso 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 compilação separada para evitar inlining. Então:
1. Se você passar um literal para essas duas funções, não verá muita diferença nas performances. Nos dois casos, um objeto string deve ser criado
2. Se você passar outro objeto std :: string, foo2terá um desempenho superior foo1, porque foo1fará uma cópia detalhada.

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 seg

4
O que é mais relevante é o que está acontecendo dentro da função: seria necessário, se chamado com uma referência, fazer uma cópia internamente que possa ser omitida ao passar por valor ?
precisa saber é o seguinte

1
@leftaroundabout Sim, claro. Minha suposição de que ambas as funções estão fazendo exatamente a mesma coisa.
BЈовић

5
Esse não é o meu ponto. Se passar por valor ou por referência é melhor depende do que você está fazendo dentro da função. No seu exemplo, você não está realmente usando muito do objeto string, portanto a referência é obviamente melhor. Mas se a tarefa da função fosse colocar a string em alguma estrutura ou executar, digamos, algum algoritmo recursivo envolvendo várias divisões da string, a passagem por valor poderia realmente salvar algumas cópias, em comparação com a passagem por referência. Nicol Bolas explica muito bem.
precisa saber é o seguinte

3
Para mim, "depende do que você faz dentro da função" é um design ruim - já que você baseia a assinatura da função nas partes internas da implementação.
Hans Olsson

1
Pode ser um erro de digitação, mas os dois últimos tempos literais têm 10 vezes menos loops.
TankorSmash 23/02

54

Resposta curta: NÃO! Resposta longa:

  • Se você não modificar a string (tratar como somente leitura), passe-a como const ref&.
    ( const ref&obviamente, ele precisa permanecer dentro do escopo enquanto a função que o utiliza é executada)
  • Se você planeja modificá-lo ou sabe que ele ficará fora do escopo (threads) , passe-o como a value, não copie o const ref&interior do corpo da sua função.

Havia uma postagem no cpp-next.com chamada "Quer velocidade, passe por valor!" . O TL; DR:

Diretriz : Não copie seus argumentos de função. Em vez disso, passe-os por valor e deixe o compilador fazer a cópia.

TRADUÇÃO DE ^

Não copie os argumentos da sua função --- significa: se você planeja modificar o valor do argumento copiando-o para uma variável interna, basta usar 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 o seguinte :

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

Quando você precisa modificar o valor do argumento no seu corpo de função.

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


2
Você recomenda passar por referência em alguns casos, mas aponta para uma diretriz que recomenda sempre passar por valor.
Keith Thompson

3
@KeithThompson Não copie seus argumentos de função. Significa não copiar const ref&para uma variável interna para modificá-la. Se você precisar modificá-lo ... torne o parâmetro um valor. É bastante claro para o meu eu que não fala inglês.
precisa saber é o seguinte

5
@KeithThompson A citação das diretrizes (não copie os argumentos da sua função. Em vez disso, passe-os por valor e deixe o compilador fazer a cópia.) É copiado nessa página. Se isso não estiver claro o suficiente, não posso ajudar. Não confio plenamente nos compiladores para fazer as melhores escolhas. Prefiro ser muito claro sobre minhas intenções na maneira como defino argumentos de função. # 1 Se for somente leitura, é um const ref&. # 2 Se preciso escrever ou sei que fica fora do escopo ... uso um valor. # 3 Se eu precisar modificar o valor original, passo por ref&. # 4 Uso pointers *se um argumento é opcional para que eu possa nullptr.
CodeAngry

11
Não estou tomando partido na questão de passar por valor ou por referência. Meu argumento é que você defende a passagem por referência em alguns casos, mas depois cita (aparentemente para apoiar sua posição) uma diretriz que recomenda sempre a passagem por valor. Se você não concorda com a diretriz, pode dizer e explicar o porquê. (Os links para cpp-next.com não está trabalhando para mim.)
Keith Thompson

4
@KeithThompson: Você está parafraseando incorretamente a diretriz. Não é para "sempre" passar por valor. Para resumir, foi "Se você tivesse feito uma cópia local, use a passagem por valor para que o compilador execute essa cópia para você". Não está dizendo para usar a passagem por valor quando você não estava fazendo uma cópia.
Ben Voigt

43

A menos que você realmente precise de uma cópia, ainda é razoável fazer isso 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 a cópia / movimentação provavelmente é mais cara, como também apresenta uma nova falha em potencial; a cópia / movimentação pode gerar uma exceção (por exemplo, a alocação durante a cópia pode falhar), enquanto a referência a um valor existente não pode.

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


Em suma, o C ++ 11 realmente não muda nada nesse sentido, exceto para pessoas que não confiam no elision da cópia.


2
Os construtores Move são tipicamente implementados com noexcept, mas os construtores de cópia obviamente não são.
precisa saber é o seguinte

25

Quase.

No C ++ 17, temos basic_string_view<?>, o que nos leva a basicamente um caso de uso restrito para std::string const&parâmetros.

A existência da semântica de movimentação eliminou um caso de uso std::string const&- se você estiver planejando armazenar o parâmetro, aceitar um std::stringvalor por é mais ideal, pois você pode movesair do parâmetro.

Se alguém chamou sua função com um C bruto, "string"isso significa que apenas um std::stringbuffer será alocado, em vez de dois no std::string const&caso.

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

Com std::string_view, desde que você não esteja passando a referida string para uma API que espera '\0'buffers de caracteres terminados no estilo C , poderá obter std::stringuma funcionalidade semelhante com mais eficiência, sem arriscar qualquer alocação. Uma string C bruta pode até ser transformada em uma std::string_viewsem nenhuma alocação ou cópia de caractere.

Nesse ponto, o uso para std::string const&é quando você não está copiando os dados por atacado e os repassa para uma API de estilo C que espera um buffer terminado nulo, e você precisa das funções de seqüência de caracteres de nível mais alto que std::stringfornecem. Na prática, esse é um conjunto raro de requisitos.


2
Agradeço esta resposta - mas quero ressaltar que ela sofre (como muitas respostas de qualidade) de um pouco de viés específico do domínio. Ou seja: “Na prática, esse é um conjunto raro de requisitos”… na minha própria experiência de desenvolvimento, essas restrições - que parecem anormalmente estreitas para o autor - são atendidas, literalmente, o tempo todo. Vale ressaltar isso.
fish2000

1
@ fish2000 Para ser claro, para std::stringdominar você não precisa apenas de alguns desses requisitos, mas de todos eles. Qualquer um ou até dois deles é, eu admito, comum. Talvez você normalmente precise dos três (como, por exemplo, você está analisando um argumento de cadeia de caracteres para escolher para qual API C você vai distribuí-lo por atacado?)
Yakk - Adam Nevraumont 6/18

@ Yakk-AdamNevraumont É uma coisa do YMMV - mas é um caso de uso frequente se (digamos) você estiver programando contra o POSIX ou outras APIs em que a semântica da string C é, como, o menor denominador comum. Devo dizer realmente que amo std::string_view- como você ressalta, “Uma string C bruta pode até ser transformada em std :: string_view sem nenhuma alocação ou cópia de caractere”, algo que vale a pena lembrar para aqueles que usam C ++ no contexto de esse uso da API, de fato.
fish2000

1
@ fish2000 "'Uma string C bruta pode até ser transformada em std :: string_view sem nenhuma alocação ou cópia de caracteres', algo que vale a pena lembrar". De fato, mas deixa de fora a melhor parte - no caso em que a string não processada é literal, ela nem exige um strlen () em tempo de execução !
Don Hatch

17

std::stringnão é dados antigos simples (POD) e seu tamanho bruto não é a coisa mais relevante de todos os tempos. Por exemplo, se você passar uma string acima do tamanho do SSO e alocada no heap, esperaria que o construtor de cópia não copiasse o armazenamento do SSO.

O motivo é recomendado porque invalé construído a partir da expressão do argumento e, portanto, sempre é movido ou copiado conforme apropriado - não há perda de desempenho, supondo que você precise ser proprietário do argumento. Caso contrário, uma constreferência ainda pode ser o melhor caminho a percorrer.


2
Ponto interessante sobre o construtor de cópias ser inteligente o suficiente para não se preocupar com o SSO, se não o estiver usando. Provavelmente correto, eu vou ter que verificar se isso é verdade ;-)
Benj

3
@ Benj: Comentário antigo, eu sei, mas se o SSO é pequeno o suficiente para copiá-lo incondicionalmente, é mais rápido do que fazer um ramo condicional. Por exemplo, 64 bytes é uma linha de cache e pode ser copiada em uma quantidade realmente trivial de tempo. Provavelmente 8 ciclos ou menos em x86_64.
Zan Lynx

Mesmo se o SSO não for copiado pelo construtor de cópia, um std::string<>é de 32 bytes alocados da pilha, 16 dos quais precisam ser inicializados. Compare isso com apenas 8 bytes alocados e inicializados para uma referência: é duas vezes a quantidade de trabalho da CPU e ocupa quatro vezes mais espaço em cache que não estará disponível para outros dados.
cmaster - restabelecer monica

Ah, e eu esqueci de falar sobre a passagem de argumentos de função nos registros; que traria o uso da pilha da referência a zero pela última callee ...
cmaster - Reintegrar monica

16

Copiei / colei a resposta desta pergunta aqui e alterei os nomes e a ortografia para atender a essa pergunta.

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

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

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ópia e o segundo número é o número de construções de movimentação:

+----+--------+--------+---------+
|    | 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 lvalues ​​e xvalues. Isso pode ou não ser aceitável para qualquer situação. Ambas as soluções têm vantagens e desvantagens.


1
std :: string é uma classe de biblioteca padrão. Já é móvel e copiável. Não vejo como isso é relevante. O OP está perguntando mais sobre o desempenho da movimentação vs. referências , não o desempenho da movimentação vs. cópia.
Nicol Bolas

3
Essa resposta conta o número de movimentos e as cópias que uma std :: string sofrerá sob o design de passagem por valor descrito por Herb e Dave, versus passar por referência com um par de funções sobrecarregadas. Eu uso o código do OP na demonstração, exceto pela substituição de uma string fictícia para gritar quando ela está sendo copiada / movida.
21712 Howard Hinnant

Você provavelmente deve otimizar o código antes de executar os testes ...
The Croissant Paramagnetic

3
@TheParamagneticCroissant: Você obteve resultados diferentes? Em caso afirmativo, usando qual compilador com quais argumentos de linha de comando?
Howard Hinnant

14

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

Existe uma armadilha não mencionada em nenhuma das outras respostas aqui: se você passar uma string literal para um const std::string&parâmetro, ela passará uma referência a uma string temporária, criada em tempo real para conter os caracteres da literal. Se você salvar essa referência, ela será inválida quando a sequência temporária for desalocada. Para estar seguro, você deve salvar uma cópia , não a referência. O problema decorre do fato de que literais de seqüência de caracteres são const char[N]tipos, 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 const char*método, conforme descrito em Existe uma maneira de passar uma string literal como referência em C ++ .

(Nota: Sutter & Stroustroup recomenda que, se você mantiver uma cópia da string, forneça também uma função sobrecarregada com um parâmetro && 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
}

RESULTADO:

const char * constructor
const std::string& constructor

Second string
Second string

Este é um problema diferente e o WidgetBadRef não precisa ter um const & parâmetro para dar errado. A questão é se o WidgetSafeCopy tivesse apenas um parâmetro de string, seria mais lento? (Eu acho que a copiar temporária para membro é certamente mais fácil de spot)
Superfly Jon

Observe que T&&nem sempre é uma referência universal; de fato, std::string&&sempre será uma referência de valor e nunca uma referência universal, porque nenhuma dedução de tipo é realizada . Assim, o conselho de Stroustroup & Sutter não entra em conflito com o de Meyers.
Justin Time - Restabelece Monica

@ JustinTime: obrigado; Eu removi a sentença final incorreta alegando, com efeito, que std :: string && seria uma referência universal.
circlepi314

@ circlepi314 De nada. É uma mistura fácil de fazer, às vezes pode ser confuso se algum dado T&&é uma referência universal deduzida ou uma referência de valor não deduzida; provavelmente seria mais claro se eles introduzissem um símbolo diferente para referências universais (como &&&, como uma combinação de &e &&), mas isso provavelmente pareceria tolo.
Justin Time - Restabelece Monica

8

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

Portanto, a resposta é: depende das circunstâncias:

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

6

Veja "Herb Sutter" De volta ao básico! Fundamentos do estilo moderno em C ++ " . Entre outros tópicos, ele analisa os conselhos de passagem de parâmetros que foram dados no passado e as novas idéias que acompanham o C ++ 11 e abordam especificamente o idéia de passar strings por valor.

slide 24

Os benchmarks mostram que a passagem de std::strings por valor, nos casos em que a função a copia de qualquer maneira, pode ser significativamente mais lenta!

Isso ocorre porque você está forçando-o a sempre fazer uma cópia completa (e depois passar para o local), enquanto a const&versão atualiza a cadeia antiga que pode reutilizar o buffer já alocado.

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

É apenas para parâmetros de "coletor" onde uma string deve ser criada (sem que seu valor existente seja 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 profundo você pode se preocupar com isso, assista à apresentação de Nicolai Josuttis e boa sorte com isso ( "Perfeito - Concluído!" , Algumas vezes após encontrar falhas na versão anterior. Já esteve lá?)


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


3

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

Seu conselho se resume a usar apenas parâmetros de valor para uma função fque recebe os chamados argumentos de afundamento, supondo que você mova a construção desses argumentos de afundamento.

Essa abordagem geral apenas adiciona a sobrecarga de um construtor de movimentação para os argumentos lvalue e rvalue, em comparação com uma implementação ideal de fargumentos personalizados para lvalue e rvalue, respectivamente. Para ver por que esse é o caso, suponha que fuse um parâmetro value, onde Testão alguns tipos construtíveis de copiar e mover:

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

Chamar fcom um argumento lvalue resultará em um construtor de cópia sendo chamado para construir xe um construtor de movimentação sendo chamado para construir y. Por outro lado, chamar fcom um argumento rvalue fará com que um construtor de movimentação seja chamado para construir xe outro construtor de movimentação seja chamado para construção y.

Em geral, a implementação ideal dos fargumentos for 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 ideal dos fargumentos for rvalue é, novamente em geral, da seguinte maneira:

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

Nesse caso, apenas um construtor de movimentação é chamado para construir y.

Portanto, um compromisso sensato é pegar um parâmetro de valor e solicitar um construtor de movimento extra para argumentos lvalue ou rvalue com relação à implementação ideal, 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 construirão algum objeto a partir do argumento coletor. Quando você tem uma função fque copia seu argumento, a abordagem de passagem por valor terá mais sobrecarga do que uma abordagem geral de referência de passagem por const. A abordagem de passagem por valor de uma função fque retém uma cópia de seu parâmetro terá a seguinte forma:

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

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

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

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

Para um argumento rvalue, a implementação mais ideal para freter uma cópia tem o formato:

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

Portanto, apenas uma atribuição de movimentação neste caso. Passar um rvalor para a versão fque leva uma referência const apenas custa uma atribuição em vez de uma atribuição de movimentação. Então, relativamente falando, a versão de ftomar uma referência const neste caso, como a implementação geral, é preferível.

Portanto, em geral, para a implementação ideal, você precisará sobrecarregar ou fazer algum tipo de encaminhamento perfeito, como mostrado na palestra. A desvantagem é uma explosão combinatória no número de sobrecargas necessárias, dependendo do número de parâmetros f, caso você opte por sobrecarregar a categoria de valor do argumento. O encaminhamento perfeito tem a desvantagem que fse torna uma função de modelo, o que evita torná-lo virtual, e resulta em um código significativamente mais complexo, se você deseja obtê-lo 100% da maneira correta (consulte a palestra para obter detalhes sangrentos).


Veja as descobertas de Herb Sutter na minha nova resposta: faça isso apenas quando você move uma construção , não move uma atribuição.
JDługosz 25/04

1
@ JDługosz, obrigado pelo ponteiro da palestra de Herb, eu apenas assisti e revi completamente minha resposta. Eu não estava ciente do (mover) conselho de atribuição.
Ton van den Heuvel

essa figura e conselho estão no documento Diretrizes padrão agora.
JDługosz

1

O problema é que "const" é um qualificador não granular. O que geralmente se entende por "const string ref" é "não modifique essa string", não "não modifique a contagem de referência". Simplesmente não há como dizer em C ++ quais membros são "const". Todos eles são, ou nenhum deles é.

Para solucionar esse problema de linguagem, o STL pode permitir que "C ()" em seu exemplo faça uma cópia semântica de movimento de qualquer maneira e ignore a "const" com respeito à contagem de referência (mutável). Contanto que fosse bem especificado, tudo bem.

Como o STL não possui, eu tenho uma versão de uma string que const_casts <> afastado o contador de referência (nenhuma maneira de tornar retroativamente algo mutável em uma hierarquia de classes) e - e eis que - você pode passar livremente o cmstring como referências const, e faça cópias deles em funções profundas, o dia inteiro, sem vazamentos ou problemas.

Como o C ++ não oferece "granularidade const de classe derivada", escrever uma boa especificação e criar um novo objeto brilhante "const movable string" (cmstring) é a melhor solução que eu já vi.


@BenVoigt sim ... em vez de jogar fora, deve ser mutável ... mas você não pode alterar um membro do STL para mutável em uma classe derivada.
Erik Aronesty
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.