Eu ouvi uma conversa recente de Herb Sutter, que sugeriu que as razões para passar por std::vector
e desapareceram amplamente. Ele sugeriu que escrever uma função como a seguinte agora é preferível:std::string
const &
std::string do_something ( std::string inval )
{
std::string return_val;
// ... do stuff ...
return return_val;
}
Eu entendo que o valor return_val
será 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, 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 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?
Respostas:
A razão pela qual Herb disse o que disse é por causa de casos como este.
Digamos que eu tenho uma função
A
que chama funçãoB
, que chama funçãoC
. EA
passa uma corda atravésB
e dentroC
.A
não conhece ou se importaC
; tudo o queA
sabemos éB
. Ou seja,C
é um detalhe de implementação deB
.Digamos que A é definido da seguinte maneira:
Se B e C pegam a string
const&
, é algo como isto:Tudo bem e bem. Você está apenas dando dicas, sem copiar, sem se mexer, todos estão felizes.
C
leva umconst&
porque não armazena a string. Simplesmente o usa.Agora, quero fazer uma alteração simples:
C
precisa armazenar a string em algum lugar.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
A
passa um temporário; não há razão paraC
ter 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
C
para pegar seu parâmetro por valor, isso apenasB
fará a cópia nesse parâmetro; Eu não ganho nada.Portanto, se eu tivesse passado o
str
valor por todas as funções, confiando emstd::move
embaralhar 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?
fonte
moved
de B a C no caso de valor? Se B éB(std::string b)
e C é,C(std::string c)
então temos que chamarC(std::move(b))
B ou devemosb
permanecer inalterados (portanto, 'inalterados') até sairB
. (Talvez um compilador de otimização mova a string sob a regra como seb
não for usada após a chamada, mas acho que não há uma garantia forte.) O mesmo vale para a cópia destr
tom_str
. Mesmo que um parâmetro de função tenha sido inicializado com um rvalue, ele é um lvalue dentro da função estd::move
é necessário mover-se desse lvalue.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 passarstd::string
por 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:
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.
fonte
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:
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,
foo2
terá um desempenho superiorfoo1
, porquefoo1
fará uma cópia detalhada.No meu PC, usando o g ++ 4.6.1, obtive estes resultados:
fonte
Resposta curta: NÃO! Resposta longa:
const ref&
.(
const ref&
obviamente, ele precisa permanecer dentro do escopo enquanto a função que o utiliza é executada)value
, não copie oconst ref&
interior do corpo da sua função.Havia uma postagem no cpp-next.com chamada "Quer velocidade, passe por valor!" . O TL; DR:
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 :
faça o seguinte :
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.
fonte
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.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 porref&
. # 4 Usopointers *
se um argumento é opcional para que eu possanullptr
.A menos que você realmente precise de uma cópia, ainda é razoável fazer isso
const &
. Por exemplo: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.
fonte
noexcept
, mas os construtores de cópia obviamente não são.Quase.
No C ++ 17, temos
basic_string_view<?>
, o que nos leva a basicamente um caso de uso restrito parastd::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 umstd::string
valor por é mais ideal, pois você podemove
sair do parâmetro.Se alguém chamou sua função com um C bruto,
"string"
isso significa que apenas umstd::string
buffer será alocado, em vez de dois nostd::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á obterstd::string
uma funcionalidade semelhante com mais eficiência, sem arriscar qualquer alocação. Uma string C bruta pode até ser transformada em umastd::string_view
sem 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 questd::string
fornecem. Na prática, esse é um conjunto raro de requisitos.fonte
std::string
dominar 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?)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.std::string
nã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, umaconst
referência ainda pode ser o melhor caminho a percorrer.fonte
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.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:
Para mim, isso gera:
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:
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.
fonte
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ãoconst char[N]
tipos, exigindo promoção parastd::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 ().)
RESULTADO:
fonte
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.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.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:
const std::string &
.std::string
comportamento do construtor de cópias.fonte
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.
Os benchmarks mostram que a passagem de
std::string
s 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.
fonte
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
f
que 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
f
argumentos personalizados para lvalue e rvalue, respectivamente. Para ver por que esse é o caso, suponha quef
use um parâmetro value, ondeT
estão alguns tipos construtíveis de copiar e mover:Chamar
f
com um argumento lvalue resultará em um construtor de cópia sendo chamado para construirx
e um construtor de movimentação sendo chamado para construiry
. Por outro lado, chamarf
com um argumento rvalue fará com que um construtor de movimentação seja chamado para construirx
e outro construtor de movimentação seja chamado para construçãoy
.Em geral, a implementação ideal dos
f
argumentos for lvalue é a seguinte:Nesse caso, apenas um construtor de cópia é chamado para construir
y
. A implementação ideal dosf
argumentos for rvalue é, novamente em geral, da seguinte maneira: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
f
que 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çãof
que retém uma cópia de seu parâmetro terá a seguinte forma: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 é:
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
y
e, 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
f
reter uma cópia tem o formato:Portanto, apenas uma atribuição de movimentação neste caso. Passar um rvalor para a versão
f
que 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 def
tomar 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 quef
se 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).fonte
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.
fonte