O que é semântica de movimento?

1702

Acabei de ouvir a entrevista de podcast de rádio da Engenharia de Software com Scott Meyers sobre C ++ 0x . A maioria dos novos recursos fazia sentido para mim, e estou realmente empolgado com o C ++ 0x agora, com exceção de um. Ainda não entendo a semântica de movimentos ... O que é exatamente?

dicroce
fonte
20
Eu achei [o artigo de blog de Eli Bendersky] ( eli.thegreenplace.net/2011/12/15/… ) sobre lvalues ​​e rvalues ​​em C e C ++ bastante informativo. Ele também menciona referências de rvalue no C ++ 11 e as apresenta com pequenos exemplos.
Nils
16
A exposição de Alex Allain sobre o assunto está muito bem escrita.
Patrick Sanan
19
Todo ano, mais ou menos, eu me pergunto sobre o que é a "nova" semântica de movimento em C ++, pesco no Google e chego a esta página. Eu li as respostas, meu cérebro se desliga. Volto para C e esqueço tudo! Estou em um impasse.
sky
7
@sky Considere std :: vector <> ... Em algum lugar existe um ponteiro para uma matriz na pilha. Se você copiar este objeto, um novo buffer deverá ser alocado e os dados do buffer precisarão ser copiados para o novo buffer. Existe alguma circunstância em que seria bom simplesmente roubar o ponteiro? A resposta é SIM, quando o compilador sabe que o objeto é temporário. A semântica de movimento permite que você defina como suas tripas de classe podem ser movidas e descartadas em um objeto diferente quando o compilador sabe que o objeto do qual você está se movendo está prestes a desaparecer.
dicroce
A única referência que eu posso entender: learncpp.com/cpp-tutorial/… , ou seja, o raciocínio original da semântica de movimentos é de ponteiros inteligentes.
jw_ 03/11

Respostas:

2481

Acho mais fácil entender a semântica de movimentação com código de exemplo. Vamos começar com uma classe de seqüência de caracteres muito simples que contém apenas um ponteiro para um bloco de memória alocado por heap:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Como escolhemos gerenciar a memória por nós mesmos, precisamos seguir a regra dos três . Vou adiar a gravação do operador de atribuição e implementar apenas o destruidor e o construtor de cópia por enquanto:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

O construtor de cópia define o que significa copiar objetos de sequência. O parâmetro const string& thatliga-se a todas as expressões do tipo string que permitem fazer cópias nos seguintes exemplos:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Agora vem a principal visão sobre a semântica de movimentos. Observe que somente na primeira linha em que copiamos xessa cópia profunda é realmente necessária, pois podemos querer inspecionar xmais tarde e ficaríamos muito surpresos se xtivessem mudado de alguma forma. Você notou como eu disse xtrês vezes (quatro vezes, se você incluir esta frase) e sempre quis dizer exatamente o mesmo objeto ? Chamamos expressões como x"lvalues".

Os argumentos nas linhas 2 e 3 não são lvalues, mas rvalues, porque os objetos de cadeia subjacentes não têm nomes; portanto, o cliente não tem como inspecioná-los novamente posteriormente. rvalues ​​denota objetos temporários que são destruídos no ponto e vírgula seguinte (para ser mais preciso: no final da expressão completa que contém lexicamente o rvalue). Isso é importante porque durante a inicialização de be c, poderíamos fazer o que quiséssemos com a cadeia de origem, e o cliente não poderia dizer a diferença !

O C ++ 0x introduz um novo mecanismo chamado "referência de rvalue" que, entre outras coisas, nos permite detectar argumentos de rvalue por sobrecarga de função. Tudo o que precisamos fazer é escrever um construtor com um parâmetro de referência rvalue. Dentro desse construtor, podemos fazer o que quisermos com a fonte, desde que a deixemos em algum estado válido:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

O que fizemos aqui? Em vez de copiar profundamente os dados da pilha, apenas copiamos o ponteiro e, em seguida, definimos o ponteiro original como nulo (para impedir que 'delete []' do destruidor do objeto de origem libere nossos 'dados apenas roubados'). Com efeito, "roubamos" os dados que originalmente pertenciam à string de origem. Novamente, o principal insight é que, sob nenhuma circunstância, o cliente pode detectar que a fonte foi modificada. Como realmente não fazemos uma cópia aqui, chamamos esse construtor de "construtor de movimentação". Seu trabalho é mover recursos de um objeto para outro em vez de copiá-los.

