É melhor em C ++ passar por valor ou passar por referência constante?


Respostas:


203

Ela costumava ser geralmente recomendado melhores práticas 1 para passar uso por ref const para todos os tipos , exceto para builtin tipos ( char, int, double, etc.), para iterators e para objetos de função (lambdas, classes decorrentes std::*_function).

Isso era especialmente verdade antes da existência da semântica de movimentação . O motivo é simples: se você passou por valor, era necessário fazer uma cópia do objeto e, exceto para objetos muito pequenos, isso sempre é mais caro do que passar uma referência.

Com o C ++ 11, ganhamos a semântica de movimentos . Em poucas palavras, a semântica de movimentação permite que, em alguns casos, um objeto possa ser passado "por valor" sem copiá-lo. Em particular, esse é o caso quando o objeto que você está passando é um rvalue .

Por si só, mover um objeto ainda é pelo menos tão caro quanto passar por referência. No entanto, em muitos casos, uma função copiará um objeto internamente de qualquer maneira - ou seja, ela assumirá a propriedade do argumento. 2

Nessas situações, temos o seguinte compromisso (simplificado):

  1. Podemos passar o objeto por referência e depois copiar internamente.
  2. Podemos passar o objeto por valor.

"Passar por valor" ainda faz com que o objeto seja copiado, a menos que o objeto seja um rvalor. No caso de um rvalue, o objeto pode ser movido, de modo que o segundo caso não é mais "copiar e depois mover", mas "mover e depois (potencialmente) mover novamente".

Para objetos grandes que implementam construtores de movimento adequados (como vetores, strings ...), o segundo caso é muito mais eficiente que o primeiro. Portanto, é recomendável usar a passagem por valor se a função se apropriar do argumento e se o tipo de objeto oferecer suporte à movimentação eficiente .


Uma nota histórica:

De fato, qualquer compilador moderno deve ser capaz de descobrir quando passar valor é caro e converter implicitamente a chamada para usar uma const ref, se possível.

Em teoria. Na prática, os compiladores nem sempre podem mudar isso sem interromper a interface binária da função. Em alguns casos especiais (quando a função está embutida), a cópia será realmente elidida se o compilador puder descobrir que o objeto original não será alterado através das ações na função.

Mas, em geral, o compilador não pode determinar isso, e o advento da semântica de movimentação no C ++ tornou essa otimização muito menos relevante.


1 Por exemplo, em Scott Meyers, C ++ eficaz .

2 Isso é especialmente verdadeiro para construtores de objetos, que podem receber argumentos e armazená-los internamente para fazer parte do estado do objeto construído.


hmmm ... não tenho certeza que vale a pena passar por ref. double-s
sergtk 6/11/08

3
Como de costume, o impulso ajuda aqui. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm possui material de modelo para descobrir automaticamente quando um tipo é um tipo interno (útil para modelos, onde às vezes você não pode saber tão facilmente).
CesarB 07/11

13
Esta resposta perde um ponto importante. Para evitar fatiar, você deve passar por referência (const ou não). Veja stackoverflow.com/questions/274626/…
ChrisN

6
@ Chris: certo. Eu deixei de fora toda a parte do polimorfismo, porque essa é uma semântica completamente diferente. Acredito que o OP (semântica) significou a passagem do argumento "por valor". Quando outras semânticas são necessárias, a questão nem se coloca.
21978 Konrad Rudolph

98

Edit: Novo artigo de Dave Abrahams no cpp-next:

Quer velocidade? Passe por valor.


A passagem por valor para estruturas em que a cópia é barata tem a vantagem adicional de o compilador assumir que os objetos não possuem alias (não são os mesmos objetos). Usando passagem por referência, o compilador não pode assumir isso sempre. Exemplo simples:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

o compilador pode otimizá-lo em

g.i = 15;
f->i = 2;

pois sabe que eg não compartilha o mesmo local. se g fosse uma referência (foo &), o compilador não poderia ter assumido isso. como gi poderia então ser aliasado por f-> ie ter que ter um valor de 7. então o compilador precisaria buscar novamente o novo valor de gi da memória.

Para regras mais práticas, aqui está um bom conjunto de regras encontradas no artigo Move Constructors (leitura altamente recomendada).

  • Se a função pretender alterar o argumento como efeito colateral, use-o por referência que não seja const.
  • Se a função não modificar seu argumento e o argumento for do tipo primitivo, aceite-o por valor.
  • Caso contrário, considere-o por referência const, exceto nos seguintes casos
    • Se a função precisar fazer uma cópia da referência const, aceite-a por valor.

