O que é std :: move () e quando deve ser usado?

656
  1. O que é isso?
  2. O que isso faz?
  3. Quando deve ser usado?

Bons links são apreciados.

Basilevs
fonte
43
Bjarne Stroustrup explica movimento em Uma Breve Introdução ao rvalue referências
DumbCoder
1
Mova semântica
Basilevs
12
Esta questão está se referindo std::move(T && t); existe também std::move(InputIt first, InputIt last, OutputIt d_first)um algoritmo relacionado a std::copy. Aponto para que outros não fiquem tão confusos quanto eu quando fui confrontado pela primeira vez com std::movetrês argumentos. pt.cppreference.com/w/cpp/algorithm/move
josaphatv

Respostas:

287

Página da Wikipedia em referências de valor R do C ++ 11 e mover construtores

  1. No C ++ 11, além de copiar construtores, os objetos podem ter construtores de movimentação.
    (Além de operadores de atribuição de cópia, eles têm operadores de atribuição de movimentação).
  2. O construtor move é usado em vez do construtor copy, se o objeto tiver o tipo "rvalue-reference" ( Type &&).
  3. std::move() é uma conversão que produz uma referência de valor-valor a um objeto, para permitir sua movimentação.

É uma nova maneira de C ++ para evitar cópias. Por exemplo, usando um construtor de movimentação, a std::vectorpoderia simplesmente copiar seu ponteiro interno para dados para o novo objeto, deixando o objeto movido em um estado movido do estado, portanto, não copiando todos os dados. Isso seria válido para C ++.

Tente pesquisar no Google para mover semântica, rvalor, encaminhamento perfeito.

Scharron
fonte
40
A semântica de movimentação exige que o objeto movido permaneça válido , o que não é um estado incorreto. (Justificativa: Ele ainda tem de destruir, fazer o trabalho.)
GManNickG
13
@ GMan: bem, ele deve estar em um estado seguro para ser destruído, mas, AFAIK, não precisa ser usado para mais nada.
Zan Lynx
8
@ZanLynx: Certo. Observe que a biblioteca padrão exige adicionalmente que objetos movidos sejam atribuíveis, mas isso é apenas para objetos usados ​​no stdlib, não um requisito geral.
GManNickG 04/10
25
-1 "std :: move () é a maneira C ++ 11 de usar a semântica de movimentação" Corrija isso. std::move()não é a maneira de usar a semântica de movimentação, a semântica de movimentação é realizada de forma transparente para o programador. moveé apenas uma conversão para passar um valor de um ponto para outro em que o valor original l não será mais usado.
precisa saber é o seguinte
15
Eu iria além. std::moveem si não faz "nada" - tem zero efeitos colaterais. Apenas sinaliza ao compilador que o programador não se importa mais com o que acontece com esse objeto. ou seja, ele dá permissão para outras partes do software para mover a partir do objeto, mas não exigir que ele seja movido. De fato, o destinatário de uma referência rvalue não precisa fazer nenhuma promessa sobre o que fará ou não com os dados.
Aaron McDaid
241

1. "O que é isso?"

Embora std::move() seja tecnicamente uma função - eu diria que não é realmente uma função . É uma espécie de conversor entre as maneiras pelas quais o compilador considera o valor de uma expressão.

2. "O que isso faz?"

A primeira coisa a notar é que std::move() , na verdade, não move nada . Ele converte uma expressão de lvalue (como uma variável nomeada) em xvalue . Um xvalue diz ao compilador:

Você pode me pilhar, mover qualquer coisa que eu esteja segurando e usá-la em outro lugar (já que eu vou ser destruído em breve de qualquer maneira) ".

em outras palavras, quando você usa std::move(x), está permitindo que o compilador canibalize x. Portanto, se xtiver, digamos, seu próprio buffer na memória - depois que std::move()o compilador puder ter outro objeto, ele será o proprietário.

Você também pode passar de um valor inicial (como um temporário que está passando), mas isso raramente é útil.

3. "Quando deve ser usado?"

Outra maneira de fazer essa pergunta é "Para que eu canibalizaria os recursos de um objeto existente?" bem, se você estiver escrevendo o código do aplicativo, provavelmente não estaria mexendo muito com objetos temporários criados pelo compilador. Então, principalmente, você faria isso em lugares como construtores, métodos de operador, funções semelhantes a algoritmos de biblioteca padrão etc. onde objetos são criados e destruídos muito automagicamente. Claro, isso é apenas uma regra de ouro.

Um uso típico é 'mover' recursos de um objeto para outro em vez de copiar. O @Guillaume fornece links para esta página, que possui um exemplo simples e simples: trocar dois objetos por menos cópias.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

usar move permite trocar os recursos em vez de copiá-los:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