Parabéns, agora você entende o básico da semântica de movimentos! Vamos continuar implementando o operador de atribuição. Se você não estiver familiarizado com o idioma de copiar e trocar , aprenda-o e volte, pois é um idioma incrível do C ++ relacionado à segurança de exceções.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Huh, é isso? "Onde está a referência de valor?" você pode perguntar. "Nós não precisamos disso aqui!" é a minha resposta :)

Observe que passamos o parâmetro that por valor , portanto, thatele deve ser inicializado como qualquer outro objeto de string. Exatamente como thatserá inicializado? Nos velhos tempos do C ++ 98 , a resposta teria sido "pelo construtor de cópias". No C ++ 0x, o compilador escolhe entre o construtor de cópia e o construtor de movimentação, com base no argumento para o operador de atribuição ser um lvalue ou um rvalue.

Portanto, se você disser a = b, o construtor de cópia será inicializado that(porque a expressão bé um valor l) e o operador de atribuição troca o conteúdo por uma cópia profunda recém-criada. Essa é a própria definição da expressão copiar e trocar - faça uma cópia, troque o conteúdo pela cópia e depois se livre da cópia, deixando o escopo. Nada de novo aqui.

Mas se você disser a = x + y, o construtor da movimentação será inicializado that(porque a expressão x + yé um rvalor), portanto, não há cópia profunda envolvida, apenas uma movimentação eficiente. thatainda é um objeto independente do argumento, mas sua construção foi trivial, pois os dados do heap não precisaram ser copiados, apenas movidos. Não foi necessário copiá-lo porque x + yé um rvalue e, novamente, não há problema em mudar de objetos de string indicados por rvalues.

Para resumir, o construtor de cópias faz uma cópia profunda, porque a fonte deve permanecer intocada. O construtor de movimentação, por outro lado, pode simplesmente copiar o ponteiro e, em seguida, definir o ponteiro na fonte como nulo. Não há problema em "anular" o objeto de origem dessa maneira, porque o cliente não tem como inspecionar o objeto novamente.

Espero que este exemplo tenha entendido o ponto principal. Há muito mais para avaliar as referências e mover a semântica que deixei intencionalmente para simplificar. Se você quiser mais detalhes, consulte minha resposta complementar .

fredoverflow
fonte
40
@Mas se meu ctor está recebendo um rvalue, que nunca pode ser usado posteriormente, por que eu preciso me preocupar em deixá-lo em um estado consistente / seguro? Em vez de definir that.data = 0, por que não deixar?
einpoklum
70
@einpoklum Porque sem that.data = 0, os personagens seriam destruídos muito cedo (quando o temporário morrer), e também duas vezes. Você quer roubar os dados, não compartilhá-los!
Fredoverflow 17/07/2013
19
@einpoklum O destruidor programado regularmente ainda é executado, portanto, você deve garantir que o estado pós-movimentação do objeto de origem não cause uma falha. Melhor, verifique se o objeto de origem também pode ser o destinatário de uma atribuição ou outra gravação.
CTMacUser 02/09
12
@pranitkothari Sim, todos os objetos devem ser destruídos, e até movidos. E como não queremos que o array de caracteres seja excluído quando isso acontecer, precisamos definir o ponteiro como nulo.
Fredoverflow
7
@ Virus721 delete[]em um nullptr é definido pelo padrão C ++ como não operacional.
fredoverflow
1057

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:

  1. 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
    
        // ...
    };
  2. 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_ptrum modelo de biblioteca padrão C ++ 98 descontinuado, que foi substituído por std::unique_ptrC ++ 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 bcom aque não copiar o triângulo, mas em vez transfere a propriedade do triângulo a partir ade 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_ptrprovavelmente 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_ptrinvocará um comportamento indefinido, portanto, você deve ter muito cuidado para não usar uma auto_ptrapó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_ptrnem 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 ae 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 aque denota uma auto_ptrvariável e a expressão make_triangle()que denota a chamada de uma função que retorna um auto_ptrvalor por, criando assim um novo auto_ptrobjeto 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 le rtê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 constna mistura, já temos quatro tipos diferentes de referências. A que tipos de expressões do tipo Xeles 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 Ypara 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_ptrconstrutor 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&& sourcesó 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=, sourcesai 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::movedentro do cabeçalho <utility>. Esse nome é um pouco infeliz, porque std::movesimplesmente 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_rvalueou 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, anã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 afim de inicializar c; não me importo amais. 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 returninstruçã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 resultcomo argumento? O escopo de resultestá prestes a terminar e será destruído durante o desenrolamento da pilha. Ninguém poderia reclamar depois que isso resultmudou de alguma maneira; quando o fluxo de controle retorna ao chamador, resultele 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::movepara mover objetos automáticos para fora das funções, pois isso inibe a "otimização do valor de retorno nomeado" (NRVO).