"Primitivo" acima significa basicamente tipos de dados pequenos, com alguns bytes de comprimento e que não são polimórficos (iteradores, objetos de função etc.) ou caros de copiar. Nesse artigo, há uma outra regra. A idéia é que, às vezes, alguém quer fazer uma cópia (caso o argumento não possa ser modificado), e outras, não deseja (caso queira usar o próprio argumento na função, se o argumento for temporário de qualquer maneira , por exemplo). O artigo explica em detalhes como isso pode ser feito. No C ++ 1x, essa técnica pode ser usada nativamente com suporte a idiomas. Até então, eu iria com as regras acima.

Exemplos: para tornar uma string em maiúscula e retornar a versão em maiúscula, sempre se deve passar por valor: é preciso tirar uma cópia dela de qualquer maneira (não foi possível alterar diretamente a referência const) - para melhor torná-la o mais transparente possível. o chamador e faça essa cópia com antecedência, para que ele possa otimizar o máximo possível - conforme detalhado nesse documento:

my::string uppercase(my::string s) { /* change s and return it */ }

No entanto, se você não precisar alterar o parâmetro de qualquer maneira, considere-o com referência a const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

No entanto, se o objetivo do parâmetro é escrever algo no argumento, passe-o por referência não const

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}

Eu achei suas regras boas, mas não tenho certeza sobre a primeira parte em que você fala sobre não aprová-las, pois um juiz o aceleraria. sim, com certeza, mas não passar algo como um ref apenas porque otimização não faz sentido. Se você deseja alterar o objeto de pilha que está passando, faça-o por ref. se não, passe por valor. se você não quiser mudar, passe como const-ref. a otimização que acompanha o valor por valor não deve importar, pois você ganha outras coisas ao passar como ref. eu não entendo o "quer velocidade?" sice se você onde vai realizar os op você deve passar por valor de qualquer maneira ..
Chikuba

Johannes: Adorei esse artigo quando o li, mas fiquei desapontado ao tentar. Este código falhou no GCC e no MSVC. Perdi alguma coisa ou isso não funciona na prática?
user541686

Acho que não concordo que, se você quiser fazer uma cópia de qualquer maneira, você a passará por valor (em vez de const ref), e então a moverá. Veja desta maneira, o que é mais eficiente, uma cópia e uma mudança (você pode até ter duas cópias se passar adiante) ou apenas uma cópia? Sim, existem alguns casos especiais para ambos os lados, mas se seus dados não puderem ser movidos de qualquer maneira (por exemplo, um POD com toneladas de números inteiros), não haverá necessidade de cópias extras.
Ion Todirel

2
Mehrdad, não tenho certeza o que você esperava, mas o código funciona como esperado
Ion Todirel

Eu consideraria a necessidade de copiar apenas para convencer o compilador de que os tipos não se sobrepõem a uma deficiência no idioma. Prefiro usar o GCC __restrict__(que também pode trabalhar em referências) a fazer cópias excessivas. Pena que o C ++ padrão não adotou a restrictpalavra-chave do C99 .
Ruslan

12

Depende do tipo. Você está adicionando a pequena sobrecarga de ter que fazer uma referência e desreferência. Para tipos com um tamanho igual ou menor que ponteiros que estão usando o copiador padrão, provavelmente seria mais rápido passar por valor.


Para tipos não nativos, você pode (dependendo de quão bem o compilador otimiza o código) obter um aumento de desempenho usando referências const em vez de apenas referências.
JO.

9

Como foi apontado, depende do tipo. Para tipos de dados internos, é melhor passar por valor. Mesmo algumas estruturas muito pequenas, como um par de entradas, podem ter um desempenho melhor passando por valor.

Aqui está um exemplo, suponha que você tenha um valor inteiro e deseje passá-lo para outra rotina. Se esse valor foi otimizado para ser armazenado em um registro, se você quiser passar como referência, primeiro ele deve ser armazenado na memória e, em seguida, um ponteiro para a memória colocada na pilha para realizar a chamada. Se estava sendo transmitido por valor, tudo o que é necessário é o registro empurrado para a pilha. (Os detalhes são um pouco mais complicados que os dados, considerando diferentes sistemas de chamada e CPUs).

Se você está fazendo programação de modelos, geralmente é forçado a sempre passar por const ref, pois você não conhece os tipos que estão sendo transferidos. Passar penalidades por passar algo ruim por valor é muito pior do que as penalidades de passar um tipo embutido por const ref.


Nota sobre terminologia: uma estrutura contendo um milhão de ints ainda é um "tipo de POD". Possivelmente você quer dizer 'para tipos internos, é melhor passar por valor'.
Steve Jessop

6

