Por que podemos usar `std :: move` em um objeto` const`?

112

Em C ++ 11, podemos escrever este código:

struct Cat {
   Cat(){}
};

const Cat cat;
std::move(cat); //this is valid in C++11

quando eu chamo std::move, significa que quero mover o objeto, ou seja, vou mudar o objeto. Mover um constobjeto não é razoável, então por que std::movenão restringe esse comportamento? Vai ser uma armadilha no futuro, certo?

Aqui, armadilha significa como Brandon mencionou no comentário:

"Eu acho que ele quis dizer isso" uma armadilha "sorrateiramente, porque se ele não perceber, ele acaba com uma cópia que não é o que ele pretendia."

No livro 'Effective Modern C ++' de Scott Meyers, ele dá um exemplo:

class Annotation {
public:
    explicit Annotation(const std::string text)
     : value(std::move(text)) //here we want to call string(string&&),
                              //but because text is const, 
                              //the return type of std::move(text) is const std::string&&
                              //so we actually called string(const string&)
                              //it is a bug which is very hard to find out
private:
    std::string value;
};

Se std::movefosse proibido de operar em um constobjeto, poderíamos facilmente descobrir o bug, certo?

camino
fonte
2
Mas tente movê-lo. Tente mudar seu estado. std::movepor si só não faz nada para o objeto. Alguém poderia argumentar que std::moveé mal nomeado.
juanchopanza
3
Na verdade, não move nada. Tudo o que ele faz é lançar em uma referência rvalue. tente CAT cat2 = std::move(cat);, supondo que CATsuporte atribuição de movimento regular.
WhozCraig
11
std::moveé apenas um elenco, na verdade não move nada
Alerta Vermelho
2
@WhozCraig: Cuidado, pois o código que você postou compila e executa sem aviso, tornando-o meio enganoso.
Mooing Duck
1
@MooingDuck Nunca disse que não iria compilar. funciona apenas porque o copy-ctor padrão está habilitado. Silencie isso e as rodas caem.
WhozCraig

Respostas:

55
struct strange {
  mutable size_t count = 0;
  strange( strange const&& o ):count(o.count) { o.count = 0; }
};

const strange s;
strange s2 = std::move(s);

aqui vemos o uso de std::moveem a T const. Ele retorna a T const&&. Temos um construtor de movimento para strangeque seja exatamente esse tipo.

E é chamado.

Agora, é verdade que esse tipo estranho é mais raro do que os bugs que sua proposta consertaria.

Mas, por outro lado, o existente std::movefunciona melhor em código genérico, onde você não sabe se o tipo com o qual está trabalhando é a Tou a T const.

Yakk - Adam Nevraumont
fonte
3
1 por ser a primeira resposta que realmente tenta explicar por que você deseja chamar std::moveum constobjeto.
Chris Drew
+1 para mostrar a execução de uma função const T&&. Isso expressa um "protocolo API" do tipo "Vou pegar um rvalue-ref, mas prometo que não vou modificá-lo". Acho que, além de usar mutável, é incomum. Talvez outro caso de uso seja ser capaz de usar forward_as_tupleem quase tudo e depois usá-lo.
F Pereira
107

Há um truque aqui que você está esquecendo, que std::move(cat) não move nada . Apenas diz ao compilador para tentar mover. No entanto, como sua classe não tem um construtor que aceite um const CAT&&, ela usará o const CAT&construtor de cópia implícito e copiará com segurança. Sem perigo, sem armadilha. Se o construtor de cópia for desativado por qualquer motivo, você receberá um erro do compilador.

struct CAT
{
   CAT(){}
   CAT(const CAT&) {std::cout << "COPY";}
   CAT(CAT&&) {std::cout << "MOVE";}
};

int main() {
    const CAT cat;
    CAT cat2 = std::move(cat);
}

impressões COPY, não MOVE.

http://coliru.stacked-crooked.com/a/0dff72133dbf9d1f

Observe que o bug no código que você mencionou é um problema de desempenho , não um problema de estabilidade , então tal bug não causará um travamento, nunca. Só usará uma cópia mais lenta. Além disso, esse bug também ocorre para objetos não constantes que não têm construtores de movimento, portanto, apenas adicionar uma constsobrecarga não detectará todos eles. Poderíamos verificar a capacidade de mover a construção ou a atribuição do tipo de parâmetro, mas isso interferiria no código de modelo genérico que deveria recorrer ao construtor de cópia. E, diabos, talvez alguém queira ser capaz de construir a partir de const CAT&&quem sou eu para dizer que ele não pode?

