O que constitui um estado válido para um object “movido de” no C ++ 11?

Eu tenho tentado entender como a semântica de movimento em C ++ 11 deve funcionar, e estou tendo problemas para entender quais condições um object movido-a-object precisa satisfazer. Olhando para a resposta aqui realmente não resolve a minha pergunta, porque não consigo ver como aplicá-lo aos objects pimpl de uma maneira sensata, apesar de argumentos que movem a semântica são perfeitos para pimpls .

A ilustração mais fácil do meu problema envolve o idioma pimpl, assim:

class Foo { std::unique_ptr impl_; public: // Inlining FooImpl's constructors for brevity's sake; otherwise it // defeats the point. Foo() : impl_(new FooImpl()) {} Foo(const Foo & rhs) : impl_(new FooImpl(*rhs.impl_)) {} Foo(Foo && rhs) : impl_(std::move(rhs.impl_)) {} Foo & operator=(Foo rhs) { std::swap(impl_, rhs.impl_); return *this; } void do_stuff () { impl_->do_stuff; } }; 

Agora, o que posso fazer depois de me mudar de um Foo ? Eu posso destruir o object movido de forma segura, e posso atribuir a ele, ambos os quais são absolutamente cruciais. No entanto, se eu tentar fazer do_stuff com o meu Foo , ele vai explodir. Antes de adicionar a semântica de movimento para minha definição de Foo , cada Foo satisfazia a invariante que poderia do_stuff – e isso não é mais o caso. Também não parece haver muitas alternativas excelentes, uma vez que (por exemplo) colocar o Foo movido de envolveria uma nova alocação dinâmica, o que parcialmente anula o propósito de mover a semântica. Eu poderia verificar se impl_ no do_stuff e inicializá-lo para um padrão FooImpl se for, mas isso adiciona uma verificação (geralmente espúrio), e se eu tiver muitos methods, isso significaria lembrar de fazer o check-in em cada um.

Devo desistir da idéia de que ser capaz de fazer do_stuff é uma invariante razoável?

Você define e documenta para seus tipos o que é um estado ‘válido’ e qual operação pode ser executada em objects movidos de seus tipos.

Mover um object de um tipo de biblioteca padrão coloca o object em um estado não especificado, que pode ser consultado como normal para determinar operações válidas.

17.6.5.15 Estado de tipos de biblioteca movidos [lib.types.movedfrom]

Objetos de tipos definidos na biblioteca padrão C ++ podem ser movidos de (12.8). As operações de movimentação podem ser explicitamente especificadas ou implicitamente geradas. A menos que especificado de outra forma, esses objects movidos de devem ser colocados em um estado válido, mas não especificado.

O object que está em um estado ‘válido’ significa que todos os requisitos que o padrão especifica para o tipo ainda são verdadeiros. Isso significa que você pode usar qualquer operação em um tipo de biblioteca padrão movido a partir do qual as condições prévias são verdadeiras.

Normalmente, o estado de um object é conhecido, portanto, você não precisa verificar se ele atende às condições prévias para cada operação que você deseja executar. A única diferença com objects movidos é que você não conhece o estado, então você precisa verificar. Por exemplo, você não deve pop_back () em uma string movida de até que tenha consultado o estado da string para determinar se as condições prévias de pop_back () foram atendidas.

 std::string s = "foo"; std::string t(std::move(s)); if (!s.empty()) // empty has no preconditions, so it's safe to call on moved-from objects s.pop_back(); // after verifying that the preconditions are met, pop_back is safe to call on moved-from objects 

O estado provavelmente não é especificado porque seria oneroso criar um único conjunto útil de requisitos para todas as implementações diferentes da biblioteca padrão.


Como você é responsável não apenas pela especificação, mas também pela implementação de seus tipos, você pode simplesmente especificar o estado e evitar a necessidade de consulta. Por exemplo, seria perfeitamente razoável especificar que a mudança do seu object tipo pimpl faz com que o do_stuff se torne uma operação inválida com comportamento indefinido (via dereferencing um ponteiro nulo). A linguagem é projetada de modo que o movimento ocorra somente quando não é possível fazer nada ao object movido, ou quando o usuário indicar de forma muito óbvia e muito explícita uma operação de movimentação, portanto, um usuário nunca deve ser surpreendido por um movimento. do object.


Observe também que os “conceitos” definidos pela biblioteca padrão não fazem concessões para objects movidos de. Isso significa que, para atender aos requisitos de qualquer um dos conceitos definidos pela biblioteca padrão, os objects movidos de seus tipos ainda devem atender aos requisitos do conceito. Isso significa que, se os objects do seu tipo não permanecerem em um estado válido (conforme definido pelo conceito relevante), você não poderá usá-lo com a biblioteca padrão (ou o resultado será um comportamento indefinido).

No entanto, se eu tentar fazer coisas com o meu Foo, ele vai explodir.

Sim. Então vai isso:

 vector first = {3, 5, 6}; vector second = std::move(first); first.size(); //Value returned is undefined. May be 0, may not 

A regra usada pelo padrão é deixar o object em um estado válido (o que significa que o object funciona), mas não especificado . Isso significa que as únicas funções que você pode chamar são aquelas que não têm condições no estado atual do object. Para vector , você pode usar seus operadores de atribuição de cópia / movimentação, bem como operações clear e empty , além de várias outras operações. Então você pode fazer isso:

 vector first = {3, 5, 6}; vector second = std::move(first); first.clear(); //Cause the vector to become empty. first.size(); //Now the value is guaranteed to be 0. 

Para o seu caso, copiar / mover a atribuição (de ambos os lados) ainda deve funcionar, assim como o destruidor. Mas todas as outras funções de vocês têm uma pré-condição baseada no estado de não ter sido movido.

Então eu não vejo o seu problema.

Se você quisesse garantir que nenhuma instância de uma class Pimpl poderia estar vazia, você implementaria a semântica de cópia apropriada e proibiria a movimentação. Movimento requer a possibilidade de um object estar em um estado vazio.