¿Se requiere std :: unique_ptr <T> para conocer la definición completa de T?

248

Tengo un código en un encabezado que se ve así:

#include <memory>

class Thing;

class MyClass
{
    std::unique_ptr< Thing > my_thing;
};

Si incluyo este encabezado en un cpp que no incluye la Thingdefinición de tipo, entonces esto no se compila en VS2010-SP1:

1> C: \ Archivos de programa (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): error C2027: uso de tipo indefinido 'Thing'

Reemplazar std::unique_ptrpor std::shared_ptry se compila.

Entonces, supongo que es la std::unique_ptrimplementación actual del VS2010 la que requiere la definición completa y depende totalmente de la implementación.

¿O es eso? ¿Hay algo en sus requisitos estándar que hace imposible que std::unique_ptrla implementación funcione solo con una declaración directa? Se siente extraño ya que solo debería contener un puntero Thing, ¿no?

Klaim
fuente
20
La mejor explicación de cuándo necesita y no necesita un tipo completo con los punteros inteligentes C ++ 0x es "Tipos incompletos y shared_ptr/ unique_ptr" de Howard Hinnant. La tabla al final debe responder a su pregunta.
James McNellis
17
Gracias por el puntero James. ¡Había olvidado dónde puse esa mesa! :-)
Howard Hinnant
55
@JamesMcNellis El enlace al sitio web de Howard Hinnant no funciona. Aquí está la versión web.archive.org de la misma. En cualquier caso, respondió perfectamente a continuación con el mismo contenido :-)
Ela782
Otra buena explicación se da en el ítem 22 de C ++ moderno efectivo de Scott Meyers.
Fred Schoen

Respuestas:

328

Adoptado desde aquí .

La mayoría de las plantillas en la biblioteca estándar de C ++ requieren que se instancian con tipos completos. Sin embargo shared_ptry unique_ptrson excepciones parciales . Algunos, pero no todos sus miembros pueden ser instanciados con tipos incompletos. La motivación para esto es apoyar modismos como pimpl usando punteros inteligentes y sin arriesgar comportamientos indefinidos.

Un comportamiento indefinido puede ocurrir cuando tiene un tipo incompleto y lo llama delete:

class A;
A* a = ...;
delete a;

Lo anterior es un 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 bloqueará. Sin embargo, un resultado más probable es que su programa perderá silenciosamente la memoria como ~A()no se llamará.

Usar auto_ptr<A>en el ejemplo anterior no ayuda. Todavía obtienes el mismo comportamiento indefinido como si hubieras usado un puntero sin formato.

Sin embargo, ¡usar clases incompletas en ciertos lugares es muy útil! Aquí es donde shared_ptry unique_ptrayuda. El uso de uno de estos punteros inteligentes le permitirá escapar con un tipo incompleto, excepto cuando sea necesario tener un tipo completo. Y lo más importante, cuando es necesario tener un tipo completo, obtiene un error en tiempo de compilación si intenta usar el puntero inteligente con un tipo incompleto en ese punto.

No más comportamientos indefinidos:

Si su código se compila, entonces ha utilizado un tipo completo en todas partes donde lo necesita.

class A
{
    class impl;
    std::unique_ptr<impl> ptr_;  // ok!

public:
    A();
    ~A();
    // ...
};

shared_ptry unique_ptrrequieren un tipo completo en diferentes lugares. Las razones son oscuras y 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 que sepa exactamente dónde se requiere un tipo completo. Solo codifique, y si se equivoca, el compilador le dirá.

Sin embargo, en caso de que le sea útil, aquí hay una tabla que documenta a varios miembros shared_ptry unique_ptrcon respecto a los requisitos de integridad. Si el miembro requiere un tipo completo, 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 para ambos unique_ptry shared_ptr.

El unique_ptr<A>{A*}constructor puede salirse con una incompleta Asolo si no se requiere que el compilador configure una llamada a ~unique_ptr<A>(). Por ejemplo, si coloca el unique_ptren el montón, puede salirse con una incompleta A. Se pueden encontrar más detalles sobre este punto en la respuesta de BarryTheHatchet aquí .