Pense no que acontece quando T, digamos, o vector<int>tamanho n. Na primeira versão, você lê e escreve 3 * n elementos, na segunda versão, basicamente lê e escreve apenas os 3 ponteiros para os buffers dos vetores, além dos tamanhos dos 3 buffers. Claro que classeT precisa saber como fazer a mudança; sua classe deve ter um operador de atribuição de movimento e um construtor de movimento para que a classe Tfuncione.

einpoklum
fonte
3
Durante muito tempo, ouvi falar dessas semânticas de movimento, nunca as examinei. A partir desta descrição que você forneceu, parece que é uma cópia superficial em vez de uma cópia profunda.
Zebrafish
7
@TitoneMaurice: Exceto que não é uma cópia - pois o valor original não é mais utilizável.
Einpoklum
3
@ Zebrafish você não poderia estar mais errado. Uma cópia superficial deixa o original exatamente no mesmo estado, uma movimentação geralmente resulta no vazio do original ou em um estado válido.
rubenvb
17
@rubenvb Zebra não está totalmente errado. Embora seja verdade que o objeto canabilizado original geralmente é deliberadamente sabotado para evitar erros confusos (por exemplo, defina seus ponteiros para nullptr para sinalizar que ele não é mais o proprietário dos ponteiros), o fato de que toda a movimentação é implementada simplesmente copiando um ponteiro da fonte para o destino (e deliberadamente evitar fazer qualquer coisa com o apontador) é de fato uma reminiscência de uma cópia superficial. De fato, eu chegaria ao ponto de dizer que um movimento é uma cópia superficial, seguida opcionalmente por uma autodestruição parcial da fonte. (cont.)
Luminosidade raças em órbita
3
(cont.) Se permitirmos essa definição (e eu gosto bastante), a observação de @ Zebrafish não está errada, apenas um pouco incompleta.
Leveza raças na órbita
146

Você pode usar move quando precisar "transferir" o conteúdo de um objeto para outro lugar, sem fazer uma cópia (ou seja, o conteúdo não é duplicado, é por isso que poderia ser usado em alguns objetos não copiáveis, como um unique_ptr). Também é possível que um objeto pegue o conteúdo de um objeto temporário sem fazer uma cópia (e economizar muito tempo), com std :: move.

Esse link realmente me ajudou:

http://thbecker.net/articles/rvalue_references/section_01.html

Sinto muito se minha resposta está chegando muito tarde, mas eu também estava procurando um bom link para o std :: move, e achei os links acima um pouco "austeros".

Isso coloca a ênfase na referência de valor-r, em que contexto você deve usá-las, e eu acho que é mais detalhado, é por isso que eu queria compartilhar esse link aqui.

Guillaume
fonte
26
Belo link. Eu sempre achei o artigo da Wikipedia, e outros links que me deparei bastante confusos, uma vez que eles apenas jogam fatos sobre você, deixando para você descobrir qual é o verdadeiro significado / lógica. Enquanto "mover semântica" em um construtor é bastante óbvio, todos esses detalhes sobre a passagem de && - valores ao redor não são ... então a descrição no estilo tutorial foi muito boa.
Christian Stieber
66

P: O que é std::move ?

UMA: std::move() é uma função da Biblioteca Padrão C ++ para converter em uma referência rvalue.

Simplisticamente std::move(t)é equivalente a:

static_cast<T&&>(t);

Um rvalue é um temporário que não persiste além da expressão que o define, como um resultado de função intermediária que nunca é armazenado em uma variável.

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Uma implementação para std :: move () é fornecida no N2027: "Uma breve introdução às referências de valor" da seguinte maneira:

template <class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
    return a;
}

Como você pode ver, std::moveretorna T&&não importa se chamado com um valor ( T), tipo de referência ( T&) ou referência de rvalor ( T&&).

P: O que faz?

R: Como um elenco, ele não faz nada durante o tempo de execução. Só é relevante no momento da compilação informar ao compilador que você gostaria de continuar considerando a referência como um rvalue.

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

O que não faz:

  • Faça uma cópia do argumento
  • Chame o construtor de cópia
  • Alterar o objeto de argumento

Q: quando deve ser usado?

R: Você deve usar std::movese desejar chamar funções que suportem a semântica de movimentação com um argumento que não seja um rvalue (expressão temporária).