Nunca use std::movepara 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::movee 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 parameterquantas 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 parameternão é mais usado após a inicialização do member. Por que não existe uma regra especial para inserir silenciosamente, std::moveassim 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 returnpalavra - chave indica um objeto automático.

Você também pode passar o parametervalor 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, Tfor 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 Te 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, moveaceita qualquer tipo de parâmetro graças à referência de encaminhamento T&&e retorna uma referência de rvalue. A std::remove_reference<T>::typechamada 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 tsempre é um lvalue (lembre-se de que uma referência nomeada rvalue é um lvalue), mas queremos ligar ta uma referência rvalue, precisamos converter explicitamente tno 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 tnão indica um objeto automático, mas um objeto que foi passado pelo chamador.

fredoverflow
fonte
202
"São apenas 10 páginas no meu monitor"
Mooing Duck 18/07/12
24
Há uma terceira razão pela qual a semântica de movimentos é importante: segurança de exceção. Frequentemente, onde uma operação de cópia pode ser lançada (porque precisa alocar recursos e a alocação pode falhar), uma operação de movimentação pode ser não-lançada (porque pode transferir a propriedade dos recursos existentes em vez de alocar novos). Ter operações que não podem falhar é sempre bom e pode ser crucial ao escrever código que forneça garantias de exceção.
Brangdon
8
Eu estava com você até 'Referências universais', mas é muito abstrato para seguir. Referência em colapso? Encaminhamento perfeito? Você está dizendo que uma referência rvalue se torna uma referência universal se o tipo é modelo? Eu gostaria que houvesse uma maneira de explicar isso para que eu soubesse se preciso entender ou não! :)
Kylotan
8
Por favor, escreva um livro agora ... esta resposta me deu motivos para acreditar que, se você abordou outros cantos do C ++ de uma maneira lúcida como essa, milhares de pessoas o entenderão.
halivingston 27/09/15
12
@halivingston Muito obrigado pelo seu feedback, eu realmente aprecio isso. O problema de escrever um livro é: é muito mais trabalho do que você pode imaginar. Se você quiser se aprofundar no C ++ 11 e além, sugiro que você compre "Effective Modern C ++", de Scott Meyers.
Fredoverflow 28/09/2015
77

A semântica de movimento é baseada em referências de rvalue .
Um rvalue é um objeto temporário, que será destruído no final da expressão. No C ++ atual, os rvalues ​​são vinculados apenas às constreferências. O C ++ 1x permitirá constreferências sem valor, ortografadas T&&, que são referências a objetos rvalue.
Como um rvalue vai morrer no final de uma expressão, você pode roubar seus dados . Em vez de copiá- lo para outro objeto, você move seus dados para ele.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

No código acima, nos compiladores antigos, o resultado de f()é copiado para o xuso Xdo construtor de cópias. Se o seu compilador suportar semântica de movimentação e Xtiver um construtor de movimentação, isso será chamado. Como seu rhsargumento é um rvalor , sabemos que não é mais necessário e podemos roubar seu valor.
Portanto, o valor é movido do temporário sem nome retornado de f()para x(enquanto os dados de x, inicializados para um vazio X, são movidos para o temporário, que será destruído após a atribuição).

sbi
fonte
1
nota que deve ser this->swap(std::move(rhs));porque as referências rvalue nomeados são lvalues
wmamrak
Isso é meio errado, de acordo com o comentário de @ Tacyt: rhsé um valor l no contexto de X::X(X&& rhs). Você precisa ligar std::move(rhs)para obter um rvalor, mas isso meio que faz a resposta ser discutível.
Asherah
O que move a semântica para tipos sem ponteiros? Mover semântica funciona como cópia?
Gusev Slava
@Gusev: Eu não tenho idéia do que você está perguntando.
SBI
60

