Como é que uma referência não-const não pode se vincular a um objeto temporário?


226

Por que não é permitido obter referências não constantes a um objeto temporário, que função getx()retorna? Claramente, isso é proibido pelo C ++ Standard, mas estou interessado no objetivo de tal restrição, não em uma referência ao padrão.

struct X
{
    X& ref() { return *this; }
};

X getx() { return X();}

void g(X & x) {}    

int f()
{
    const X& x = getx(); // OK
    X& x = getx(); // error
    X& x = getx().ref(); // OK
    g(getx()); //error
    g(getx().ref()); //OK
    return 0;
}
  1. É claro que a vida útil do objeto não pode ser a causa, porque a referência constante a um objeto não é proibida pelo C ++ Standard.
  2. É claro que o objeto temporário não é constante na amostra acima, porque chamadas para funções não constantes são permitidas. Por exemplo, ref()poderia modificar o objeto temporário.
  3. Além disso, ref()permite enganar o compilador e obter um link para esse objeto temporário e isso resolve o nosso problema.

Além do que, além do mais:

Eles dizem que "atribuir um objeto temporário à referência const prolonga a vida útil desse objeto" e "Embora nada seja dito sobre referências não const". Minha pergunta adicional . A atribuição a seguir prolonga a vida útil do objeto temporário?

X& x = getx().ref(); // OK

4
Eu discordo da parte "a vida útil do objeto não pode ser a causa", apenas porque é declarado no padrão, que atribuir um objeto temporário à referência const estende a vida útil desse objeto à vida útil da referência const. Nada é dito sobre referências não-const embora ...
SadSido

3
Bem, qual é a causa disso "Embora nada seja dito sobre referências não constantes ...". Faz parte da minha pergunta. Existe algum sentido nisso? Pode ser que os autores do Standard tenham se esquecido das referências não constantes e em breve veremos a próxima edição principal?
Alexey Malistov 14/10/2009

2
GotW # 88: Um candidato para a "const mais importante". herbsutter.spaces.live.com/blog/cns!2D4327CC297151BB!378.entry
fnieto - Fernando Nieto

4
@ Michael: VC liga rvalues ​​a referências não const. Eles chamam isso de recurso, mas na verdade é um bug. (Note-se que não é um erro, porque é inerentemente ilógico, mas porque ele foi descartado explicitamente para evitar erros bobos.)
SBI

Respostas:


97

Neste artigo do blog do Visual C ++ sobre referências de rvalue :

... C ++ não deseja que você modifique temporariamente acidentalmente, mas chamar diretamente uma função de membro que não seja const em um rvalue modificável é explícito, portanto é permitido ...

Basicamente, você não deve tentar modificar temporários pela mesma razão de que eles são objetos temporários e morrerão a qualquer momento. A razão pela qual você pode chamar métodos não-const é que, bem, você é bem-vindo a fazer algumas coisas "estúpidas", desde que saiba o que está fazendo e seja explícito (como usar reinterpret_cast). Mas se você vincular um temporário a uma referência que não seja const, poderá continuar transmitindo-o "para sempre" apenas para que sua manipulação do objeto desapareça, porque em algum momento ao longo do caminho você esqueceu completamente que isso era temporário.

Se eu fosse você, repensaria o design de minhas funções. Por que g () está aceitando referência, ele modifica o parâmetro? Se não, faça const const, se sim, por que você tenta passar temporariamente para ele, você não se importa que seja um temporário que você está modificando? Por que getx () está retornando temporário de qualquer maneira? Se você compartilhar seu cenário real e o que está tentando realizar, poderá obter boas sugestões sobre como fazê-lo.

Ir contra a linguagem e enganar o compilador raramente resolve problemas - geralmente ele cria problemas.


Editar: Responder a perguntas no comentário: 1) X& x = getx().ref(); // OK when will x die?- Não sei e não ligo, porque é exatamente isso que quero dizer com "ir contra a linguagem". A linguagem diz que "os temporários morrem no final da declaração, a menos que sejam obrigados a const const, caso em que morrem quando a referência sai do escopo". Aplicando essa regra, parece que x já está morto no início da próxima instrução, pois não está vinculado à referência const (o compilador não sabe o que ref () retorna). Este é apenas um palpite, no entanto.

2) Eu declarei o objetivo claramente: você não tem permissão para modificar temporários, porque simplesmente não faz sentido (ignorando as referências de valor C ++ 0x rvalue). A pergunta "então por que tenho permissão para chamar membros não-constantes?" é bom, mas não tenho uma resposta melhor do que a que eu já afirmei acima.

