Minha primeira resposta foi uma introdução extremamente simplificada para mover a semântica, e muitos detalhes foram deixados de propósito para mantê-la simples. No entanto, há muito mais para mover a semântica, e pensei que era hora de uma segunda resposta para preencher as lacunas. A primeira resposta já é bastante antiga e não parecia correto substituí-la por um texto completamente diferente. Eu acho que ainda serve bem como uma primeira introdução. Mas se você quiser aprofundar, continue lendo :)
Stephan T. Lavavej dedicou um tempo para fornecer feedback valioso. Muito obrigado, Stephan!
Introdução
A semântica de movimentação permite que um objeto, sob certas condições, aproprie-se dos recursos externos de algum outro objeto. Isso é importante de duas maneiras:
Transformando cópias caras em movimentos baratos. Veja minha primeira resposta para um exemplo. Observe que, se um objeto não gerenciar pelo menos um recurso externo (direta ou indiretamente através de seus objetos membros), a semântica de movimentação não oferecerá nenhuma vantagem sobre a semântica de cópia. Nesse caso, copiar e mover um objeto significa exatamente a mesma coisa:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
Implementando tipos seguros de "somente movimento"; isto é, tipos para os quais copiar não faz sentido, mas mover faz. Os exemplos incluem bloqueios, identificadores de arquivo e ponteiros inteligentes com semântica de propriedade exclusiva. Nota: Esta resposta discute std::auto_ptr
um modelo de biblioteca padrão C ++ 98 descontinuado, que foi substituído por std::unique_ptr
C ++ 11. Os programadores intermediários de C ++ provavelmente estão pelo menos um pouco familiarizados e std::auto_ptr
, devido à "semântica de movimento" exibida, parece um bom ponto de partida para discutir a semântica de movimento no C ++ 11. YMMV.
O que é uma jogada?
A biblioteca padrão C ++ 98 oferece um ponteiro inteligente com semântica de propriedade exclusiva chamada std::auto_ptr<T>
. Caso você não esteja familiarizado auto_ptr
, seu objetivo é garantir que um objeto alocado dinamicamente seja sempre liberado, mesmo diante de exceções:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
O que auto_ptr
é incomum é o seu comportamento de "cópia":
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Note como a inicialização b
com a
que não copiar o triângulo, mas em vez transfere a propriedade do triângulo a partir a
de b
. Também dizemos " a
é movido para b
" ou "o triângulo é movido de a
para b
". Isso pode parecer confuso, porque o próprio triângulo sempre permanece no mesmo local da memória.
Mover um objeto significa transferir a propriedade de algum recurso que ele gerencia para outro objeto.
O construtor de cópia de auto_ptr
provavelmente se parece com isso (um pouco simplificado):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
Movimentos perigosos e inofensivos
O mais perigoso auto_ptr
é que o que sintaticamente se parece com uma cópia é realmente uma mudança. Tentar chamar uma função de membro em uma mudança de auto_ptr
invocará um comportamento indefinido, portanto, você deve ter muito cuidado para não usar uma auto_ptr
após a mudança de:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Mas auto_ptr
nem sempre é perigoso. As funções de fábrica são um caso de uso perfeito para auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Observe como os dois exemplos seguem o mesmo padrão sintático:
auto_ptr<Shape> variable(expression);
double area = expression->area();
E, no entanto, um deles invoca um comportamento indefinido, enquanto o outro não. Então, qual é a diferença entre as expressões a
e make_triangle()
? Eles não são do mesmo tipo? De fato são, mas têm diferentes categorias de valor .
Categorias de valor
Obviamente, deve haver alguma diferença profunda entre a expressão a
que denota uma auto_ptr
variável e a expressão make_triangle()
que denota a chamada de uma função que retorna um auto_ptr
valor por, criando assim um novo auto_ptr
objeto temporário toda vez que é chamado. a
é um exemplo de um lvalue , enquanto que make_triangle()
é um exemplo de um rvalue .
Passar de lvalues como a
é perigoso, porque mais tarde poderíamos tentar chamar uma função de membro a
, invocando um comportamento indefinido. Por outro lado, passar de rvalores como make_triangle()
é perfeitamente seguro, porque depois que o construtor de cópias fez seu trabalho, não podemos usar o temporário novamente. Não há expressão que denuncie o referido temporário; se simplesmente escrevermos make_triangle()
novamente, obteremos um temporário diferente . De fato, o temporário movido de já foi para a próxima linha:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Observe que as letras l
e r
têm uma origem histórica no lado esquerdo e no lado direito de uma tarefa. Isso não é mais verdade no C ++, porque existem lvalues que não podem aparecer no lado esquerdo de uma atribuição (como matrizes ou tipos definidos pelo usuário sem um operador de atribuição) e existem rvalues que podem (todos os rvalues dos tipos de classe com um operador de atribuição).
Um rvalue do tipo de classe é uma expressão cuja avaliação cria um objeto temporário. Sob circunstâncias normais, nenhuma outra expressão dentro do mesmo escopo indica o mesmo objeto temporário.
Referências de valor
Agora entendemos que mudar de valores é potencialmente perigoso, mas mudar de valores é inofensivo. Se o C ++ tivesse suporte de linguagem para distinguir argumentos de lvalue de argumentos de rvalue, poderíamos proibir completamente a mudança de lvalues ou, pelo menos, explicitar a mudança de lvalues no site da chamada, para que não movamos mais por acidente.
A resposta do C ++ 11 para esse problema são as referências de rvalue . Uma referência rvalue é um novo tipo de referência que se liga apenas a rvalues, e a sintaxe é X&&
. A boa e antiga referência X&
agora é conhecida como referência lvalue . (Observe que nãoX&&
é uma referência a uma referência; não existe tal coisa em C ++.)
Se jogarmos const
na mistura, já temos quatro tipos diferentes de referências. A que tipos de expressões do tipo X
eles podem se ligar?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
Na prática, você pode esquecer const X&&
. Ser restrito à leitura de rvalues não é muito útil.
Uma referência rvalue X&&
é um novo tipo de referência que se liga apenas a rvalues.
Conversões implícitas
As referências de valor passaram por várias versões. Desde a versão 2.1, uma referência rvalue X&&
também se liga a todas as categorias de valor de um tipo diferente Y
, desde que haja uma conversão implícita de Y
para X
. Nesse caso, um temporário do tipo X
é criado e a referência rvalue é vinculada a esse temporário:
void some_function(std::string&& r);
some_function("hello world");
No exemplo acima, "hello world"
é um tipo de Ivalue const char[12]
. Como há uma conversão implícita de const char[12]
até const char*
para std::string
, um tipo temporário std::string
é criado er
é ligada a esse temporária. Este é um dos casos em que a distinção entre rvalues (expressões) e temporários (objetos) é um pouco embaçada.
Mover construtores
Um exemplo útil de uma função com um X&&
parâmetro é o construtor move X::X(X&& source)
. Seu objetivo é transferir a propriedade do recurso gerenciado da origem para o objeto atual.
No C ++ 11, std::auto_ptr<T>
foi substituído pelo std::unique_ptr<T>
que tira proveito das referências de rvalue. Vou desenvolver e discutir uma versão simplificada do unique_ptr
. Primeiro, encapsulamos um ponteiro bruto e sobrecarregamos os operadores ->
e *
, portanto, nossa classe parece um ponteiro:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
O construtor assume a propriedade do objeto e o destruidor o exclui:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Agora vem a parte interessante, o construtor de movimentação:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Esse construtor de movimentação faz exatamente o que o auto_ptr
construtor de cópia fez, mas só pode ser fornecido com rvalues:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
A segunda linha falha ao compilar, porque a
é um lvalue, mas o parâmetro unique_ptr&& source
só pode ser vinculado a rvalues. Isso é exatamente o que queríamos; movimentos perigosos nunca devem estar implícitos. A terceira linha compila muito bem, porque make_triangle()
é um rvalue. O construtor de movimentação transferirá a propriedade do temporário para c
. Novamente, é exatamente isso que queríamos.
O construtor de movimentação transfere a propriedade de um recurso gerenciado para o objeto atual.
Mover operadores de atribuição
A última peça que falta é o operador de atribuição de movimento. Seu trabalho é liberar o recurso antigo e adquirir o novo recurso a partir de seu argumento:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Observe como essa implementação do operador de atribuição de movimentação duplica a lógica do destruidor e do construtor de movimentação. Você está familiarizado com o idioma de copiar e trocar? Também pode ser aplicado para mover a semântica como o idioma mover-e-trocar:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Agora que source
é uma variável do tipo unique_ptr
, será inicializada pelo construtor move; isto é, o argumento será movido para o parâmetro O argumento ainda é necessário para ser um rvalue, porque o próprio construtor move possui um parâmetro de referência rvalue. Quando o fluxo de controle atinge a chave de fechamento de operator=
, source
sai do escopo, liberando o recurso antigo automaticamente.
O operador de atribuição de movimentação transfere a propriedade de um recurso gerenciado para o objeto atual, liberando o recurso antigo. O idioma de movimentação e troca simplifica a implementação.
Movendo-se de lvalues
Às vezes, queremos passar de lvalues. Ou seja, às vezes queremos que o compilador trate um lvalue como se fosse um rvalue, para que ele possa chamar o construtor move, mesmo que possa ser potencialmente inseguro. Para esse propósito, o C ++ 11 oferece um modelo de função de biblioteca padrão chamado std::move
dentro do cabeçalho <utility>
. Esse nome é um pouco infeliz, porque std::move
simplesmente lança um valor lvalue para um valor rvalue; ele não mover qualquer coisa por si só. Apenas permite o movimento. Talvez devesse ter sido nomeado std::cast_to_rvalue
ou std::enable_move
, mas já estamos presos ao nome.
Aqui está como você se move explicitamente de um lvalue:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Observe que após a terceira linha, a
não possui mais um triângulo. Tudo bem, porque, ao escrever explicitamentestd::move(a)
, deixamos claras nossas intenções: "Caro construtor, faça o que quiser com a a
fim de inicializar c
; não me importo a
mais. Sinta-se à vontade para seguir em frente a
".
std::move(some_lvalue)
lança um lvalue para um rvalue, permitindo assim um movimento subsequente.
Xvalues
Observe que, embora std::move(a)
seja um rvalue, sua avaliação não cria um objeto temporário. Esse dilema forçou o comitê a introduzir uma terceira categoria de valor. Algo que pode ser vinculado a uma referência rvalue, mesmo que não seja um rvalue no sentido tradicional, é chamado de xvalue (valor eXpiring). Os valores tradicionais foram renomeados para valores prévios (valores puros).
Prvalues e xvalues são rvalues. Xvalues e lvalues são ambos glvalues (lvalues generalizados). Os relacionamentos são mais fáceis de entender com um diagrama:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Observe que apenas xvalues são realmente novos; o resto é apenas devido à renomeação e agrupamento.
Os valores C ++ 98 são conhecidos como valores prévios no C ++ 11. Substitua mentalmente todas as ocorrências de "rvalue" nos parágrafos anteriores por "prvalue".
Saindo de funções
Até agora, vimos movimento em variáveis locais e em parâmetros de função. Mas o movimento também é possível na direção oposta. Se uma função retornar por valor, algum objeto no site de chamada (provavelmente uma variável local ou temporária, mas poderia ser qualquer tipo de objeto) será inicializado com a expressão após a return
instrução como um argumento para o construtor de movimentação:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Talvez surpreendentemente, objetos automáticos (variáveis locais que não são declaradas como static
) também podem ser implicitamente removidos de funções:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Como o construtor move aceita o lvalue result
como argumento? O escopo de result
está prestes a terminar e será destruído durante o desenrolamento da pilha. Ninguém poderia reclamar depois que isso result
mudou de alguma maneira; quando o fluxo de controle retorna ao chamador, result
ele não existe mais! Por esse motivo, o C ++ 11 possui uma regra especial que permite retornar objetos automáticos das funções sem precisar escrever std::move
. Na verdade, você nunca deve usar std::move
para mover objetos automáticos para fora das funções, pois isso inibe a "otimização do valor de retorno nomeado" (NRVO).
Nunca use std::move
para mover objetos automáticos para fora das funções.
Observe que, em ambas as funções de fábrica, o tipo de retorno é um valor, não uma referência de valor nominal. As referências de valor ainda são referências e, como sempre, você nunca deve retornar uma referência a um objeto automático; o chamador acabaria com uma referência pendente se você enganasse o compilador a aceitar seu código, assim:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Nunca retorne objetos automáticos por referência rvalue. A movimentação é realizada exclusivamente pelo construtor da movimentação, não por std::move
e não apenas vinculando um rvalue a uma referência de rvalue.
Mudando para membros
Mais cedo ou mais tarde, você escreverá um código como este:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
Basicamente, o compilador reclamará que parameter
é um valor l. Se você observar seu tipo, verá uma referência rvalue, mas uma referência rvalue significa simplesmente "uma referência vinculada a um rvalue"; isso não significa que a própria referência seja um rvalue! De fato, parameter
é apenas uma variável comum com um nome. Você pode usar parameter
quantas vezes quiser dentro do corpo do construtor, e sempre indica o mesmo objeto. Mover-se implicitamente seria perigoso, por isso a linguagem o proíbe.
Uma referência nomeada rvalue é um lvalue, assim como qualquer outra variável.
A solução é ativar manualmente a movimentação:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Você poderia argumentar que parameter
não é mais usado após a inicialização do member
. Por que não existe uma regra especial para inserir silenciosamente, std::move
assim como nos valores de retorno? Provavelmente porque seria muito pesado para os implementadores do compilador. Por exemplo, e se o corpo do construtor estivesse em outra unidade de tradução? Por outro lado, a regra do valor de retorno simplesmente precisa verificar as tabelas de símbolos para determinar se o identificador após a return
palavra - chave indica um objeto automático.
Você também pode passar o parameter
valor por. Para tipos somente de movimentação unique_ptr
, parece que ainda não existe um idioma estabelecido. Pessoalmente, prefiro passar por valor, pois causa menos confusão na interface.
Funções-membro especiais
O C ++ 98 declara implicitamente três funções-membro especiais sob demanda, ou seja, quando são necessárias em algum lugar: o construtor de cópia, o operador de atribuição de cópia e o destruidor.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
As referências de valor passaram por várias versões. Desde a versão 3.0, o C ++ 11 declara duas funções-membro especiais adicionais sob demanda: o construtor de movimentação e o operador de atribuição de movimentação. Observe que nem o VC10 nem o VC11 estão em conformidade com a versão 3.0, portanto, você precisará implementá-los você mesmo.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Essas duas novas funções especiais de membro são declaradas implicitamente apenas se nenhuma das funções especiais de membro for declarada manualmente. Além disso, se você declarar seu próprio construtor de movimentação ou operador de atribuição de movimentação, nem o construtor de cópia nem o operador de atribuição de cópia serão declarados implicitamente.
O que essas regras significam na prática?
Se você escrever uma classe sem recursos não gerenciados, não há necessidade de declarar qualquer uma das cinco funções especiais de membro e obterá a semântica correta da cópia e moverá a semântica gratuitamente. Caso contrário, você precisará implementar as funções especiais de membro. Obviamente, se sua classe não se beneficiar da semântica de movimentação, não há necessidade de implementar operações de movimentação especiais.
Observe que o operador de atribuição de cópia e o operador de atribuição de movimentação podem ser fundidos em um único operador de atribuição unificado, assumindo seu argumento por valor:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
Dessa forma, o número de funções-membro especiais a serem implementadas cai de cinco para quatro. Há uma troca entre segurança e eficiência de exceção aqui, mas não sou especialista neste assunto.
Referências de encaminhamento ( anteriormente conhecidas como referências universais )
Considere o seguinte modelo de função:
template<typename T>
void foo(T&&);
Você pode esperar T&&
vincular apenas a rvalues, porque, à primeira vista, parece uma referência a rvalue. No entanto, T&&
também se liga a lvalues:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Se o argumento é um rvalor do tipo X
, T
é deduzido como sendo X
, portanto T&&
significa X&&
. Isto é o que alguém esperaria. Mas se o argumento for um valor l de tipo X
, devido a uma regra especial, T
for deduzido como sendo X&
, isso T&&
significaria algo parecido X& &&
. Mas desde C ++ ainda não tem noção de referências a referências, o tipo X& &&
está em colapso em X&
. Isso pode parecer confuso e inútil no começo, mas o recolhimento de referência é essencial para o encaminhamento perfeito (que não será discutido aqui).
T&& não é uma referência de valor, mas uma referência de encaminhamento. Também se liga a lvalues, nesse caso T
e T&&
são ambas referências a lvalue.
Se você deseja restringir um modelo de função a rvalues, é possível combinar SFINAE com características de tipo:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Implementação de mudança
Agora que você entende o recolhimento de referência, eis como std::move
é implementado:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Como você pode ver, move
aceita qualquer tipo de parâmetro graças à referência de encaminhamento T&&
e retorna uma referência de rvalue. A std::remove_reference<T>::type
chamada de meta-função é necessária porque, caso contrário, para lvalues do tipo X
, o tipo de retorno seria o X& &&
qual entraria em colapso X&
. Como t
sempre é um lvalue (lembre-se de que uma referência nomeada rvalue é um lvalue), mas queremos ligar t
a uma referência rvalue, precisamos converter explicitamente t
no tipo de retorno correto. A chamada de uma função que retorna uma referência rvalue é em si um xvalue. Agora você sabe de onde vêm os xvalues;)
A chamada de uma função que retorna uma referência rvalue, como std::move
, é um xvalue.
Observe que retornar por referência rvalue é bom neste exemplo, porque t
não indica um objeto automático, mas um objeto que foi passado pelo chamador.