Suponha que você tenha uma função que retorne um objeto substancial:

Matrix multiply(const Matrix &a, const Matrix &b);

Quando você escreve um código como este:

Matrix r = multiply(a, b);

então, um compilador C ++ comum criará um objeto temporário para o resultado de multiply(), chame o construtor de cópia para inicializar re destrua o valor de retorno temporário. A semântica de movimentação no C ++ 0x permite que o "construtor de movimentação" seja chamado para inicializar rcopiando seu conteúdo e depois descarte o valor temporário sem precisar destruí-lo.

Isso é especialmente importante se (como talvez o Matrixexemplo acima), o objeto a ser copiado alocar memória extra na pilha para armazenar sua representação interna. Um construtor de cópias teria que fazer uma cópia completa da representação interna ou usar a contagem de referência e a semântica de copiar na gravação de forma interativa. Um construtor de movimentação deixaria a memória da pilha em paz e apenas copiaria o ponteiro dentro do Matrixobjeto.

Greg Hewgill
fonte
2
Como os construtores de movimentação e cópia são diferentes?
dicroce
1
@ dicroce: Eles diferem quanto à sintaxe, um se parece com Matrix (const Matrix & src) (construtor de cópias) e o outro se parece com Matrix (matrix && src) (construtor de movimento), verifique minha resposta principal para obter um exemplo melhor.
snk_kid
3
@ dicroce: Um faz um objeto em branco e um faz uma cópia. Se os dados armazenados no objeto forem grandes, uma cópia poderá ser cara. Por exemplo, std :: vector.
precisa
1
@ kunj2aan: Depende do seu compilador, suspeito. O compilador pode criar um objeto temporário dentro da função e movê-lo para o valor de retorno do chamador. Ou, pode ser capaz de construir diretamente o objeto no valor de retorno, sem a necessidade de usar um construtor de movimentação.
Greg Hewgill
2
@Jichao: Isso é uma otimização chamado RVO, consulte esta pergunta para obter mais informações sobre a diferença: stackoverflow.com/questions/5031778/...
Greg Hewgill
30

Se você estiver realmente interessado em uma boa explicação detalhada da semântica de movimentos, recomendo a leitura do artigo original, "Uma proposta para adicionar suporte à semântica de movimentos à linguagem C ++".

É muito acessível e fácil de ler e é um excelente argumento para os benefícios que eles oferecem. Existem outros trabalhos mais recentes e atualizados sobre a semântica de movimentos disponíveis no site do WG21 , mas este é provavelmente o mais direto, pois aborda as coisas de uma visão de nível superior e não se aprofunda nos detalhes da linguagem.

James McNellis
fonte
27

Mover semântica é transferir recursos, em vez de copiá-los quando ninguém mais precisar do valor de origem.

No C ++ 03, os objetos geralmente são copiados, apenas para serem destruídos ou atribuídos antes que qualquer código use o valor novamente. Por exemplo, quando você retorna por valor de uma função - a menos que o RVO entre em ação - o valor retornado é copiado para o quadro de pilha do chamador e, em seguida, sai do escopo e é destruído. Este é apenas um dos muitos exemplos: veja a passagem por valor quando o objeto de origem é temporário, algoritmos como sortesse apenas reorganizam itens, realocam vectorquando seu valor capacity()é excedido, etc.

Quando esses pares de copiar / destruir são caros, geralmente é porque o objeto possui algum recurso pesado. Por exemplo, vector<string>pode possuir um bloco de memória alocado dinamicamente contendo uma matriz de stringobjetos, cada um com sua própria memória dinâmica. Copiar esse objeto é caro: você precisa alocar nova memória para cada bloco alocado dinamicamente na fonte e copiar todos os valores. Então você precisa desalocar toda a memória que acabou de copiar. No entanto, mover um grande vector<string>significa apenas copiar alguns ponteiros (que se referem ao bloco de memória dinâmica) para o destino e zerá-los na fonte.

Dave Abrahams
fonte
23

Em termos fáceis (práticos):

Copiar um objeto significa copiar seus membros "estáticos" e chamar o newoperador para seus objetos dinâmicos. Direita?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

No entanto, mover um objeto (repito, do ponto de vista prático) implica apenas copiar os ponteiros de objetos dinâmicos, e não criar novos.