3) Bem, se eu estou certo sobre x ao X& x = getx().ref();morrer no final da declaração, os problemas são óbvios.

De qualquer forma, com base em suas perguntas e comentários, acho que nem essas respostas extras o satisfarão. Aqui está uma tentativa / resumo final: O comitê C ++ decidiu que não faz sentido modificar temporários; portanto, eles não permitiam a ligação a referências não constantes. Pode haver alguma implementação do compilador ou problemas históricos também envolvidos, não sei. Então, surgiu um caso específico, e foi decidido que, apesar de todas as probabilidades, eles ainda permitirão a modificação direta através da chamada do método não-const. Mas isso é uma exceção - geralmente você não tem permissão para modificar temporários. Sim, o C ++ costuma ser tão estranho.


2
@sbk: 1) Na verdade, a frase correta é: "... no final da expressão completa ...". Uma "expressão completa", acredito, é definida como aquela que não é uma subexpressão de outra expressão. Se isso sempre é o mesmo que "o fim da declaração", não tenho certeza.
Sb 14/10/09

5
@sbk: 2) Na verdade, você tem permissão para modificar rvalues ​​(temporários). É proibido para built-in tipos ( intetc.), mas é permitido para tipos definidos pelo usuário: (std::string("A")+"B").append("C").
Sb 14/10/09

6
@sbk: 3) A razão pela qual Stroustrup dá (em D&E) a proibição de vincular rvalues ​​a referências não-const é que, se o Alexey g()modificasse o objeto (o que você esperaria de uma função usando uma referência não-const), modificaria um objeto que morrerá, para que ninguém pudesse obter o valor modificado de qualquer maneira. Ele diz que isso, provavelmente, é um erro.
Sbi 14/10/2009

2
@ sbk: Me desculpe se eu o ofendi, mas acho que 2) não é nada. Os valores simplesmente não são const, a menos que você os faça e pode alterá-los, a menos que sejam incorporados. Demorei um pouco para entender se, com, por exemplo, o exemplo de string, eu faço algo errado (JFTR: eu não faço), então eu tendem a levar essa distinção a sério.
Sbi 15/10/2009

5
Eu concordo com o sbi - esse assunto não é de todo preciso. É toda a base da semântica de movimentação que os valores de tipo de classe são melhor mantidos como não constantes.
Johannes Schaub - litb 15/10/09

38

No seu código, getx()retorna um objeto temporário, o chamado "rvalue". Você pode copiar rvalues ​​em objetos (também conhecidos como variáveis) ou vinculá-los a referências const (que prolongarão sua vida útil até o final da vida útil da referência). Você não pode vincular rvalues ​​a referências que não sejam const.

Esta foi uma decisão deliberada de design para impedir que os usuários modifiquem acidentalmente um objeto que morrerá no final da expressão:

g(getx()); // g() would modify an object without anyone being able to observe

Se você quiser fazer isso, precisará primeiro fazer uma cópia local ou do objeto ou vinculá-lo a uma referência const:

X x1 = getx();
const X& x2 = getx(); // extend lifetime of temporary to lifetime of const reference

g(x1); // fine
g(x2); // can't bind a const reference to a non-const reference

Observe que o próximo padrão C ++ incluirá referências rvalue. O que você conhece como referências está, portanto, se tornando chamado "referências de valor". Você poderá vincular rvalues ​​a referências de rvalue e poderá sobrecarregar funções em "rvalue-ness":

void g(X&);   // #1, takes an ordinary (lvalue) reference
void g(X&&);  // #2, takes an rvalue reference

X x; 
g(x);      // calls #1
g(getx()); // calls #2
g(X());    // calls #2, too

A idéia por trás das referências rvalue é que, como esses objetos vão morrer de qualquer maneira, você pode tirar proveito desse conhecimento e implementar o que é chamado de "mover semântica", um certo tipo de otimização:

class X {
  X(X&& rhs)
    : pimpl( rhs.pimpl ) // steal rhs' data...
  {
    rhs.pimpl = NULL; // ...and leave it empty, but deconstructible
  }

  data* pimpl; // you would use a smart ptr, of course
};


X x(getx()); // x will steal the rvalue's data, leaving the temporary object empty

