¿Por qué podemos usar `std :: move` en un objeto` const`?

113

En C ++ 11, podemos escribir este código:

struct Cat {
   Cat(){}
};

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

cuando llamo std::move, significa que quiero mover el objeto, es decir, cambiaré el objeto. Mover un constobjeto no es razonable, entonces, ¿por qué std::moveno restringe este comportamiento? Será una trampa en el futuro, ¿verdad?

Aquí trampa significa como Brandon mencionó en el comentario:

"Creo que lo que quiere decir es que lo" atrapa "furtivamente, porque si no se da cuenta, termina con una copia que no es lo que pretendía".

En el libro 'Effective Modern C ++' de Scott Meyers, da un ejemplo:

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;
};

Si std::moveestuviera prohibido operar en un constobjeto, podríamos descubrir fácilmente el error, ¿verdad?

camino
fuente
2
Pero intenta moverlo. Intente cambiar su estado. std::movepor sí solo no le hace nada al objeto. Se podría argumentar que std::moveestá mal nombrado.
juanchopanza
3
En realidad, no mueve nada. Todo lo que hace es convertir a una referencia rvalue. intente CAT cat2 = std::move(cat);, asumiendo que CATadmite la asignación de movimiento regular.
WhozCraig
11
std::movees solo un yeso, en realidad no mueve nada
Alerta roja
2
@WhozCraig: Cuidado, ya que el código que publicaste se compila y ejecuta sin previo aviso, lo que lo hace algo engañoso.
Mooing Duck
1
@MooingDuck Nunca dijo que no se compilaría. funciona solo porque el copy-ctor predeterminado está habilitado. Silencia eso y las ruedas se caen.
WhozCraig

Respuestas:

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);

aquí vemos un uso de std::moveen a T const. Devuelve un T const&&. Tenemos un constructor de movimientos strangeque toma exactamente este tipo.

Y se llama.

Ahora bien, es cierto que este tipo extraño es más raro que los errores que solucionaría tu propuesta.

Pero, por otro lado, el existente std::movefunciona mejor en código genérico, donde no sabes si el tipo con el que estás trabajando es a To a T const.

Yakk - Adam Nevraumont
fuente
3
+1 por ser la primera respuesta que realmente intenta explicar por qué querrías llamar std::movea un constobjeto.
Chris Drew
+1 para mostrar una función tomando const T&&. Esto expresa un "protocolo API" del tipo "Tomaré un rvalue-ref pero prometo que no lo modificaré". Supongo que, aparte de cuando se usa mutable, es poco común. Quizás otro caso de uso es poder usarlo forward_as_tupleen casi cualquier cosa y luego usarlo.
F Pereira
107

Hay un truco aquí que estás pasando por alto, es decir, que en std::move(cat) realidad no mueve nada . Simplemente le dice al compilador que intente moverse. Sin embargo, dado que su clase no tiene un constructor que acepte a const CAT&&, en su lugar utilizará el const CAT&constructor de copia implícito y copiará de forma segura. Sin peligro, sin trampa. Si el constructor de copia está deshabilitado por cualquier motivo, obtendrá un error de 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);
}

impresiones COPY, no MOVE.

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

Tenga en cuenta que el error en el código que menciona es un problema de rendimiento , no un problema de estabilidad , por lo que dicho error nunca causará un bloqueo. Solo usará una copia más lenta. Además, este error también ocurre para objetos no constantes que no tienen constructores de movimiento, por lo que simplemente agregar una constsobrecarga no los detectará a todos. Podríamos comprobar la capacidad de mover construir o mover asignar desde el tipo de parámetro, pero eso interferiría con el código de plantilla genérico que se supone que recurre al constructor de copia. Y diablos, tal vez alguien quiera poder construir const CAT&&, ¿quién soy yo para decir que no puede?

Pato morando
fuente
Mejorado. Vale la pena señalar que la eliminación implícita del copy-ctor en la definición definida por el usuario del constructor de movimiento regular o el operador de asignación también demostrará esto a través de una compilación rota. Buena respuesta.
WhozCraig
También vale la pena mencionar que un constructor de copias que necesita un valor que no sea constl tampoco ayudará. [class.copy] §8: "De lo contrario, el constructor de copia declarado implícitamente tendrá la forma X::X(X&)"
Desduplicador
9
No creo que quisiera decir "trampa" en términos de computadora / ensamblaje. Creo que lo que quiere decir es que lo "atrapa" furtivamente furtivo porque si no se da cuenta, termina con una copia que no es lo que pretendía. Supongo ...
Brandon
que obtengas 1 voto a favor más, y yo obtengo 4 más, por una insignia de oro. ;)
Yakk - Adam Nevraumont
Choca esos cinco en ese código de muestra. Entiende muy bien el punto.
Brent escribe código
20

Una razón por la que el resto de las respuestas se ha pasado por alto hasta ahora es la capacidad del código genérico para ser resistente frente al movimiento. Por ejemplo, digamos que quería escribir una función genérica que moviera todos los elementos de un tipo de contenedor para crear otro tipo de contenedor con los mismos 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()));
}

Genial, ahora puedo crear de forma relativamente eficiente un vector<string>desde a deque<string>y cada individuo se stringmoverá en el proceso.

Pero, ¿y si quiero pasar de un 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';
}

Si se std::moveinsiste en un no constargumento, la instanciación anterior de move_eachno se compilaría porque está intentando mover a const int(el key_typede map). Pero a este código no le importa si no puede mover el key_type. Quiere mover el mapped_type( std::string) por motivos de rendimiento.

Es para este ejemplo, y otros innumerables ejemplos como este en la codificación genérica, que std::movees una solicitud para moverse , no una demanda para moverse.

Howard Hinnant
fuente
2

Tengo la misma preocupación que el OP.

std :: move no mueve un objeto, ni garantiza que el objeto sea movible. Entonces, ¿por qué se llama mover?

Creo que no ser movible puede ser uno de los siguientes dos escenarios:

1. El tipo móvil es constante.

La razón por la que tenemos la palabra clave const en el lenguaje es que queremos que el compilador evite cualquier cambio en un objeto definido como const. Dado el ejemplo en el libro 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;
    };

¿Qué significa literalmente? Mueva una cadena constante al miembro de valor; al menos, eso es lo que entiendo antes de leer la explicación.

Si el lenguaje tiene la intención de no mover o no garantizar que el movimiento sea aplicable cuando se llama a std :: move (), entonces es literalmente engañoso cuando se usa la palabra mover.

Si el lenguaje está animando a las personas que usan std :: move a tener una mayor eficiencia, tiene que prevenir trampas como esta lo antes posible, especialmente para este tipo de contradicción literal obvia.

Estoy de acuerdo en que la gente debe saber que mover una constante es imposible, pero esta obligación no debe implicar que el compilador pueda guardar silencio cuando ocurre una contradicción obvia.

2. El objeto no tiene constructor de movimiento

Personalmente, creo que esta es una historia separada de la preocupación de OP, como dijo Chris Drew.

@hvd Eso me parece un poco sin argumento. El hecho de que la sugerencia de OP no solucione todos los errores del mundo no significa necesariamente que sea una mala idea (probablemente lo sea, pero no por la razón que usted da). - Chris Drew

Bogeegee
fuente
1

Me sorprende que nadie haya mencionado el aspecto de compatibilidad con versiones anteriores de esto. Creo que std::movefue diseñado a propósito para hacer esto en C ++ 11. Imagine que está trabajando con una base de código heredada, que depende en gran medida de las bibliotecas de C ++ 98, por lo que sin el respaldo de la asignación de copia, la mudanza rompería las cosas.

mlo
fuente