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?

Derek Thurn
fonte
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
3
De volta ao básico de Herb Sutter ! A apresentação do Essentials of Modern C ++ na CppCon entrou em detalhes. Vídeo aqui .
Chris de Drew

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.

Luc Danton
fonte
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!

Ayjay
fonte
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 .

Edward Brey
fonte
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.

Patrick Fromberg
fonte