Mas isso não é perigoso? Obviamente, você pode destruir um objeto dinâmico duas vezes (falha de segmentação). Portanto, para evitar isso, você deve "invalidar" os ponteiros de origem para evitar destruí-los duas vezes:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, mas se eu mover um objeto, o objeto de origem se torna inútil, não? Claro, mas em certas situações isso é muito útil. O mais evidente é quando chamo uma função com um objeto anônimo (temporal, objeto rvalue, ..., você pode chamá-lo com nomes diferentes):

void heavyFunction(HeavyType());

Nessa situação, um objeto anônimo é criado, depois copiado para o parâmetro de função e excluído posteriormente. Portanto, aqui é melhor mover o objeto, porque você não precisa do objeto anônimo e pode economizar tempo e memória.

Isso leva ao conceito de uma referência "rvalue". Eles existem no C ++ 11 apenas para detectar se o objeto recebido é anônimo ou não. Eu acho que você já sabe que um "lvalue" é uma entidade atribuível (a parte esquerda do =operador), portanto, você precisa de uma referência nomeada a um objeto para poder atuar como um lvalue. Um rvalue é exatamente o oposto, um objeto sem referências nomeadas. Por esse motivo, objeto anônimo e rvalue são sinônimos. Assim:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

Nesse caso, quando um objeto do tipo Adeve ser "copiado", o compilador cria uma referência lvalue ou uma referência rvalue de acordo com se o objeto transmitido é nomeado ou não. Quando não, seu construtor de movimentação é chamado e você sabe que o objeto é temporal e pode mover seus objetos dinâmicos em vez de copiá-los, economizando espaço e memória.

É importante lembrar que objetos "estáticos" são sempre copiados. Não há maneiras de "mover" um objeto estático (objeto na pilha e não na pilha). Portanto, a distinção "mover" / "copiar" quando um objeto não possui membros dinâmicos (direta ou indiretamente) é irrelevante.

Se o seu objeto for complexo e o destruidor tiver outros efeitos secundários, como chamar a função de uma biblioteca, chamar outras funções globais ou o que quer que seja, talvez seja melhor sinalizar um movimento com uma bandeira:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Portanto, seu código é mais curto (você não precisa fazer uma nullptratribuição para cada membro dinâmico) e mais geral.

Outra pergunta típica: qual é a diferença entre A&&e const A&&? Claro, no primeiro caso, você pode modificar o objeto e no segundo não, mas, significado prático? No segundo caso, você não pode modificá-lo, portanto não há como invalidar o objeto (exceto com um sinalizador mutável ou algo parecido) e não há diferença prática para um construtor de cópias.

E o que é encaminhamento perfeito ? É importante saber que uma "referência rvalue" é uma referência a um objeto nomeado no "escopo do chamador". Porém, no escopo real, uma referência rvalue é um nome para um objeto e, portanto, atua como um objeto nomeado. Se você passar uma referência rvalue para outra função, estará passando um objeto nomeado, portanto, o objeto não será recebido como um objeto temporal.

void some_function(A&& a)
{
   other_function(a);
}

O objeto aseria copiado para o parâmetro real de other_function. Se você deseja que o objeto acontinue sendo tratado como um objeto temporário, use a std::movefunção:

other_function(std::move(a));

Com esta linha, std::moveconverterá aem um rvalue e other_functionreceberá o objeto como um objeto sem nome. Obviamente, se other_functionnão houver sobrecarga específica para trabalhar com objetos não nomeados, essa distinção não será importante.

Esse encaminhamento é perfeito? Não, mas estamos muito perto. O encaminhamento perfeito é útil apenas para trabalhar com modelos, com o objetivo de dizer: se eu precisar passar um objeto para outra função, preciso que, se eu receber um objeto nomeado, o objeto seja passado como um objeto nomeado e, quando não, Quero passar como um objeto sem nome:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

Essa é a assinatura de uma função prototípica que utiliza o encaminhamento perfeito, implementada no C ++ 11 por meio de std::forward. Esta função explora algumas regras de instanciação de modelo:

 `A& && == A&`
 `A&& && == A&&`

