¿Por qué no hay asignación de movimiento / constructor de movimiento predeterminado?

89

Soy un simple programador. Las variables de mis miembros de clase a menudo consisten en tipos POD y contenedores STL. Debido a esto, rara vez tengo que escribir operadores de asignación o copiar constructores, ya que estos se implementan de forma predeterminada.

Agregue a esto, si uso std::moveen objetos no móviles, utiliza el operador de asignación, lo que significa que std::movees perfectamente seguro.

Como soy un programador simple, me gustaría aprovechar las capacidades de movimiento sin agregar un constructor de movimiento / operador de asignación a cada clase que escribo, ya que el compilador podría simplemente implementarlos como " this->member1_ = std::move(other.member1_);..."

Pero no es así (al menos no en Visual 2010), ¿hay alguna razón en particular para esto?

Más importante; ¿Hay alguna forma de evitar esto?

Actualización: si observa la respuesta de GManNickG, proporciona una gran macro para esto. Y si no lo sabía, si implementa la semántica de movimiento, puede eliminar la función de miembro de intercambio.

Viktor Sehr
fuente
5
usted sabe que puede hacer que el compilador genere un movimiento predeterminado ctor
aaronman
3
std :: move no realiza un movimiento, simplemente lanza de un valor l a un valor r. El movimiento aún lo realiza el constructor de movimientos.
Owen Delahoy
1
Estás hablando MyClass::MyClass(Myclass &&) = default;?
Sandburg
Sí, hoy en día :)
Viktor Sehr

Respuestas:

76

La generación implícita de constructores de movimientos y operadores de asignación ha sido polémica y ha habido importantes revisiones en borradores recientes del estándar C ++, por lo que los compiladores actualmente disponibles probablemente se comportarán de manera diferente con respecto a la generación implícita.

Para obtener más información sobre la historia del problema, consulte la lista de documentos del WG21 de 2010 y busque "mov"

La especificación actual (N3225, de noviembre) establece (N3225 12.8 / 8):

Si la definición de una clase Xno declara explícitamente un constructor de movimiento, uno será declarado implícitamente como predeterminado si y solo si

  • X no tiene un constructor de copia declarado por el usuario, y

  • X no tiene un operador de asignación de copia declarado por el usuario,

  • X no tiene un operador de asignación de movimiento declarado por el usuario,

  • X no tiene un destructor declarado por el usuario, y

  • el constructor de movimiento no se definiría implícitamente como eliminado.

Hay un lenguaje similar en 12.8 / 22 que especifica cuándo el operador de asignación de movimiento se declara implícitamente como predeterminado. Puede encontrar la lista completa de cambios realizados para respaldar la especificación actual de generación de movimientos implícitos en N3203: Apretar las condiciones para generar movimientos implícitos , que se basó en gran medida en una de las resoluciones propuestas por el artículo N3201 de Bjarne Stroustrup : Avanzar a la derecha .

James McNellis
fuente
4
Escribí un pequeño artículo con algunos diagramas que describen las relaciones para el constructor / asignación implícito (mover) aquí: mmocny.wordpress.com/2010/12/09/implicit-move-wont-go
mmocny
Ugh, así que siempre que tengo que definir destructores en blanco en clases base polimórficas solo por el hecho de especificarlo como virtual, también tengo que definir explícitamente el constructor de movimiento y el operador de asignación :(.
someguy
@James McNellis: Eso es algo que probé anteriormente, pero al compilador no pareció gustarle. Iba a publicar el mensaje de error en esta misma respuesta, pero después de intentar reproducir el error, me di cuenta de que lo menciona cannot be defaulted *in the class body*. Entonces, definí el destructor afuera y funcionó :). Aunque lo encuentro un poco extraño. ¿Alguien tiene una explicación? El compilador es gcc 4.6.1
someguy
3
¿Quizás podríamos obtener una actualización de esta respuesta ahora que C ++ 11 está ratificado? Curioso qué comportamientos ganaron.
Joseph Garvin
2
@Guy Avraham: Creo que lo que estaba diciendo (han pasado 7 años) es que si tengo un destructor declarado por el usuario (incluso uno virtual vacío), ningún constructor de movimientos se declarará implícitamente como predeterminado. ¿Supongo que eso resultaría en una semántica de copia? (No he tocado C ++ en años). James McNellis luego comentó que virtual ~D() = default;debería funcionar y aún permitir un constructor de movimiento implícito.
someguy
13

Los constructores de movimientos generados implícitamente se han considerado para el estándar, pero pueden ser peligrosos. Vea el análisis de Dave Abrahams .

Sin embargo, al final, el estándar incluyó la generación implícita de constructores de movimientos y operadores de asignación de movimientos, aunque con una lista bastante sustancial de limitaciones:

Si la definición de una clase X no declara explícitamente un constructor de movimiento, uno se declarará implícitamente como predeterminado si y solo si
- X no tiene un constructor de copia declarado por el usuario,
- X no tiene un operador de asignación de copia declarado por el usuario ,
- X no tiene un operador de asignación de movimiento declarado por el usuario,
- X no tiene un destructor declarado por el usuario, y
- el constructor de movimiento no se definiría implícitamente como eliminado.

Sin embargo, eso no es todo lo que hay en la historia. Un ctor puede declararse, pero aún así definirse como eliminado:

