Mover semântica em C ++ - Mover-retornar de variáveis ​​locais

10

Meu entendimento é que no C ++ 11, quando você retorna uma variável local de uma função por valor, o compilador pode tratar essa variável como uma referência de valor r e 'movê-la para fora da função para retorná-la (se O RVO / NRVO não acontece, é claro).

Minha pergunta é: isso não pode quebrar o código existente?

Considere o seguinte código:

#include <iostream>
#include <string>

struct bar
{
  bar(const std::string& str) : _str(str) {}
  bar(const bar&) = delete;
  bar(bar&& other) : _str(std::move(other._str)) {other._str = "Stolen";}
  void print() {std::cout << _str << std::endl;}

  std::string _str;
};

struct foo
{
  foo(bar& b) : _b(b) {}
  ~foo() {_b.print();}

  bar& _b;
};

bar foobar()
{
  bar b("Hello, World!");
  foo f(b);

  return std::move(b);
}

int main()
{
  foobar();
  return EXIT_SUCCESS;
}

Eu pensava que seria possível para um destruidor de um objeto local fazer referência ao objeto que é implicitamente movido e, portanto, inesperadamente ver um objeto 'vazio'. Eu tentei testar isso (ver http://ideone.com/ZURoeT ), mas eu tenho o resultado 'correta' sem a explícita std::moveno foobar(). Suponho que isso se deva ao NRVO, mas não tentei reorganizar o código para desativá-lo.

Estou certo de que essa transformação (causando uma saída da função) ocorre implicitamente e pode quebrar o código existente?

ATUALIZAÇÃO Aqui está um exemplo que ilustra o que estou falando. Os dois links a seguir são para o mesmo código. http://ideone.com/4GFIRu - C ++ 03 http://ideone.com/FcL2Xj - C ++ 11

Se você olhar para a saída, é diferente.

Então, acho que agora essa pergunta se torna: foi considerada ao adicionar uma mudança implícita ao padrão e foi decidido que não havia problema em adicionar essa alteração, pois esse tipo de código é raro o suficiente? Gostaria também de saber se algum compilador irá avisar em casos como este ...

Bwmat
fonte
Uma movimentação deve sempre deixar o objeto em um estado destrutível.
Zan Lynx
Sim, mas essa não é a questão. O código anterior à c ++ 11 poderia assumir que o valor de uma variável local não mudaria simplesmente devido ao retorno da mesma; portanto, esse movimento implícito poderia quebrar essa suposição.
Bwmat # 7/14
Foi isso que tentei elucidar no meu exemplo; via destruidores, você pode inspecionar o estado de (um subconjunto de) variáveis ​​locais de uma função 'após' a instrução return ser executada, mas antes que a função realmente retorne.
Bwmat
Essa é uma excelente pergunta com o exemplo que você adicionou. Espero que isso obtenha mais respostas de profissionais que possam elucidar isso. O único feedback real que posso dar é: é por isso que os objetos geralmente não devem ter visualizações não proprietárias dos dados. Na verdade, existem muitas maneiras de escrever código de aparência inocente que segfaults quando você fornece objetos que não possuem visões (ponteiros ou referências brutos). Posso elaborar isso em uma resposta adequada, se você quiser, mas acho que não é disso que você realmente quer ouvir falar. E já é sabido que o 11 pode quebrar o código existente, por exemplo, definindo novas palavras-chave.
Nir Friedman
Sim, eu sei que C ++ 11 nunca reivindicou para não quebrar qualquer código antigo, mas isso é muito sutil, e seria muito fácil perder (sem erros de compilador, avisos segfaults)
Bwmat

Respostas:

8

Scott Meyers postou no comp.lang.c ++ (agosto de 2010) sobre um problema em que a geração implícita de construtores de movimentação poderia quebrar os invariantes da classe C ++ 03:

struct X
{
  // invariant: v.size() == 5
  X() : v(5) {}

  ~X() { std::cout << v[0] << std::endl; }

private:    
  std::vector<int> v;
};

int main()
{
    std::vector<X> y;
    y.push_back(X()); // X() rvalue: copied in C++03, moved in C++0x
}

Aqui o problema é que no C ++ 03, Xhavia um invariante de que seu vmembro sempre teve 5 elementos. X::~X()contava com esse invariante, mas o construtor de movimentação recém-introduzido saiu de v, definindo seu comprimento para zero.

Isso está relacionado ao seu exemplo, pois a invariante quebrada é detectada apenas no Xdestruidor do (como você diz que é possível que um destruidor de um objeto local faça referência ao objeto que é movido implicitamente e, portanto, inesperadamente vê um objeto vazio ).

O C ++ 11 tenta alcançar um equilíbrio entre quebrar parte do código existente e fornecer otimizações úteis com base em construtores de movimentação.

O comitê decidiu inicialmente que os construtores de movimentação e os operadores de atribuição de movimentação deveriam ser gerados pelo compilador quando não fornecidos pelo usuário.

Decidiu então que isso realmente era motivo de alarme e restringiu a geração automática de construtores de movimentação e operadores de atribuição de movimentação de tal maneira que é muito menos provável, embora não impossível, que o código existente seja quebrado (por exemplo, destruidor explicitamente definido).

É tentador pensar que impedir a geração de construtores de movimentação implícitos quando um destruidor definido pelo usuário está presente é suficiente, mas não é verdade ( N3153 - Movimento implícito deve ir para obter mais detalhes).

No N3174 - Mover ou não Mover Stroupstrup diz:

Considero isso um problema de design de linguagem, em vez de um simples problema de compatibilidade com versões anteriores. É fácil evitar a quebra de código antigo (por exemplo, basta remover as operações de movimentação do C ++ 0x), mas vejo que o C ++ 0x é uma linguagem melhor, tornando as operações de movimentação difundidas um objetivo principal para o qual pode valer a pena quebrar alguns C + Código +98.

manlio
fonte