Isso gera as seguintes perguntas de acompanhamento para mim:

  • O que são semânticas de movimento? Mover semântica em contraste com copiar semântica é uma técnica de programação na qual os membros de um objeto são inicializados 'assumindo' em vez de copiar os membros de outro objeto. Esse 'controle' só faz sentido com ponteiros e identificadores de recursos, que podem ser transferidos de maneira barata, copiando o ponteiro ou identificador inteiro, em vez dos dados subjacentes.

  • Que tipo de classes e objetos suportam mover semântica? Cabe a você, como desenvolvedor, implementar a semântica de movimentação em suas próprias classes, se elas se beneficiarem da transferência de seus membros, em vez de copiá-los. Depois de implementar a semântica de movimentação, você se beneficiará diretamente do trabalho de muitos programadores de bibliotecas que adicionaram suporte para lidar com classes com semântica de movimentação com eficiência.

  • Por que o compilador não pode descobrir sozinho? O compilador não pode simplesmente chamar outra sobrecarga de uma função, a menos que você diga. Você deve ajudar o compilador a escolher se a versão regular ou móvel da função deve ser chamada.

  • Em quais situações eu gostaria de dizer ao compilador que ele deve tratar uma variável como um rvalue? Isso provavelmente acontecerá nas funções de modelo ou biblioteca, nas quais você sabe que um resultado intermediário pode ser recuperado.

Christopher Oezbek
fonte
2
Grande +1 para exemplos de código com semântica nos comentários. As outras respostas principais definem std :: move usando o próprio "move" - ​​realmente não esclarece nada! --- Eu acredito que vale a pena mencionar que não fazer uma cópia do argumento significa que o valor original não pode ser usado com segurança.
ty
34

std :: move em si realmente não faz muito. Eu pensei que ele chamava o construtor movido para um objeto, mas realmente executa apenas uma conversão de tipo (convertendo uma variável lvalue em um rvalue para que a referida variável possa ser passada como argumento para um construtor de movimentação ou operador de atribuição).

Portanto, std :: move é usado apenas como precursor do uso da semântica de movimento. A semântica de movimento é essencialmente uma maneira eficiente de lidar com objetos temporários.

Considere o objeto A = B + C + D + E + F;

Este é um código bonito, mas o E + F produz um objeto temporário. Então D + temp produz outro objeto temporário e assim por diante. Em cada operador "+" normal de uma classe, ocorrem cópias profundas.

Por exemplo

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

A criação do objeto temporário nessa função é inútil - esses objetos temporários serão excluídos no final da linha de qualquer maneira, à medida que saem do escopo.

Podemos usar a semântica de movimento para "saquear" os objetos temporários e fazer algo como

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Isso evita que cópias profundas desnecessárias sejam feitas. Com referência ao exemplo, a única parte em que a cópia profunda ocorre agora é E + F. O restante usa semântica de movimento. O construtor de movimentação ou o operador de atribuição também precisam ser implementados para atribuir o resultado a A.

user929404
fonte
3
você falou sobre a semântica de movimentos. você deve adicionar à sua resposta como o std :: move pode ser usado, porque a pergunta é sobre isso.
Koushik Shetty
2
@Koushik std :: move não faz muito - mas é usado para implementar a semântica dos movimentos. Se você não sabe sobre std :: se move, você provavelmente não semântica saber mover qualquer
user929404
1
"não faz muito" (sim, apenas um static_cast para uma referência rvalue). o que realmente faz e y faz é o que o OP pediu. você não precisa saber como o std :: move funciona, mas precisa saber o que a semântica do movimento faz. além disso, "mas é usado para implementar a semântica de movimentação", é o contrário. sabe mover semântica e você entenderá std :: move caso contrário não. O movimento apenas ajuda no movimento e ele próprio usa a semântica do movimento. std :: move não faz nada além de converter seu argumento em referência rvalue, que é o que a semântica de movimento exige.
Koushik Shetty
10
"mas o E + F produz um objeto temporário" - o operador +vai da esquerda para a direita, não da direita para a esquerda. Daí B+Cseria o primeiro!
Ajay
8

"O que é isso?" e "O que isso faz?" foi explicado acima.

Vou dar um exemplo de "quando deve ser usado".

Por exemplo, temos uma classe com muitos recursos, como grande matriz.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Código do teste:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

saída como abaixo:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Podemos ver que std::movecom move constructortorna o recurso de transformação facilmente.

Onde mais é std::moveútil?

std::movetambém pode ser útil ao classificar uma matriz de elementos. Muitos algoritmos de classificação (como seleção e bolha) funcionam trocando pares de elementos. Anteriormente, tivemos que recorrer à semântica de cópias para fazer a troca. Agora podemos usar a semântica de movimentação, que é mais eficiente.

Também pode ser útil se quisermos mover o conteúdo gerenciado por um ponteiro inteligente para outro.

Citado:

Jayhello
fonte
0

Aqui está um exemplo completo, usando std :: move para um vetor personalizado (simples)

Saída esperada:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Compilar como:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Código:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
Neil McGill
fonte