Un constructor de copia / movimiento declarado implícitamente es un miembro público en línea de su clase. Un constructor de copia / movimiento predeterminado para una clase X se define como eliminado (8.4.3) si X tiene:

- un miembro variante con un constructor correspondiente no trivial y X es una clase de tipo unión,
- un miembro de datos no estático de clase tipo M (o matriz del mismo) que no se puede copiar / mover debido a la resolución de sobrecarga (13.3), como aplicada al constructor correspondiente de M, da como resultado una ambigüedad o una función que se elimina o es inaccesible desde el constructor predeterminado,
- una clase básica B directa o virtual que no se puede copiar / mover debido a la resolución de sobrecarga (13.3), según se aplica al constructor correspondiente de B , da como resultado una ambigüedad o una función que se elimina o es inaccesible desde el constructor predeterminado, - para el constructor de copia, un miembro de datos no estático del tipo de referencia rvalue, o
- cualquier clase base directa o virtual o miembro de datos no estáticos de un tipo con un destructor que es eliminado o inaccesible desde el constructor predeterminado,

- para el constructor de movimiento, un miembro de datos no estático o una clase base directa o virtual con un tipo que no tiene un constructor de movimiento y no se puede copiar trivialmente.

Jerry Coffin
fuente
El borrador de trabajo actual permite la generación de movimientos implícitos bajo ciertas condiciones y creo que la resolución aborda en gran medida las preocupaciones de Abrahams.
James McNellis
No estoy seguro de haber entendido qué movimiento puede romperse en el ejemplo entre Tweak 2 y Tweak 3. ¿Podría explicarlo?
Matthieu M.
@Matthieu M .: tanto Tweak 2 como Tweak 3 están rotos, y de manera bastante similar, en realidad. En Tweak 2, hay miembros privados con invariantes que pueden romperse con el movimiento ctor. En Tweak 3, la clase no tiene miembros privados en sí misma , pero dado que usa herencia privada, los miembros públicos y protegidos de la base se convierten en miembros privados del derivado, lo que lleva al mismo problema.
Jerry Coffin
1
Realmente no entendí cómo el constructor de movimientos rompería la clase invariante Tweak2. Supongo que tiene algo que ver con el hecho de que Numberse movería y vectorse copiaría ... pero no estoy seguro: / Entiendo que el problema se propagaría en cascada Tweak3.
Matthieu M.
¿El enlace que diste parece estar muerto?
Wolf
8

(por ahora, estoy trabajando en una macro estúpida ...)

Sí, yo también fui por esa ruta. Aquí está su macro:

// detail/move_default.hpp
#ifndef UTILITY_DETAIL_MOVE_DEFAULT_HPP
#define UTILITY_DETAIL_MOVE_DEFAULT_HPP

#include <boost/preprocessor.hpp>

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE(pR, pData, pBase) pBase(std::move(pOther))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE(pR, pData, pBase) pBase::operator=(std::move(pOther));

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR(pR, pData, pMember) pMember(std::move(pOther.pMember))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT(pR, pData, pMember) pMember = std::move(pOther.pMember);

#define UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        ,                                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)                                                   \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#endif

// move_default.hpp
#ifndef UTILITY_MOVE_DEFAULT_HPP
#define UTILITY_MOVE_DEFAULT_HPP

#include "utility/detail/move_default.hpp"

// move bases and members
#define UTILITY_MOVE_DEFAULT(pT, pBases, pMembers) UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)

// base only version
#define UTILITY_MOVE_DEFAULT_BASES(pT, pBases) UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)

// member only version
#define UTILITY_MOVE_DEFAULT_MEMBERS(pT, pMembers) UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)

#endif

(Eliminé los comentarios reales, que son extensos y documentales).

Usted especifica las bases y / o miembros de su clase como una lista de preprocesadores, por ejemplo:

#include "move_default.hpp"

struct foo
{
    UTILITY_MOVE_DEFAULT_MEMBERS(foo, (x)(str));

    int x;
    std::string str;
};

struct bar : foo, baz
{
    UTILITY_MOVE_DEFAULT_BASES(bar, (foo)(baz));
};

struct baz : bar
{
    UTILITY_MOVE_DEFAULT(baz, (bar), (ptr));

    void* ptr;
};

Y sale un constructor de movimientos y un operador de asignación de movimientos.

(Aparte, si alguien sabe cómo podría combinar los detalles en una macro, sería genial).

GManNickG
fuente
Muchas gracias, el mío es bastante similar, excepto que tuve que pasar el número de variables miembro como argumento (lo que realmente apesta).
Viktor Sehr
1
@ Viktor: No hay problema. Si no es demasiado tarde, creo que debería marcar una de las otras respuestas como aceptada. El mío era más un "por cierto, aquí hay un camino" y no una respuesta a tu pregunta real.
GManNickG
1
Si leo su macro correctamente, tan pronto como su compilador implemente miembros de movimiento predeterminados, sus ejemplos anteriores no se podrán copiar. La generación implícita de miembros de copia se inhibe cuando hay presentes miembros de movimiento declarados explícitamente.
Howard Hinnant
@Howard: Está bien, es una solución temporal hasta entonces. :)
GManNickG
GMan: esta macro agrega moveconstructor \ assign si tiene una función de intercambio:
Viktor Sehr
4

VS2010 no lo hace porque no eran estándar en el momento de la implementación.

Perrito
fuente