Howard Hinnant
fuente
3
Excelente respuesta Lo haría +5 si pudiera. Estoy seguro de que me referiré a esto en mi próximo proyecto, en el que estoy tratando de hacer un uso completo de los punteros inteligentes.
Matthias
44
si uno puede explicar lo que significa la tabla, supongo que ayudará a más personas
Ghita
8
Una nota más: un constructor de clase hará referencia a los destructores de sus miembros (para el caso en el que se produce una excepción, es necesario llamar a esos destructores). Entonces, aunque el destructor de unique_ptr necesita un tipo completo, no es suficiente tener un destructor definido por el usuario en una clase, sino que también necesita un constructor.
Johannes Schaub - litb
77
@Mehrdad: Esta decisión se tomó para C ++ 98, que es anterior a mi tiempo. Sin embargo, creo que la decisión provino de una preocupación sobre la implementabilidad y la dificultad de la especificación (es decir, exactamente qué partes de un contenedor requieren o no un tipo completo). Incluso hoy, con 15 años de experiencia desde C ++ 98, sería una tarea no trivial relajar la especificación del contenedor en esta área y garantizar que no se prohíban importantes técnicas de implementación u optimizaciones. Creo que se puede hacer. Yo que sería mucho trabajo. Soy consciente de una persona haciendo el intento.
Howard Hinnant
99
Porque no es obvio a partir de los comentarios anteriores, para cualquier persona que tiene este problema, ya que definen una unique_ptrcomo una variable miembro de una clase, simplemente explícitamente declarar un destructor (y constructor) en la declaración de clase (en el archivo de cabecera) y proceder a definir las en el archivo de origen (y coloque el encabezado con la declaración completa de la clase apuntada en el archivo de origen) para evitar que el compilador incluya automáticamente el constructor o destructor en el archivo de encabezado (que desencadena el error). stackoverflow.com/a/13414884/368896 también me ayuda a recordar esto.
Dan Nissenbaum
42

El compilador necesita la definición de Thing 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.

Igor Nazarenko
fuente
55
Creo que esta es la oportunidad perfecta para usar una función predeterminada. MyClass::~MyClass() = default;en el archivo de implementación, parece menos probable que alguien que asuma que el cuerpo del dispositivo se borró accidentalmente en lugar de dejarlo en blanco deliberadamente lo elimine inadvertidamente.
Dennis Zickefoose
@Dennis Zickefoose: Desafortunadamente, el OP está usando VC ++, y VC ++ aún no admite miembros de clase defaulted deleteyd.
ildjarn
66
+1 para saber cómo mover la puerta al archivo .cpp. También parece MyClass::~MyClass() = defaultque no lo mueve al archivo de implementación en Clang. (¿todavía?)
Eonil
También debe mover la implementación del constructor al archivo CPP, al menos en VS 2017. Vea, por ejemplo, esta respuesta: stackoverflow.com/a/27624369/5124002
jciloa
15

Esto no depende de la implementación. La razón por la que funciona es porque shared_ptrdetermina el destructor correcto para llamar en tiempo de ejecución: no es parte de la firma de tipo. Sin embargo, unique_ptrel destructor es parte de su tipo y debe conocerse en tiempo de compilación.

Perrito
fuente
8

Parece que las respuestas actuales no están aclarando exactamente por qué el constructor predeterminado (o destructor) es un problema, pero las vacías declaradas en cpp no ​​lo son.

Aquí está lo que está sucediendo:

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 contructor / 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 va en el binario de su biblioteca e intenta obtener el constructor / destructor, no encuentra ninguno y obtiene un error. Si el código del constructor / destructor estaba en su .cpp, entonces el binario de su biblioteca lo tiene disponible para vincular.

Esto no tiene nada que ver con el uso de unique_ptr o shared_ptr y otras respuestas parecen ser posibles errores confusos en el antiguo VC ++ para la implementación de unique_ptr (VC ++ 2015 funciona bien en mi máquina).

Entonces, la moraleja de la historia es que su encabezado debe permanecer libre de cualquier definición de constructor / destructor. Solo puede contener su declaración. Por ejemplo, ~MyClass()=default;en hpp no ​​funcionará. Si permite que el compilador inserte un 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 su 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.

Shital Shah
fuente
¡Gracias! Esa fue una explicación muy sucinta de una peculiaridad de C ++ increíblemente oscura. Me salvó muchos problemas.
JPNotADragon
5

Solo para completar:

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 la 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 nuevamente).

Joaquín
fuente
1

Se requiere la definición completa de la Cosa en el punto de creación de instancias de plantilla. Esta es la razón exacta por la cual compila el modismo de pimpl.

Si no fuera posible, la gente no haría preguntas como esta .

BЈовић
fuente
-2

La respuesta simple es simplemente usar shared_ptr en su lugar.

deltanine
fuente
-7

Como para mí,

QList<QSharedPointer<ControllerBase>> controllers;

Solo incluye el encabezado ...

#include <QSharedPointer>
Sanbrother
fuente
Respuesta no relacionada y no relevante para la pregunta.
Mikus