A passagem por valor é um padrão razoável no C ++ 11?


142

No C ++ tradicional, a passagem de valor para funções e métodos é lenta para objetos grandes e geralmente é desaprovada. Em vez disso, os programadores de C ++ tendem a passar referências, o que é mais rápido, mas apresenta todos os tipos de perguntas complicadas sobre propriedade e principalmente sobre gerenciamento de memória (no caso de o objeto ser alocado por heap)

Agora, no C ++ 11, temos referências Rvalue e movemos construtores, o que significa que é possível implementar um objeto grande (como um std::vector) que é barato para passar valor por uma função.

Então, isso significa que o padrão deve ser passar valor para instâncias de tipos como std::vectore std::string? E os objetos personalizados? Qual é a nova melhor prática?


22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated). Não entendo como é complicado ou problemático para a propriedade? Pode ser que eu perdi alguma coisa?
Iammilind 29/09

1
@iammilind: Um exemplo da experiência pessoal. Um segmento tem um objeto de sequência. É passado para uma função que gera outro encadeamento, mas, desconhecido para o chamador, a função pegou a sequência como const std::string&e não uma cópia. O primeiro thread então saiu ...
Zan Lynx 29/09

12
@ZanLynx: Isso soa como uma função que claramente nunca foi projetada para ser chamada como uma função de thread.
Nicol Bolas

5
Concordando com iammilind, não vejo nenhum problema. Passar por referência const deve ser o padrão para objetos "grandes" e por valor para objetos menores. Eu colocaria o limite entre grande e pequeno em torno de 16 bytes (ou 4 ponteiros em um sistema de 32 bits).
JN

Respostas:


138

É um padrão razoável se você precisar fazer uma cópia dentro do corpo. Isto é o que Dave Abrahams está defendendo :

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

No código, isso significa não fazer isso:

void foo(T const& t)
{
    auto copy = t;
    // ...
}

mas faça o seguinte:

void foo(T t)
{
    // ...
}

qual tem a vantagem que o chamador pode usar fooassim:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

e apenas um trabalho mínimo é feito. Você precisaria de duas sobrecargas para fazer o mesmo com referências void foo(T const&);e void foo(T&&);.

Com isso em mente, agora escrevi meus valiosos construtores como tais:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

Caso contrário, passar por referência a conststill é razoável.


29
+1, especialmente para o último bit :) Não se deve esquecer que Mover construtores só pode ser chamado se não for esperado que o objeto para mover depois seja inalterado: SomeProperty p; for (auto x: vec) { x.foo(p); }não cabe, por exemplo. Além disso, os Mover Construtores têm um custo (quanto maior o objeto, maior o custo), enquanto const&são essencialmente gratuitos.
Matthieu M.

25
@MatthieuM. Mas é importante saber o que "quanto maior o objeto, maior o custo" da movimentação significa: "maior" na verdade significa "mais variáveis ​​de membro ele possui". Por exemplo, mover um std::vectorcom um milhão de elementos custa o mesmo que mover um com cinco elementos, pois apenas o ponteiro para a matriz na pilha é movido, nem todos os objetos no vetor. Portanto, não é realmente um problema tão grande.
Lucas

+1 Eu também tendem a usar a construção de passar por valor e depois mover desde que comecei a usar o C ++ 11. Isso me faz sentir embora um pouco desconfortável, já que meu código agora tem std::movetodo o lugar ..
Stijn

1
Existe um risco const&que me tropeçou algumas vezes. void foo(const T&); int main() { S s; foo(s); }. Isso pode ser compilado, mesmo que os tipos sejam diferentes, se houver um construtor T que use S como argumento. Isso pode ser lento, porque um objeto T grande pode estar sendo construído. Você pode pensar em passar uma referência sem copiar, mas pode estar. Veja esta resposta a uma pergunta que pedi mais. Basicamente, &geralmente se liga apenas a lvalues, mas há uma exceção para rvalue. Existem alternativas.
Aaron McDaid 22/10/2013

1
@AaronMcDaid Esta é uma notícia antiga, no sentido de que você sempre deve estar ciente mesmo antes do C ++ 11. E nada mudou muito com relação a isso.
Luc Danton

71

Em quase todos os casos, sua semântica deve ser:

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

Todas as outras assinaturas devem ser usadas apenas com moderação e com boa justificativa. O compilador agora praticamente sempre trabalha isso da maneira mais eficiente. Você pode simplesmente escrever seu código!


2
Embora eu prefira passar um ponteiro se quiser modificar um argumento. Concordo com o guia de estilo do Google que isso torna mais óbvio que o argumento será modificado sem a necessidade de verificar novamente a assinatura da função ( google-styleguide.googlecode.com/svn/trunk/… ).
precisa saber é o seguinte

40
O motivo pelo qual não gosto de passar ponteiros é que ele adiciona um possível estado de falha às minhas funções. Eu tento escrever todas as minhas funções de modo que sejam comprovadamente correta, uma vez que muito reduz o espaço para erros para esconder. foo(bar& x) { x.a = 3; }É um pedaço de um monte mais confiável (e legível!) Do quefoo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjay

22
@Max Lybbert: Com um parâmetro de ponteiro, você não precisa verificar a assinatura da função, mas precisa verificar a documentação para saber se tem permissão para passar ponteiros nulos, se a função assumirá propriedade, etc. IMHO, um parâmetro ponteiro transmite muito menos informações que uma referência não-const. Concordo, no entanto, que seria bom ter uma pista visual no site de chamada de que o argumento pode ser modificado (como a refpalavra - chave em C #).
Luc Touraille

No que diz respeito à transmissão de valor e à dependência da semântica de movimentação, considero que essas três opções explicam melhor o uso pretendido do parâmetro. Estas são as diretrizes que eu sempre sigo também.
Trevor Hickey

1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Ambas as suposições estão incorretas. unique_ptre shared_ptrpode conter nullptrvalores / nulos . Se você não quiser se preocupar com valores nulos, deve usar referências, porque elas nunca podem ser nulas. Você também não terá que digitar ->, o que você acha que seja irritante :)
Julian

10

Passe parâmetros por valor se, dentro do corpo da função, você precisar de uma cópia do objeto ou apenas precisar movê-lo. Passe const&se você precisar apenas de acesso sem mutação ao objeto.

Exemplo de cópia de objeto:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

Exemplo de movimentação de objeto:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

Exemplo de acesso sem mutação:

void read_pattern(T const& t) {
    t.some_const_function();
}

Para justificativa, consulte as postagens de Dave Abrahams e Xiang Fan .


0

A assinatura de uma função deve refletir seu uso pretendido. A legibilidade é importante, também para o otimizador.

Essa é a melhor pré-condição para um otimizador criar código mais rápido - pelo menos em teoria e, se não na realidade, em alguns anos.

As considerações de desempenho são muitas vezes superestimadas no contexto da passagem de parâmetros. O encaminhamento perfeito é um exemplo. Funções como a emplace_backmaioria são muito curtas e alinhadas de qualquer maneira.

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.