Portanto, se Tfor uma referência de valor l para A( T = A &), atambém ( A & && => A &). Se Té uma referência de valor igual a A, atambém (A && && => A &&). Nos dois casos, aé um objeto nomeado no escopo real, mas Tcontém as informações do seu "tipo de referência" do ponto de vista do escopo do chamador. Esta informação ( T) é passada como parâmetro de modelo para forwarde 'a' é movido ou não de acordo com o tipo de T.

Peregring-lk
fonte
20

É como copiar semântica, mas, em vez de ter que duplicar todos os dados, você rouba os dados do objeto que está sendo "movido".

Terry Mahaffey
fonte
13

Você sabe o que significa semântica de cópia, certo? significa que você tem tipos que podem ser copiados; para tipos definidos pelo usuário, você define isso escrevendo explicitamente um construtor de cópias e um operador de atribuição ou o compilador os gera implicitamente. Isso fará uma cópia.

A semântica de movimentação é basicamente um tipo definido pelo usuário com o construtor que recebe uma referência de valor r (novo tipo de referência usando && (sim dois e comercial)) que não é const, isso é chamado de construtor de movimentação, o mesmo vale para o operador de atribuição. Então, o que um construtor de movimentação faz, bem, em vez de copiar a memória do argumento de origem, ele 'move' a memória da origem para o destino.

Quando você quer fazer isso? well std :: vector é um exemplo, digamos que você criou um std :: vector temporário e você o retorna de uma função, digamos:

std::vector<foo> get_foos();

Você terá sobrecarga do construtor de cópias quando a função retornar, se (e em C ++ 0x) std :: vector tiver um construtor de movimentação em vez de copiar, basta definir seus ponteiros e 'mover' alocado dinamicamente memória para a nova instância. É como uma semântica de transferência de propriedade com std :: auto_ptr.

snk_kid
fonte
1
Eu não acho que este seja um ótimo exemplo, porque nesses exemplos de função de retorno de valor, a Otimização do Valor de Retorno provavelmente já está eliminando a operação de cópia.
Zan Lynx
7

Para ilustrar a necessidade de semântica de movimentação , vamos considerar este exemplo sem semântica de movimentação:

Aqui está uma função que pega um objeto do tipo Te retorna um objeto do mesmo tipo T:

T f(T o) { return o; }
  //^^^ new object constructed

A função acima usa chamada por valor, o que significa que, quando essa função é chamada, um objeto deve ser construído para ser usado pela função.
Como a função também retorna por valor , outro novo objeto é construído para o valor de retorno:

T b = f(a);
  //^ new object constructed

Dois novos objetos foram construídos, um dos quais é um objeto temporário usado apenas durante a duração da função.

Quando o novo objeto é criado a partir do valor retornado, o construtor de cópia é chamado para copiar o conteúdo do objeto temporário para o novo objeto b. Depois que a função é concluída, o objeto temporário usado na função sai do escopo e é destruído.


Agora, vamos considerar o que um construtor de cópias faz.

Ele deve primeiro inicializar o objeto e depois copiar todos os dados relevantes do objeto antigo para o novo.
Dependendo da classe, talvez seja um contêiner com muitos dados; isso pode representar muito tempo e uso de memória

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

Com a semântica de movimentação , agora é possível tornar a maior parte desse trabalho menos desagradável, basta mover os dados em vez de copiar.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Mover os dados envolve associar novamente os dados ao novo objeto. E nenhuma cópia ocorre .

Isso é realizado com uma rvaluereferência.
Uma rvaluereferência funciona praticamente como uma lvaluereferência com uma diferença importante:
uma referência rvalue pode ser movida e um lvalue não.

De cppreference.com :

Para tornar possível uma forte garantia de exceção, os construtores de movimentação definidos pelo usuário não devem lançar exceções. De fato, os contêineres padrão geralmente contam com std :: move_if_noexcept para escolher entre mover e copiar quando os elementos do contêiner precisam ser realocados. Se forem fornecidos construtores copy e move, a resolução de sobrecarga selecionará o construtor move se o argumento for um rvalue (um prvalue como um temporário sem nome ou um xvalue como o resultado de std :: move) e selecionará o construtor copy se o argumento é um lvalue (objeto nomeado ou uma função / operador retornando referência lvalue). Se apenas o construtor de cópia for fornecido, todas as categorias de argumentos o selecionarão (desde que faça referência a const, já que rvalues ​​podem ser vinculados a referências const), o que torna a cópia o substituto da movimentação, quando a movimentação não está disponível. Em muitas situações, os construtores de movimentação são otimizados, mesmo que produzam efeitos colaterais observáveis, consulte elision da cópia. Um construtor é chamado de 'mover construtor' quando recebe uma referência de rvalue como parâmetro. Não é obrigatório mover nada, a classe não precisa ter um recurso a ser movido e um 'construtor de movimentação' pode não ser capaz de mover um recurso, como no caso permitido (mas talvez não sensato) em que o parâmetro é um referência de valor constante (const T &&).