Isto é o que eu normalmente trabalho ao projetar a interface de uma função que não é de modelo:

  1. Passe por valor se a função não desejar modificar o parâmetro e o valor for barato para copiar (int, double, float, char, bool, etc ... Observe que std :: string, std :: vector e o resto dos contêineres na biblioteca padrão NÃO são)

  2. Passe pelo ponteiro const se o valor for caro para copiar e a função não desejar modificar o valor apontado e NULL é um valor que a função manipula.

  3. Passe pelo ponteiro não-const se o valor for caro para copiar e a função quiser modificar o valor apontado e NULL é um valor que a função manipula.

  4. Passe pela referência const quando o valor for caro para copiar e a função não desejar modificar o valor referido e NULL não seria um valor válido se um ponteiro fosse usado.

  5. Passe por referência não const quando o valor for caro para copiar e a função desejar modificar o valor referido e NULL não seria um valor válido se um ponteiro fosse usado.


Adicione std::optionalà imagem e você não precisará mais de ponteiros.
Violet Giraffe

5

Parece que você recebeu sua resposta. Passar por valor é caro, mas fornece uma cópia para você trabalhar, se necessário.


Não sei por que isso foi rejeitado? Faz sentido para mim. Se você precisar do valor atualmente armazenado, passe por valor. Caso contrário, passe a referência.
Totty

4
É totalmente dependente do tipo. Fazer um tipo de POD (dados antigos simples) por referência pode de fato reduzir o desempenho, causando mais acessos à memória.
Torlack 6/11/08

1
Obviamente, passar int por referência não salva nada! Eu acho que a pergunta implica em coisas que são maiores que um ponteiro.
GeekyMonkey 6/11

4
Não é tão óbvio, eu já vi muitos códigos de pessoas que realmente não entendem como os computadores funcionam passando coisas simples por const ref porque eles disseram que essa é a melhor coisa a fazer.
Torlack 6/11/08

4

Como regra, passar pela referência const é melhor. Mas se você precisar modificar o argumento da função localmente, use melhor a passagem por valor. Para alguns tipos básicos, o desempenho geralmente é o mesmo, tanto para passar por valor quanto por referência. Na verdade, faça referência internamente representada pelo ponteiro, é por isso que você pode esperar, por exemplo, que, para o ponteiro, as duas passagens sejam iguais em termos de desempenho, ou mesmo a passagem por valor possa ser mais rápida por causa de desreferência desnecessária.


Se você precisar modificar a cópia do parâmetro do receptor, poderá fazer uma cópia no código chamado em vez de passar por valor. Na IMO, geralmente você não deve escolher a API com base em um detalhe de implementação como esse: a fonte do código de chamada é a mesma, mas seu código de objeto não.
9788 Steve Jessop #

Se você passar pelo valor, a cópia será criada. E na IMO, não importa de que maneira você cria uma cópia: via argumento passando por valor ou localmente - é isso que diz respeito ao C ++. Mas do ponto de vista do design, eu concordo com você. Mas descrevo os recursos do C ++ aqui apenas e não toque no design.
sergtk

1

Como regra geral, valor para tipos que não são de classe e referência const para classes. Se uma classe é realmente pequena, é provavelmente melhor passar por valor, mas a diferença é mínima. O que você realmente deseja evitar é passar alguma classe gigantesca por valor e duplicar tudo - isso fará uma enorme diferença se você estiver passando, por exemplo, um std :: vector com alguns elementos.


Meu entendimento é que, std::vectorna verdade, aloca seus itens na pilha e o próprio objeto vetorial nunca cresce. Oh espere. Se a operação fizer com que uma cópia do vetor seja feita, ela irá de fato duplicar todos os elementos. Isso seria ruim.
Steven Lu

1
Sim, era isso que eu estava pensando. sizeof(std::vector<int>)é constante, mas passá-lo por valor ainda copiará o conteúdo na ausência de qualquer inteligência do compilador.
Peter

1

Passe por valor para tipos pequenos.

Passe por referências const para tipos grandes (a definição de big pode variar entre máquinas) MAS, no C ++ 11, passe por valor se você for consumir os dados, pois você pode explorar a semântica de movimento. Por exemplo:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Agora o código de chamada faria:

Person p(std::string("Albert"));

E apenas um objeto seria criado e movido diretamente para o membro name_da classe Person. Se você passar pela referência const, será necessário fazer uma cópia para colocá-la name_.


-5

Diferença simples: - Na função, temos parâmetro de entrada e saída; portanto, se o seu parâmetro de entrada e saída de passagem for o mesmo, use a chamada por referência; caso contrário, se os parâmetros de entrada e saída forem diferentes, é melhor usar a chamada por valor.

exemplo void amount(int account , int deposit , int total )

parâmetro de entrada: conta, parâmetro de saída de depósito: total

entrada e saída é chamada de uso diferente por vaule

  1. void amount(int total , int deposit )

entrada total depósito saída total

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.