A PERGUNTA MAIS IMPORTANTE PRIMEIRO:
Existem práticas recomendadas de como enviar parâmetros em C ++ porque eu realmente acho, digamos, nada trivial
Se sua função precisar modificar o objeto original que está sendo passado, de modo que após o retorno da chamada, as modificações nesse objeto ficarão visíveis para o chamador, então você deve passar pela referência lvalue :
void foo(my_class& obj)
{
// Modify obj here...
}
Se sua função não precisa modificar o objeto original e não precisa criar uma cópia dele (em outras palavras, ela só precisa observar seu estado), você deve passar pela referência lvalue paraconst
:
void foo(my_class const& obj)
{
// Observe obj here
}
Isso permitirá que você chame a função com lvalues (lvalues são objetos com uma identidade estável) e com rvalues (rvalues são, por exemplo , temporários ou objetos dos quais você está prestes a se mover como resultado da chamada std::move()
).
Também se poderia argumentar que para tipos fundamentais ou para os quais a cópia é rápida , como int
, bool
ou char
, não há necessidade de passar por referência se a função simplesmente precisar observar o valor, e a passagem por valor deve ser favorecida . Isso está correto se a semântica de referência não for necessária, mas e se a função quisesse armazenar um ponteiro para esse mesmo objeto de entrada em algum lugar, de modo que leituras futuras por meio desse ponteiro verão as modificações de valor que foram realizadas em alguma outra parte do código? Nesse caso, passar por referência é a solução correta.
Se sua função não precisa modificar o objeto original, mas precisa armazenar uma cópia desse objeto ( possivelmente para retornar o resultado de uma transformação da entrada sem alterar a entrada ), então você pode considerar tomar por valor :
void foo(my_class obj) // One copy or one move here, but not working on
// the original object...
{
// Working on obj...
// Possibly move from obj if the result has to be stored somewhere...
}
Invocar a função acima sempre resultará em uma cópia ao passar lvalues e em um movimento ao passar rvalues. Se a sua função precisa armazenar este objeto em algum lugar, você poderia realizar um adicional de movimento a partir dele (por exemplo, no caso foo()
é uma função membro que as necessidades para armazenar o valor em um membro de dados ).
Caso as movimentações sejam caras para objetos do tipo my_class
, você pode considerar sobrecarregar foo()
e fornecer uma versão para lvalues (aceitando uma referência lvalue para const
) e uma versão para rvalues (aceitando uma referência rvalue):
// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = obj; // Copy!
// Working on copyOfObj...
}
// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = std::move(obj); // Move!
// Notice, that invoking std::move() is
// necessary here, because obj is an
// *lvalue*, even though its type is
// "rvalue reference to my_class".
// Working on copyOfObj...
}
As funções acima são tão semelhantes, na verdade, que você poderia fazer uma única função com isso: foo()
poderia se tornar um modelo de função e você poderia usar o encaminhamento perfeito para determinar se um movimento ou uma cópia do objeto sendo passado será gerado internamente:
template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
// ^^^
// Beware, this is not always an rvalue reference! This will "magically"
// resolve into my_class& if an lvalue is passed, and my_class&& if an
// rvalue is passed
{
my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
// Working on copyOfObj...
}
Você pode querer aprender mais sobre este design assistindo a esta palestra de Scott Meyers (lembre-se do fato de que o termo " Referências Universais " que ele está usando não é padrão).
Uma coisa a ter em mente é que std::forward
geralmente resultará em um movimento para rvalues, então, embora pareça relativamente inocente, encaminhar o mesmo objeto várias vezes pode ser uma fonte de problemas - por exemplo, mover-se do mesmo objeto duas vezes! Portanto, tome cuidado para não colocar isso em um loop e não encaminhar o mesmo argumento várias vezes em uma chamada de função:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
Observe também que você normalmente não recorre à solução baseada em modelo, a menos que tenha um bom motivo para isso, pois torna o código mais difícil de ler. Normalmente, você deve se concentrar na clareza e na simplicidade .
Os itens acima são apenas diretrizes simples, mas na maioria das vezes eles irão apontar para boas decisões de design.
SOBRE O RESTO DE SUA POSTAGEM:
Se eu reescrever, [...] haverá 2 movimentos e nenhuma cópia.
Isso não está correto. Para começar, uma referência de rvalue não pode ser associada a um lvalue, então isso só será compilado quando você estiver passando um rvalue do tipo CreditCard
para seu construtor. Por exemplo:
// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));
CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
Account acc("asdasd",345, std::move(cc));
Mas não funcionará se você tentar fazer isso:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
Porque cc
é um lvalue e as referências de rvalue não podem ser associadas a lvalues. Além disso, ao vincular uma referência a um objeto, nenhum movimento é executado : é apenas uma vinculação de referência. Portanto, haverá apenas um movimento.
Portanto, com base nas diretrizes fornecidas na primeira parte desta resposta, se você estiver preocupado com o número de movimentos sendo gerados quando você toma um CreditCard
valor por, você poderia definir duas sobrecargas de construtor, uma tomando uma referência de lvalue para const
( CreditCard const&
) e outra tomando uma referência rvalue ( CreditCard&&
).
A resolução de sobrecarga selecionará o primeiro ao passar um lvalue (neste caso, uma cópia será executada) e o último ao passar um rvalue (neste caso, um movimento será executado).
Account(std::string number, float amount, CreditCard const& creditCard)
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }
O uso de std::forward<>
normalmente é visto quando você deseja obter um encaminhamento perfeito . Nesse caso, seu construtor seria, na verdade, um modelo de construtor e seria mais ou menos como segue
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
De certa forma, isso combina as sobrecargas que mostrei anteriormente em uma única função: C
será deduzido como CreditCard&
no caso de você estar passando um lvalue e, devido às regras de colapso de referência, fará com que esta função seja instanciada:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
Isso causará uma construção de cópia de creditCard
, como você deseja. Por outro lado, quando um rvalue é passado, C
será deduzido ser CreditCard
, e esta função será instanciada em seu lugar:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
Isso causará uma construção de movimentação de creditCard
, que é o que você deseja (porque o valor que está sendo passado é um rvalue, e isso significa que estamos autorizados a mover a partir dele).