2
Olá, esta é uma resposta incrível. Precisa saber uma coisa, a g(getx())não funciona porque sua assinatura é g(X& x)e get(x)retorna um objeto temporário, portanto não podemos vincular um objeto temporário ( rvalue ) a uma referência não constante, correto? E em sua primeira peça de código, eu acho que vai ser const X& x2 = getx();em vez de const X& x1 = getx();..
Sexy Beast

1
Obrigado por apontar esse bug na minha resposta 5 anos depois que eu o escrevi! :-/Sim, seu raciocínio está correto, embora um pouco atrasado: não podemos vincular temporários a constreferências que não sejam (lvalue) e, portanto, o temporário retornado por getx()(e não get(x)) não pode ser vinculado à referência lvalue à qual o argumento se refere g().
SBI

Umm, o que você quis dizer com getx()(e não get(x)) ?
SexyBeast

Quando escrevo "... getx () (e não get (x)) ..." , quero dizer que o nome da função é getx()e não get(x)(como você escreveu).
Sbi

Esta resposta mistura terminologia. Um rvalue é uma categoria de expressão. Um objeto temporário é um objeto. Um rvalue pode ou não denotar um objeto temporário; e um objeto temporário pode ou não ser indicado por um rvalue.
877 MM

16

O que você está mostrando é que o encadeamento do operador é permitido.

 X& x = getx().ref(); // OK

A expressão é 'getx (). Ref ();' e isso é executado até a conclusão antes da atribuição a 'x'.

Observe que getx () não retorna uma referência, mas um objeto totalmente formado no contexto local. O objeto é temporário, mas é não const, permitindo assim que você chamar outros métodos para calcular um valor ou ter outros efeitos colaterais acontecer.

// It would allow things like this.
getPipeline().procInstr(1).procInstr(2).procInstr(3);

// or more commonly
std::cout << getManiplator() << 5;

Veja o final desta resposta para um melhor exemplo disso.

Você não pode vincular um temporário a uma referência, pois isso gera uma referência a um objeto que será destruído no final da expressão, deixando assim uma referência pendente (que é desarrumada e o padrão não é desarrumado).

O valor retornado por ref () é uma referência válida, mas o método não presta atenção à vida útil do objeto que está retornando (porque não pode ter essas informações em seu contexto). Você basicamente fez o equivalente a:

x& = const_cast<x&>(getX());

O motivo de fazer isso com uma referência const a um objeto temporário é que o padrão estende a vida útil do temporário para a vida útil da referência, para que a vida útil dos objetos temporários seja estendida além do final da instrução.

Portanto, a única questão remanescente é por que o padrão não deseja permitir que a referência a temporários estenda a vida do objeto além do final da declaração?

Eu acredito que é porque isso tornaria muito difícil o compilador se corrigir para objetos temporários. Isso foi feito para referências const a temporários, pois isso tem uso limitado e, portanto, forçou você a fazer uma cópia do objeto para fazer algo útil, mas fornece algumas funcionalidades limitadas.

Pense nesta situação:

int getI() { return 5;}
int x& = getI();

x++; // Note x is an alias to a variable. What variable are you updating.

Estender a vida útil desse objeto temporário será muito confuso.
Enquanto o seguinte:

int const& y = getI();

Fornecerá o código que é intuitivo de usar e entender.

Se você deseja modificar o valor, retorne o valor a uma variável. Se você está tentando evitar o custo de copiar o objeto de volta da função (como parece que o objeto é uma cópia construída de volta (tecnicamente é)). Então não se preocupe, o compilador é muito bom em 'Return Value Optimization'


3
"Portanto, a única questão restante é por que o padrão não deseja permitir que a referência a temporários estenda a vida útil do objeto além do final da declaração?" É isso! Você entende minha pergunta. Mas eu discordo da sua opinião. Você diz "tornar o compilador muito difícil", mas foi feito para referência const. Você diz em sua amostra "Nota x é um alias para uma variável. Qual variável você está atualizando". Sem problemas. Existe uma variável única (temporária). Algum objeto temporário (igual a 5) deve ser alterado.
Alexey Malistov 14/10/09

@ Martin: Referências pendentes não são apenas desarrumadas. Eles podem levar a erros graves quando acessados ​​mais tarde no método!
mmmmmmmm

1
@ Alexey: Observe que o fato de vinculá-lo a uma referência const aumenta a vida útil de um temporário é uma exceção que foi adicionada deliberadamente (TTBOMK para permitir otimizações manuais). Não houve uma exceção adicionada para referências não-const, porque a ligação de uma referência temporária a uma não-const foi vista como provavelmente um erro de programador.
Sbi 14/10/2009