Andreas DM
fonte
7

Estou escrevendo isso para garantir que eu o entenda corretamente.

A semântica de movimento foi criada para evitar a cópia desnecessária de objetos grandes. Bjarne Stroustrup, em seu livro "A linguagem de programação C ++", usa dois exemplos em que a cópia desnecessária ocorre por padrão: um, a troca de dois objetos grandes e dois, o retorno de um objeto grande de um método.

Trocar dois objetos grandes geralmente envolve copiar o primeiro objeto para um objeto temporário, copiar o segundo objeto para o primeiro objeto e copiar o objeto temporário para o segundo objeto. Para um tipo interno, isso é muito rápido, mas para objetos grandes, essas três cópias podem demorar muito tempo. Uma "atribuição de movimentação" permite que o programador substitua o comportamento padrão da cópia e troque as referências aos objetos, o que significa que não há cópia e a operação de troca é muito mais rápida. A atribuição de movimentação pode ser chamada chamando o método std :: move ().

Retornar um objeto de um método por padrão envolve fazer uma cópia do objeto local e de seus dados associados em um local acessível ao chamador (porque o objeto local não é acessível ao chamador e desaparece quando o método termina). Quando um tipo interno está sendo retornado, essa operação é muito rápida, mas se um objeto grande estiver sendo retornado, isso poderá levar muito tempo. O construtor move permite que o programador substitua esse comportamento padrão e, em vez disso, "reutilize" os dados de heap associados ao objeto local, apontando o objeto retornado ao chamador para heap de dados associados ao objeto local. Portanto, nenhuma cópia é necessária.

Em idiomas que não permitem a criação de objetos locais (ou seja, objetos na pilha), esses tipos de problemas não ocorrem porque todos os objetos são alocados no heap e sempre são acessados ​​por referência.

Chris B
fonte
"Uma" atribuição de movimentação "permite que o programador substitua o comportamento padrão da cópia e troque referências aos objetos, o que significa que não há cópia e a operação de troca é muito mais rápida." - estas alegações são ambíguas e enganosas. Para trocar dois objetos xe y, você não pode simplesmente "trocar referências aos objetos" ; pode ser que os objetos contenham ponteiros que façam referência a outros dados e esses ponteiros possam ser trocados, mas os operadores de movimentação não precisam trocar nada. Eles podem apagar os dados do objeto movido de, em vez de preservar os dados de destino nele.
Tony Delroy
Você pode escrever swap()sem mover a semântica. "A atribuição de movimentação pode ser chamada chamando o método std :: move ()." - às vezes é necessário usar std::move()- embora isso não mova nada - apenas permite que o compilador saiba que o argumento é móvel, algumas vezes std::forward<>()(com referências de encaminhamento) e outras vezes o compilador sabe que um valor pode ser movido.
Tony Delroy
-2

Aqui está uma resposta do livro "A linguagem de programação C ++", de Bjarne Stroustrup. Se você não quiser ver o vídeo, poderá ver o texto abaixo:

Considere este trecho. Retornar de um operador + envolve copiar o resultado da variável local rese em algum lugar onde o chamador possa acessá-lo.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Nós realmente não queremos uma cópia; nós apenas queríamos obter o resultado de uma função. Portanto, precisamos mover um vetor em vez de copiá-lo. Podemos definir o construtor de movimentação da seguinte maneira:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

O && significa "referência ao valor" e é uma referência à qual podemos vincular um valor. "rvalue" 'visa complementar "lvalue", que significa aproximadamente "algo que pode aparecer no lado esquerdo de uma tarefa". Portanto, um rvalue significa aproximadamente "um valor que você não pode atribuir", como um número inteiro retornado por uma chamada de função e a resvariável local no operador + () para Vectors.

Agora, a declaração return res;não será copiada!

Rob Pei
fonte