Eu sou um iniciante em C ++, mas não um iniciante em programação. Estou tentando aprender C ++ (c ++ 11) e não está claro para mim o mais importante: passar parâmetros.
Considerei estes exemplos simples:
Uma classe que tem todos os tipos primitivos de seus membros:
CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)
Uma classe que tem como membros tipos primitivos + 1 tipo complexo:
Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)
Uma classe que tem como membros tipos primitivos + 1 coleção de algum tipo complexo:
Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)
Quando eu crio uma conta, eu faço o seguinte:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc);
Obviamente, o cartão de crédito será copiado duas vezes neste cenário. Se eu reescrever esse construtor como
Account(std::string number, float amount, CreditCard& creditCard)
: number(number)
, amount(amount)
, creditCard(creditCard)
haverá uma cópia. Se eu reescrever como
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number)
, amount(amount)
, creditCard(std::forward<CreditCard>(creditCard))
Haverá 2 movimentos e nenhuma cópia.
Eu acho que às vezes você pode querer copiar algum parâmetro, às vezes você não deseja copiar ao criar esse objeto.
Venho do C # e, acostumado a referências, é um pouco estranho para mim e acho que deveria haver 2 sobrecargas para cada parâmetro mas sei que estou errado.
Existem boas práticas de como enviar parâmetros em C ++ porque eu realmente acho isso, digamos, nada trivial. Como você lidaria com meus exemplos apresentados acima?
std::string
é uma classe, assim comoCreditCard
, não um tipo primitivo."abc"
não é do tipostd::string
, não do tipochar */const char *
, mas do tipoconst char[N]
(com N = 4 neste caso devido a três caracteres e um nulo). Esse é um equívoco comum e bom para sair do caminho.Respostas:
A PERGUNTA MAIS IMPORTANTE PRIMEIRO:
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 :
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 para
const
: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
ouchar
, 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 :
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 sobrecarregarfoo()
e fornecer uma versão para lvalues (aceitando uma referência lvalue paraconst
) e uma versão para rvalues (aceitando uma referência rvalue):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: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: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:
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:Mas não funcionará se você tentar fazer isso:
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 paraconst
(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).
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 segueDe certa forma, isso combina as sobrecargas que mostrei anteriormente em uma única função:
C
será deduzido comoCreditCard&
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:Isso causará uma construção de cópia de
creditCard
, como você deseja. Por outro lado, quando um rvalue é passado,C
será deduzido serCreditCard
, e esta função será instanciada em seu lugar: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).fonte
const
. Se você precisar modificar o objeto original, leve por ref para non-`const. Se precisar fazer uma cópia e os movimentos forem baratos, pegue pelo valor e depois mova.obj
vez de fazer uma cópia local para mover, não?std::forward
podem ser chamados uma vez . Já vi pessoas colocando-o em loops, etc. e uma vez que esta resposta será vista por muitos iniciantes, IMHO deve haver um grande "Aviso!" - rótulo para ajudá-los a evitar essa armadilha.std::is_constructible<>
traço de tipo, a menos que sejam apropriadamente SFINAE- restrito - o que pode não ser trivial para alguns.Primeiro, deixe-me corrigir alguns detalhes. Quando você diz o seguinte:
Isso é falso. Vincular a uma referência rvalue não é um movimento. Só existe um movimento.
Além disso, uma vez que
CreditCard
não é um parâmetro de modelo,std::forward<CreditCard>(creditCard)
é apenas uma forma prolixa de dizerstd::move(creditCard)
.Agora...
Se seus tipos têm movimentos "baratos", você pode simplesmente tornar sua vida mais fácil e levar tudo por valor e "
std::move
junto".Essa abordagem renderá dois movimentos quando poderia render apenas um, mas se os movimentos forem baratos, eles podem ser aceitáveis.
Enquanto estamos neste assunto de "movimentos baratos", devo lembrá-lo que
std::string
muitas vezes é implementado com a chamada otimização de string pequeno, então seus movimentos podem não ser tão baratos quanto copiar alguns ponteiros. Como de costume com questões de otimização, se importa ou não, é algo para perguntar ao seu criador de perfil, não a mim.O que fazer se você não quiser incorrer nesses movimentos extras? Talvez eles sejam muito caros, ou pior, talvez os tipos não possam realmente ser movidos e você possa gerar cópias extras.
Se houver apenas um parâmetro problemático, você pode fornecer duas sobrecargas, com
T const&
eT&&
. Isso vinculará referências o tempo todo até a inicialização do membro real, onde ocorre uma cópia ou movimentação.No entanto, se você tiver mais de um parâmetro, isso leva a uma explosão exponencial no número de sobrecargas.
Este é um problema que pode ser resolvido com o encaminhamento perfeito. Isso significa que você escreve um modelo e usa
std::forward
para transportar a categoria de valor dos argumentos para seu destino final como membros.fonte
Account("",0,{brace, initialisation})
mais escrever .Em primeiro lugar,
std::string
é um tipo de classe bastante robustostd::vector
. Certamente não é primitivo.Se você estiver pegando qualquer tipo móvel grande por valor em um construtor, eu os colocaria
std::move
no membro:É exatamente assim que eu recomendaria implementar o construtor. Isso faz com que os membros
number
ecreditCard
para ser jogada construída, ao invés de cópia construído. Quando você usa este construtor, haverá uma cópia (ou movimento, se temporário) conforme o objeto é passado para o construtor e, em seguida, um movimento ao inicializar o membro.Agora vamos considerar este construtor:
Você está certo, isso envolverá uma cópia de
creditCard
, porque primeiro é passado para o construtor por referência. Mas agora você não pode passarconst
objetos para o construtor (porque a referência não éconst
) e você não pode passar objetos temporários. Por exemplo, você não poderia fazer isso:Agora vamos considerar:
Aqui você mostrou um mal-entendido de referências de rvalue e
std::forward
. Você só deve realmente usarstd::forward
quando o objeto que está encaminhando for declarado comoT&&
para algum tipo deduzidoT
. AquiCreditCard
não é deduzido (estou supondo) e, portanto, ostd::forward
está sendo usado por engano. Procure referências universais .fonte
Eu uso uma regra bastante simples para o caso geral: usar cópia para POD (int, bool, double, ...) e const & para todo o resto ...
E querer copiar ou não, não é respondido pela assinatura do método, mas mais pelo que você faz com os parâmetros.
precisão para ponteiro: quase nunca os usei. A única vantagem sobre & é que eles podem ser nulos ou reatribuídos.
fonte
Os ponteiros (*) podem referir-se à memória alocada dinamicamente, portanto, quando possível, você deve preferir referências a ponteiros, mesmo se as referências forem, no final, geralmente implementadas como ponteiros.
(**) "normalmente" significa por construtor de cópia (se passa um objeto do mesmo tipo do parâmetro) ou por construtor normal (se passa um tipo compatível para a classe). Quando você passa um objeto como
myMethod(std::string)
, por exemplo, o construtor de cópia será usado se umstd::string
for passado para ele, portanto, você deve ter certeza de que existe um.fonte