1
@ alexy: uma referência a uma variável invisível! Não é tão intuitivo.
Martin Iorque

const_cast<x&>(getX());Não faz sentido
curiousguy

10

Por que é discutido nas perguntas frequentes sobre C ++ (mina de negrito ):

No C ++, as referências não const podem ser vinculadas a lvalues ​​e as referências const podem ser vinculadas a lvalues ​​ou rvalues, mas não há nada que possa ser vinculado a um valor não constante. Isso é para proteger as pessoas de alterar os valores dos temporários que são destruídos antes que seu novo valor possa ser usado . Por exemplo:

void incr(int& a) { ++a; }
int i = 0;
incr(i);    // i becomes 1
incr(0);    // error: 0 is not an lvalue

Se esse incr (0) fosse permitido, algum temporário que ninguém nunca viu seria incrementado ou - muito pior - o valor de 0 se tornaria 1. O último parece bobo, mas na verdade havia um bug como esse nos primeiros compiladores Fortran que definiam de lado um local de memória para armazenar o valor 0.


Teria sido engraçado ver o rosto do programador que foi mordido pelo "zero bug" do Fortran! x * 0 x? O que? O que??
John D

Esse último argumento é particularmente fraco. Nenhum compilador que vale a pena mencionar jamais alteraria o valor de 0 para 1, ou mesmo interpretaria incr(0);dessa maneira. Obviamente, se isso fosse permitido, seria interpretada como um inteiro temporária e passá-lo paraincr()
user3204459

6

A questão principal é que

g(getx()); //error

é um erro lógico: gestá modificando o resultado, getx()mas você não tem chance de examinar o objeto modificado. Se gnão fosse necessário modificar seu parâmetro, ele não exigiria uma referência lvalue, poderia ter tomado o parâmetro por valor ou por referência const.

const X& x = getx(); // OK

é válido porque às vezes você precisa reutilizar o resultado de uma expressão e é bem claro que você está lidando com um objeto temporário.

No entanto, não é possível fazer

X& x = getx(); // error

válido sem g(getx())validar, que é o que os designers de linguagem estavam tentando evitar em primeiro lugar.

g(getx().ref()); //OK

é válido porque os métodos sabem apenas sobre a consistência do this, eles não sabem se são chamados em um lvalue ou em um rvalue.

Como sempre em C ++, você tem uma solução alternativa para essa regra, mas precisa sinalizar ao compilador que sabe o que está fazendo, sendo explícito:

g(const_cast<x&>(getX()));

5

Parece que a pergunta original sobre por que isso não é permitido foi respondida claramente: "porque provavelmente é um erro".

FWIW, pensei em mostrar como isso poderia ser feito, mesmo que eu não ache que seja uma boa técnica.

Às vezes, eu quero passar um método temporário para um método usando uma referência não-const, descartando intencionalmente um valor retornado por referência que o método de chamada não se importa. Algo assim:

// Assuming: void Person::GetNameAndAddr(std::string &name, std::string &addr);
string name;
person.GetNameAndAddr(name, string()); // don't care about addr

Conforme explicado nas respostas anteriores, isso não compila. Mas isso compila e funciona corretamente (com meu compilador):

person.GetNameAndAddr(name,
    const_cast<string &>(static_cast<const string &>(string())));

Isso apenas mostra que você pode usar a conversão para mentir para o compilador. Obviamente, seria muito mais limpo declarar e passar uma variável automática não utilizada:

string name;
string unused;
person.GetNameAndAddr(name, unused); // don't care about addr

Essa técnica introduz uma variável local desnecessária no escopo do método. Se, por algum motivo, você deseja impedir que ele seja usado posteriormente no método, por exemplo, para evitar confusão ou erro, você pode ocultá-lo em um bloco local:

string name;
{
    string unused;
    person.GetNameAndAddr(name, unused); // don't care about addr
}

- Chris


4

Por que você iria querer X& x = getx();? Basta usar X x = getx();e confiar no RVO.


3
Porque eu quero ligar em g(getx())vez de #g(getx().ref())
Alexey Malistov 14/10/09

4
@ Alexey, essa não é uma razão real. Se você fizer isso, terá um erro lógico em qualquer lugar, porque gmodificará algo que você não pode mais colocar em suas mãos.
Johannes Schaub - litb 14/10/09

3
@ JohannesSchaub-litb talvez ele não se importe.
precisa

