c++ - ¿Se requiere std:: unique_ptr<T> para conocer la definición completa de T?
visual-studio-2010 c++11 (7)
Tengo un código en un encabezado que se parece a esto:
#include <memory>
class Thing;
class MyClass
{
std::unique_ptr< Thing > my_thing;
};
Si incluyo este encabezado en un cpp que no incluye la definición del tipo de Thing
, entonces esto no se compila bajo VS2010-SP1:
1> C: / Archivos de programa (x86) / Microsoft Visual Studio 10.0 / VC / include / memory (2067): error C2027: uso de tipo indefinido ''Cosa''
Reemplace std::unique_ptr
por std::shared_ptr
y compila.
Entonces, supongo que es la implementación actual de VS2010 std::unique_ptr
la que requiere la definición completa y es totalmente dependiente de la implementación.
¿O es eso? ¿Hay algo en sus requisitos estándar que hace imposible que la implementación de std::unique_ptr
funcione solo con una declaración de reenvío? Se siente extraño, ya que solo debe mantener un puntero a la Thing
, ¿no es así?
Adoptado desde here .
La mayoría de las plantillas en la biblioteca estándar de C ++ requieren que sean instanciadas con tipos completos. Sin embargo, shared_ptr
y unique_ptr
son excepciones parciales . Algunos, pero no todos sus miembros, pueden ser instanciados con tipos incompletos. La motivación para esto es admitir expresiones idiomáticas como pimpl usando punteros inteligentes y sin arriesgarse a comportamientos indefinidos.
El comportamiento indefinido puede ocurrir cuando tienes un tipo incompleto y llamas a delete
:
class A;
A* a = ...;
delete a;
Lo anterior es código legal. Se compilará. Su compilador puede o no emitir una advertencia para el código anterior como el anterior. Cuando se ejecuta, probablemente sucederán cosas malas. Si tienes mucha suerte tu programa se estrellará. Sin embargo, un resultado más probable es que su programa perderá silenciosamente la memoria ya que no se llamará a ~A()
.
El uso de auto_ptr<A>
en el ejemplo anterior no ayuda. Aún obtienes el mismo comportamiento indefinido como si hubieras usado un puntero en bruto.
¡Sin embargo, usar clases incompletas en ciertos lugares es muy útil! Aquí es donde shared_ptr
y unique_ptr
ayudan. El uso de uno de estos punteros inteligentes le permitirá salir con un tipo incompleto, excepto cuando sea necesario tener un tipo completo. Y lo más importante, cuando es necesario tener un tipo completo, se obtiene un error en tiempo de compilación si intenta usar el puntero inteligente con un tipo incompleto en ese momento.
No más comportamiento indefinido:
Si su código se compila, entonces ha usado un tipo completo donde lo necesite.
class A
{
class impl;
std::unique_ptr<impl> ptr_; // ok!
public:
A();
~A();
// ...
};
shared_ptr
y unique_ptr
requieren un tipo completo en diferentes lugares. Las razones son oscuras, que tienen que ver con un eliminador dinámico frente a un eliminador estático. Las razones precisas no son importantes. De hecho, en la mayoría de los códigos no es realmente importante saber exactamente dónde se requiere un tipo completo. Solo codifica, y si te equivocas, el compilador te lo dirá.
Sin embargo, en caso de que sea útil para usted, aquí hay una tabla que documenta a varios miembros de shared_ptr
y unique_ptr
con respecto a los requisitos de integridad. Si el miembro requiere un tipo completo, entonces la entrada tiene una "C", de lo contrario, la entrada de la tabla se rellena con "I".
Complete type requirements for unique_ptr and shared_ptr
unique_ptr shared_ptr
+------------------------+---------------+---------------+
| P() | I | I |
| default constructor | | |
+------------------------+---------------+---------------+
| P(const P&) | N/A | I |
| copy constructor | | |
+------------------------+---------------+---------------+
| P(P&&) | I | I |
| move constructor | | |
+------------------------+---------------+---------------+
| ~P() | C | I |
| destructor | | |
+------------------------+---------------+---------------+
| P(A*) | I | C |
+------------------------+---------------+---------------+
| operator=(const P&) | N/A | I |
| copy assignment | | |
+------------------------+---------------+---------------+
| operator=(P&&) | C | I |
| move assignment | | |
+------------------------+---------------+---------------+
| reset() | C | I |
+------------------------+---------------+---------------+
| reset(A*) | C | C |
+------------------------+---------------+---------------+
Cualquier operación que requiera conversiones de puntero requiere tipos completos tanto para unique_ptr
como shared_ptr
.
El unique_ptr<A>{A*}
puede obtener una A
incompleta solo si no se requiere que el compilador configure una llamada a ~unique_ptr<A>()
. Por ejemplo, si pones el unique_ptr
en el montón, puede salirse con una A
incompleta. Más detalles sobre este punto se pueden encontrar en BarryTheHatchet''s respuesta BarryTheHatchet''s here .
Como para mí,
QList<QSharedPointer<ControllerBase>> controllers;
Solo incluye el encabezado ...
#include <QSharedPointer>
El compilador necesita la definición de Cosa para generar el destructor predeterminado para MyClass. Si declara explícitamente el destructor y mueve su implementación (vacía) al archivo CPP, el código debe compilarse.
Esto no depende de la implementación. La razón por la que funciona es porque shared_ptr
determina el destructor correcto para llamar en tiempo de ejecución, no es parte de la firma de tipo. Sin embargo, el unique_ptr
de unique_ptr
es parte de su tipo, y debe ser conocido en tiempo de compilación.
La definición completa de la Cosa se requiere en el punto de creación de instancias de la plantilla. Esta es la razón exacta por la cual se compila el lenguaje pimpl.
Si no fuera posible, la gente no haría preguntas como this .
Parece que las respuestas actuales no están precisando exactamente por qué el constructor (o destructor) predeterminado es un problema, pero las vacías declaradas en cpp no lo son.
Esto es lo que está pasando:
Si la clase externa (es decir, MyClass) no tiene constructor o destructor, el compilador genera los predeterminados. El problema con esto es que el compilador esencialmente inserta el constructor / destructor vacío predeterminado en el archivo .hpp. Esto significa que el código para el controlador / destructor predeterminado se compila junto con el binario del ejecutable del host, no junto con los binarios de su biblioteca. Sin embargo, estas definiciones realmente no pueden construir las clases parciales. Entonces, cuando el enlazador entra en el binario de su biblioteca y trata de obtener constructor / destructor, no encuentra ninguno y se produce un error. Si el código del constructor / destructor estaba en su .cpp, entonces el binario de su biblioteca tiene ese enlace disponible.
Por lo tanto, esto no tiene nada que ver con el uso de unique_ptr en lugar de shared_ptr para el escenario anterior siempre que esté usando compiladores modernos (el antiguo compilador VC ++ puede tener un error en la implementación de unique_ptr pero VC ++ 2015 funciona bien en mi máquina).
La moraleja de la historia es que su encabezado debe permanecer libre de cualquier definición de constructor / destructor. Sólo puede contener su declaración. Por ejemplo, ~MyClass()=default;
en hpp no funcionará. Si permite que el compilador inserte el constructor o destructor predeterminado, obtendrá un error de vinculador.
Otra nota al margen: si aún recibe este error incluso después de tener el constructor y el destructor en el archivo cpp, lo más probable es que el motivo sea que la biblioteca no se está compilando correctamente. Por ejemplo, una vez simplemente cambié el tipo de proyecto de Consola a Biblioteca en VC ++ y obtuve este error porque VC ++ no agregó el símbolo del preprocesador _LIB y eso produjo exactamente el mismo mensaje de error.
Sólo para estar completo:
Encabezado: Ah
class B; // forward declaration
class A
{
std::unique_ptr<B> ptr_; // ok!
public:
A();
~A();
// ...
};
Fuente A.cpp:
class B { ... }; // class definition
A::A() { ... }
A::~A() { ... }
La definición de clase B debe ser vista por el constructor, el destructor y cualquier cosa que pueda eliminar implícitamente B. (Aunque el constructor no aparece en la lista anterior, en VS2017 incluso el constructor necesita la definición de B. Y esto tiene sentido cuando se considera que en caso de una excepción en el constructor, el unique_ptr se destruye de nuevo.)