Vamos imaginar que temos uma estrutura para manter 3 duplos com algumas funções-membro:
struct Vector {
double x, y, z;
// ...
Vector &negate() {
x = -x; y = -y; z = -z;
return *this;
}
Vector &normalize() {
double s = 1./sqrt(x*x+y*y+z*z);
x *= s; y *= s; z *= s;
return *this;
}
// ...
};
Isso é um pouco artificial para simplificar, mas tenho certeza de que você concorda que código semelhante existe por aí. Os métodos permitem que você encadeie convenientemente, por exemplo:
Vector v = ...;
v.normalize().negate();
Ou ainda:
Vector v = Vector{1., 2., 3.}.normalize().negate();
Agora, se fornecermos as funções begin () e end (), poderíamos usar nosso Vector em um novo estilo de loop for, digamos, para fazer um loop sobre as 3 coordenadas x, y e z (você pode, sem dúvida, construir exemplos mais "úteis" substituindo Vector por, por exemplo, String):
Vector v = ...;
for (double x : v) { ... }
Podemos até fazer:
Vector v = ...;
for (double x : v.normalize().negate()) { ... }
e também:
for (double x : Vector{1., 2., 3.}) { ... }
No entanto, o seguinte (parece-me) está quebrado:
for (double x : Vector{1., 2., 3.}.normalize()) { ... }
Embora pareça uma combinação lógica dos dois usos anteriores, acho que este último uso cria uma referência pendente, enquanto os dois anteriores são perfeitamente adequados.
- Isso é correto e amplamente apreciado?
- Qual das opções acima é a parte "ruim" que deve ser evitada?
- A linguagem seria melhorada mudando a definição do loop for baseado em intervalo de forma que os temporários construídos na expressão for existissem durante o loop?
Respostas:
Sim, seu entendimento das coisas está correto.
A parte ruim é pegar uma referência de valor l para um temporário retornado de uma função e vinculá-la a uma referência de valor r. É tão ruim quanto isso:
O
Vector{1., 2., 3.}
tempo de vida do temporário não pode ser estendido porque o compilador não tem ideia de que o valor de retorno de faznormalize
referência a ele.Isso seria altamente inconsistente com o funcionamento do C ++.
Isso impediria certas pegadinhas feitas por pessoas usando expressões encadeadas em temporários ou vários métodos de avaliação preguiçosa para expressões? Sim. Mas também exigiria código do compilador de caso especial, além de ser confuso quanto ao motivo pelo qual não funciona com outras construções de expressão.
Uma solução muito mais razoável seria informar ao compilador que o valor de retorno de uma função é sempre uma referência e
this
, portanto, se o valor de retorno estiver vinculado a uma construção de extensão temporária, ele estenderá o temporário correto. No entanto, essa é uma solução em nível de linguagem.Atualmente (se o compilador suportar), você pode fazer com que
normalize
não ser chamado temporariamente:Isso causará
Vector{1., 2., 3.}.normalize()
um erro de compilação, enquantov.normalize()
funcionará bem. Obviamente, você não será capaz de corrigir coisas como esta:Mas você também não poderá fazer coisas incorretas.
Como alternativa, conforme sugerido nos comentários, você pode fazer com que a versão de referência rvalue retorne um valor em vez de uma referência:
Se
Vector
fosse um tipo com recursos reais para mover, você poderia usar em seuVector ret = std::move(*this);
lugar. A otimização do valor de retorno nomeado torna isso razoavelmente ideal em termos de desempenho.fonte
delete
você poderia fornecer uma operação alternativa que retorna um rvalue:Vector normalize() && { normalize(); return std::move(*this); }
(Eu acredito que a chamada paranormalize
dentro da função irá despachar para a sobrecarga de lvalue, mas alguém deve verificar :)&
/&&
qualificação de métodos. É do C ++ 11 ou é alguma (talvez muito difundida) extensão de compilador proprietário. Oferece possibilidades interessantes.Isso não é uma limitação da linguagem, mas um problema com seu código. A expressão
Vector{1., 2., 3.}
cria um temporário, mas anormalize
função retorna uma referência de lvalue . Porque a expressão é um lvalue , o compilador assume que o objeto estará vivo, mas como é uma referência a um temporário, o objeto morre depois que a expressão completa é avaliada, então você fica com uma referência pendente.Agora, se você alterar seu design para retornar um novo objeto por valor, em vez de uma referência ao objeto atual, não haverá nenhum problema e o código funcionará conforme o esperado.
fonte
const
referência estenderia a vida útil do objeto neste caso?normalize()
uma função mutante em um objeto existente. Daí a questão. O fato de um temporário ter uma "vida útil estendida" quando usado para o propósito específico de uma iteração, e não de outra forma, é, eu acho, uma característica confusa.const&
) tem seu tempo de vida estendido.Vector & r = Vector{1.,2.,3.}.normalize();
. Seu design tem essa limitação, e isso significa que ou você está disposto a retornar por valor (o que pode fazer sentido em muitas circunstâncias, e mais ainda com rvalue-referências e mover ), ou então você precisa lidar com o problema no lugar de call: crie uma variável apropriada e, em seguida, use-a no loop for. Observe também que a expressãoVector v = Vector{1., 2., 3.}.normalize().negate();
cria dois objetos ...T const& f(T const&);
está tudo bem.T const& t = f(T());
está completamente bem. E aí, em outra TU você descobre issoT const& f(T const& t) { return t; }
e chora ... Seoperator+
opera com valores, é mais seguro ; então o compilador pode otimizar a cópia (Quer velocidade? Passar por valores), mas isso é um bônus. A única vinculação de temporários que eu permitiria é a vinculação a referências de valores r, mas as funções devem então retornar valores para segurança e contar com Copiar Elisão / Semântica de Movimento.IMHO, o segundo exemplo já está falho. O retorno dos operadores modificadores
*this
é conveniente da maneira que você mencionou: permite o encadeamento de modificadores. Ele pode ser usado simplesmente para entregar o resultado da modificação, mas fazer isso está sujeito a erros porque pode ser facilmente esquecido. Se eu vir algo comoEu não suspeitaria automaticamente que as funções se modificam
v
como um efeito colateral. Claro, eles poderiam , mas seria confuso. Então, se eu fosse escrever algo assim, faria com que issov
permanecesse constante. Para seu exemplo, eu adicionaria funções gratuitase então escrever os loops
e
É IMO melhor legível e mais seguro. Obviamente, isso requer uma cópia extra; no entanto, para dados alocados em heap, isso provavelmente poderia ser feito em uma operação de movimentação barata do C ++ 11.
fonte