Pato mugindo
fonte
Aumentado. Vale a pena notar que a exclusão implícita do copy-ctor na definição definida pelo usuário do construtor de movimento regular ou operador de atribuição também demonstrará isso por meio de compilação interrompida. Boa resposta.
WhozCraig
Também vale a pena mencionar que um construtor de cópia que precisa de um não-valor consttambém não ajudará. [class.copy] §8: "Caso contrário, o construtor de cópia declarado implicitamente terá a forma X::X(X&)"
Desduplicador de
9
Não acho que ele quis dizer "armadilha" em termos de computador / montagem. Acho que ele quis dizer que o "prendeu" sorrateiramente, porque se ele não perceber, ele acaba com uma cópia que não é o que ele pretendia. Eu acho ...
Brandon
você pode obter mais 1 voto positivo e eu recebo mais 4, por um distintivo dourado. ;)
Yakk - Adam Nevraumont
Mais cinco nessa amostra de código. Transmite o ponto muito bem.
Brent Writes Code
20

Um motivo pelo qual o restante das respostas foi negligenciado até agora é a capacidade do código genérico de ser resiliente em caso de mudança. Por exemplo, digamos que eu quisesse escrever uma função genérica que movesse todos os elementos de um tipo de contêiner para criar outro tipo de contêiner com os mesmos valores:

template <class C1, class C2>
C1
move_each(C2&& c2)
{
    return C1(std::make_move_iterator(c2.begin()),
              std::make_move_iterator(c2.end()));
}

Legal, agora posso criar um a vector<string>partir de a com relativa eficiência deque<string>e cada indivíduo stringserá movido no processo.

Mas e se eu quiser mudar de um map?

int
main()
{
    std::map<int, std::string> m{{1, "one"}, {2, "two"}, {3, "three"}};
    auto v = move_each<std::vector<std::pair<int, std::string>>>(m);
    for (auto const& p : v)
        std::cout << "{" << p.first << ", " << p.second << "} ";
    std::cout << '\n';
}

Se std::moveinsistido em um não- constargumento, a instanciação acima de move_eachnão seria compilada porque está tentando mover a const int(o key_typede map). Mas este código não se importa se ele não pode mover o key_type. Ele deseja mover o mapped_type( std::string) por motivos de desempenho.

É para este exemplo, e incontáveis ​​outros exemplos como este na codificação genérica, que std::moveé uma solicitação para mover , não uma demanda para mover.

Howard Hinnant
fonte
2

Tenho a mesma preocupação do OP.

std :: move não move um objeto, nem garante que o objeto seja móvel. Então, por que é chamado de movimento?

Acho que não ser móvel pode ser um dos seguintes dois cenários:

1. O tipo de movimento é constante.

A razão de termos a palavra-chave const na linguagem é que queremos que o compilador evite qualquer alteração em um objeto definido como const. Dado o exemplo no livro de Scott Meyers:

    class Annotation {
    public:
     explicit Annotation(const std::string text)
     : value(std::move(text)) // "move" text into value; this code
     {  } // doesn't do what it seems to!    
     
    private:
     std::string value;
    };

O que significa literalmente? Mova uma string const para o membro de valor - pelo menos, foi o que entendi antes de ler a explicação.

Se a linguagem pretende não se mover ou não garantir que o movimento seja aplicável quando std :: move () é chamado, então é literalmente enganoso ao usar a palavra mover.

Se a linguagem está encorajando as pessoas que usam std :: move a ter melhor eficiência, ela deve evitar armadilhas como essa o mais cedo possível, especialmente para esse tipo de contradição literal óbvia.

Concordo que as pessoas devem estar cientes de que mover uma constante é impossível, mas essa obrigação não deve implicar que o compilador possa ficar em silêncio quando ocorrer uma contradição óbvia.

2. O objeto não tem construtor de movimento

Pessoalmente, acho que esta é uma história diferente da preocupação da OP, como disse Chris Drew

@hvd Isso parece um pouco sem argumento para mim. Só porque a sugestão do OP não corrige todos os bugs do mundo, não significa necessariamente que seja uma má ideia (provavelmente é, mas não pelo motivo que você deu). - Chris Drew

Bogeegee
fonte
1

Estou surpreso que ninguém mencionou o aspecto de compatibilidade com versões anteriores disso. Eu acredito que std::movefoi projetado propositalmente para fazer isso em C ++ 11. Imagine que você está trabalhando com uma base de código legada, que depende fortemente de bibliotecas C ++ 98, portanto, sem o fallback na atribuição de cópia, mover quebraria as coisas.

mlo
fonte