" confiar no RVO ", exceto que não é chamado "RVO".
curiousguy

2
@curiousguy: Esse é um termo muito aceito para isso. Não há absolutamente nada de errado em se referir a ele como "RVO".
Filhote de cachorro


3

Excelente pergunta, e aqui está minha tentativa de uma resposta mais concisa (já que muitas informações úteis estão nos comentários e são difíceis de descobrir no barulho).

Qualquer referência vinculada diretamente a um temporário prolongará sua vida útil [12.2.5]. Por outro lado, uma referência inicializada com outra referência não será (mesmo que seja finalmente a mesma temporária). Isso faz sentido (o compilador não sabe a que referência se refere).

Mas toda essa idéia é extremamente confusa. Por exemplo const X &x = X();, fará o temporário durar contanto que a xreferência, mas const X &x = X().ref();NÃO (quem sabe o que ref()realmente retornou). Neste último caso, o destruidor de Xé chamado no final desta linha. (Isso é observável com um destruidor não trivial.)

Portanto, geralmente parece confuso e perigoso (por que complicar as regras sobre a vida útil do objeto?), Mas presumivelmente havia uma necessidade pelo menos de referências const, portanto o padrão define esse comportamento para eles.

[Do comentário do sbi ]: Observe que o fato de vinculá- lo a uma referência const aumenta a vida útil de um temporário é uma exceção que foi adicionada deliberadamente (TTBOMK para permitir otimizações manuais). Não houve uma exceção adicionada para referências não-const, porque a ligação de uma referência temporária a uma não-const foi vista como provavelmente um erro de programador.

Todos os temporários persistem até o final da expressão completa. Para usá-los, no entanto, você precisa de um truque como o seu ref(). Isso é legal. Não parece haver uma boa razão para o aro extra, exceto para lembrar ao programador que algo incomum está acontecendo (a saber, um parâmetro de referência cujas modificações serão perdidas rapidamente).

[Outro comentário do sbi ] A razão pela qual Stroustrup dá (em D&E) a proibição de vincular rvalues ​​a referências não-const é que, se o g () de Alexey modificasse o objeto (o que você esperaria de uma função usando um não-const) referência), modificaria um objeto que morrerá, para que ninguém pudesse obter o valor modificado de qualquer maneira. Ele diz que isso, provavelmente, é um erro.


2

"Está claro que o objeto temporário não é constante na amostra acima, porque chamadas para funções não constantes são permitidas. Por exemplo, ref () pode modificar o objeto temporário."

No seu exemplo, getX () não retorna uma const X, portanto você pode chamar ref () da mesma maneira que poderia chamar X (). Ref (). Você está retornando uma referência não const e, portanto, pode chamar métodos não const, o que você não pode fazer é atribuir a referência a uma referência não const.

Juntamente com o comentário do SadSidos, isso torna seus três pontos incorretos.


1

Tenho um cenário que gostaria de compartilhar onde gostaria de poder fazer o que Alexey está pedindo. Em um plug-in do Maya C ++, eu tenho que fazer o seguinte shenanigan para obter um valor em um atributo de nó:

MFnDoubleArrayData myArrayData;
MObject myArrayObj = myArrayData.create(myArray);   
MPlug myPlug = myNode.findPlug(attributeName);
myPlug.setValue(myArrayObj);

Isso é entediante de escrever, por isso escrevi as seguintes funções auxiliares:

MPlug operator | (MFnDependencyNode& node, MObject& attribute){
    MStatus status;
    MPlug returnValue = node.findPlug(attribute, &status);
    return returnValue;
}

void operator << (MPlug& plug, MDoubleArray& doubleArray){
    MStatus status;
    MFnDoubleArrayData doubleArrayData;
    MObject doubleArrayObject = doubleArrayData.create(doubleArray, &status);
    status = plug.setValue(doubleArrayObject);
}

E agora eu posso escrever o código desde o início do post como:

(myNode | attributeName) << myArray;

O problema é que ele não é compilado fora do Visual C ++, porque está tentando vincular a variável temporária retornada do | operador à referência MPlug do operador <<. Eu gostaria que fosse uma referência, porque esse código é chamado muitas vezes e eu prefiro que o MPlug não seja copiado tanto. Eu só preciso que o objeto temporário viva até o final da segunda função.

Bem, este é o meu cenário. Apenas pensei em mostrar um exemplo em que alguém gostaria de fazer o que Alexey descreve. Congratulo-me com todas as críticas e sugestões!

Obrigado.

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.