std :: unique_ptr con un tipo incompleto no se compilará

203

Estoy usando el pimpl-idiom con std::unique_ptr:

class window {
  window(const rectangle& rect);

private:
  class window_impl; // defined elsewhere
  std::unique_ptr<window_impl> impl_; // won't compile
};

Sin embargo, recibo un error de compilación con respecto al uso de un tipo incompleto, en la línea 304 en <memory>:

Aplicación no válida de ' sizeof' a un tipo incompleto ' uixx::window::window_impl'

Por lo que sé, std::unique_ptrdebería poder usarse con un tipo incompleto. ¿Es esto un error en libc ++ o estoy haciendo algo mal aquí?


fuente
Enlace de referencia para los requisitos de integridad: stackoverflow.com/a/6089065/576911
Howard Hinnant
1
Un pimpl a menudo se construye y no se modifica desde entonces. Usualmente uso un std :: shared_ptr <const window_impl>
mfnx
Relacionado: Me gustaría saber por qué esto funciona en MSVC y cómo evitar que funcione (para no romper las compilaciones de mis colegas del CCG).
Len

Respuestas:

258

Aquí hay algunos ejemplos de std::unique_ptrtipos incompletos. El problema radica en la destrucción.

Si usa pimpl con unique_ptr, debe declarar un destructor:

class foo
{ 
    class impl;
    std::unique_ptr<impl> impl_;

public:
    foo(); // You may need a def. constructor to be defined elsewhere

    ~foo(); // Implement (with {}, or with = default;) where impl is complete
};

porque de lo contrario, el compilador genera uno predeterminado y necesita una declaración completa de foo::implpara esto.

Si tienes constructores de plantillas, entonces estás jodido, incluso si no construyes el impl_miembro:

template <typename T>
foo::foo(T bar) 
{
    // Here the compiler needs to know how to
    // destroy impl_ in case an exception is
    // thrown !
}

En el ámbito del espacio de nombres, el uso unique_ptrtampoco funcionará:

class impl;
std::unique_ptr<impl> impl_;

ya que el compilador debe saber aquí cómo destruir este objeto de duración estática. Una solución alternativa es:

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;
Alexandre C.
fuente
3
Creo que su primera solución (agregar el destructor foo ) permite que se compile la declaración de clase, pero declarar un objeto de ese tipo en cualquier lugar da como resultado el error original ("aplicación no válida de 'sizeof' ...").
Jeff Trull
38
excelente respuesta, solo para notar; todavía podemos usar el constructor / destructor predeterminado colocando, por ejemplo, foo::~foo() = default;en el archivo src
assem
2
Una forma de vivir con los constructores de plantillas sería declarar pero no definir el constructor en el cuerpo de la clase, definirlo en algún lugar donde se vea la definición impl completa e instanciar explícitamente todas las instancias necesarias allí.
enobayram
2
¿Podría explicar cómo funcionaría esto en algunos casos y no en otros? He usado el idioma pimpl con un unique_ptr y una clase sin destructor, y en otro proyecto mi código no puede compilarse con el error OP mencionado ..
Curioso
1
Parece que si el valor predeterminado para unique_ptr se establece en {nullptr} en el archivo de encabezado de la clase con estilo c ++ 11, también se necesita una declaración completa por el motivo anterior.
feirainy
53

Como mencionó Alexandre C. , el problema se reduce a que windowel destructor se define implícitamente en lugares donde el tipo de window_impltodavía está incompleto. Además de sus soluciones, otra solución alternativa que he usado es declarar un functor Deleter en el encabezado:

// Foo.h

class FooImpl;
struct FooImplDeleter
{
  void operator()(FooImpl *p);
};

class Foo
{
...
private:
  std::unique_ptr<FooImpl, FooImplDeleter> impl_;
};

// Foo.cpp

...
void FooImplDeleter::operator()(FooImpl *p)
{
  delete p;
}

Tenga en cuenta que el uso de una función Deleter personalizada impide el uso de std::make_unique(disponible en C ++ 14), como ya se discutió aquí .

Fernando Costa Bertoldi
fuente
66
Esta es la solución correcta en lo que a mí respecta. No es exclusivo del uso de pimpl-idiom, es un problema general con el uso de std :: unique_ptr con clases incompletas. El eliminador predeterminado utilizado por std :: unique_ptr <X> intenta hacer "eliminar X", lo que no puede hacer si X es una declaración de reenvío. Al especificar una función de eliminación, puede colocar esa función en un archivo fuente donde la clase X está completamente definida. Otros archivos fuente pueden usar std :: unique_ptr <X, DeleterFunc> a pesar de que X es solo una declaración directa siempre que estén vinculados con el archivo fuente que contiene DeleterFunc.
sheltond
1
Esta es una buena solución cuando debe tener una definición de función en línea creando una instancia de su tipo "Foo" (por ejemplo, un método estático "getInstance" que hace referencia al constructor y destructor), y no desea moverlos a un archivo de implementación como @ adspx5 sugiere.
GameSalutes
20

usar un eliminador personalizado

El problema es que unique_ptr<T>debe llamar al destructor T::~T()en su propio destructor, su operador de asignación de movimiento y la unique_ptr::reset()función miembro (solo). Sin embargo, estos deben llamarse (implícita o explícitamente) en varias situaciones PIMPL (ya en el destructor de la clase externa y el operador de asignación de movimiento).

Como ya se ha señalado en otra respuesta, una forma de evitar que se va a mover todas las operaciones que requieren unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)y unique_ptr::reset()en el archivo de origen, donde la clase pimpl ayudante se define realmente.

Sin embargo, esto es bastante inconveniente y desafía hasta cierto punto el punto mismo de la idolatría. Una solución mucho más limpia que evita todo eso es usar un eliminador personalizado y solo mover su definición al archivo fuente donde vive la clase de ayuda de espinillas. Aquí hay un ejemplo simple:

// file.h
class foo
{
  struct pimpl;
  struct pimpl_deleter { void operator()(pimpl*) const; };
  std::unique_ptr<pimpl,pimpl_deleter> m_pimpl;
public:
  foo(some data);
  foo(foo&&) = default;             // no need to define this in file.cc
  foo&operator=(foo&&) = default;   // no need to define this in file.cc
//foo::~foo()          auto-generated: no need to define this in file.cc
};

// file.cc
struct foo::pimpl
{
  // lots of complicated code
};
void foo::pimpl_deleter::operator()(foo::pimpl*ptr) const { delete ptr; }

En lugar de una clase de eliminación separada, también puede usar una función o staticmiembro libre foojunto con un lambda:

class foo {
  struct pimpl;
  static void delete_pimpl(pimpl*);
  std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};
Walter
fuente
15

Probablemente tenga algunos cuerpos de función dentro del archivo .h dentro de la clase que usa un tipo incompleto.

Asegúrese de que dentro de su ventana .h para clase solo tenga una declaración de función. Todos los cuerpos de función para la ventana deben estar en el archivo .cpp. Y para window_impl también ...

Por cierto, debe agregar explícitamente la declaración de destructor para la clase de Windows en su archivo .h.

Pero NO PUEDE poner el cuerpo dtor vacío en su archivo de encabezado:

class window {
    virtual ~window() {};
  }

Debe ser solo una declaración:

  class window {
    virtual ~window();
  }
adspx5
fuente
Esta fue mi solución también. Mucho más conciso. Simplemente haga que su constructor / destructor se declare en el encabezado y se defina en el archivo cpp.
Kris Morness
2

Para agregar a las respuestas del otro sobre el eliminador personalizado, en nuestra "biblioteca de utilidades" interna agregué un encabezado auxiliar para implementar este patrón común ( std::unique_ptrde tipo incompleto, conocido solo por algunos de los TU para, por ejemplo, evitar largos tiempos de compilación o para proporcionar solo un mango opaco para los clientes).

Proporciona el andamiaje común para este patrón: una clase de eliminación personalizada que invoca una función de eliminación definida externamente, un alias de tipo para unique_ptrcon esta clase de eliminación y una macro para declarar la función de eliminación en una TU que tiene una definición completa de tipo. Creo que esto tiene alguna utilidad general, así que aquí está:

#ifndef CZU_UNIQUE_OPAQUE_HPP
#define CZU_UNIQUE_OPAQUE_HPP
#include <memory>

/**
    Helper to define a `std::unique_ptr` that works just with a forward
    declaration

    The "regular" `std::unique_ptr<T>` requires the full definition of `T` to be
    available, as it has to emit calls to `delete` in every TU that may use it.

    A workaround to this problem is to have a `std::unique_ptr` with a custom
    deleter, which is defined in a TU that knows the full definition of `T`.

    This header standardizes and generalizes this trick. The usage is quite
    simple:

    - everywhere you would have used `std::unique_ptr<T>`, use
      `czu::unique_opaque<T>`; it will work just fine with `T` being a forward
      declaration;
    - in a TU that knows the full definition of `T`, at top level invoke the
      macro `CZU_DEFINE_OPAQUE_DELETER`; it will define the custom deleter used
      by `czu::unique_opaque<T>`
*/

namespace czu {
template<typename T>
struct opaque_deleter {
    void operator()(T *it) {
        void opaque_deleter_hook(T *);
        opaque_deleter_hook(it);
    }
};

template<typename T>
using unique_opaque = std::unique_ptr<T, opaque_deleter<T>>;
}

/// Call at top level in a C++ file to enable type %T to be used in an %unique_opaque<T>
#define CZU_DEFINE_OPAQUE_DELETER(T) namespace czu { void opaque_deleter_hook(T *it) { delete it; } }

#endif
Matteo Italia
fuente
1

Puede que no sea la mejor solución, pero a veces puede usar shared_ptr en su lugar. Si por supuesto es un poco exagerado, pero ... en cuanto a unique_ptr, tal vez esperaré 10 años más hasta que los creadores de estándares de C ++ decidan usar lambda como deletor.

Otro lado. Según su código, puede suceder que en la etapa de destrucción window_impl esté incompleto. Esto podría ser una razón de comportamiento indefinido. Vea esto: ¿Por qué, realmente, eliminar un tipo incompleto es un comportamiento indefinido?

Entonces, si es posible, definiría un objeto muy básico para todos sus objetos, con destructor virtual. Y ya casi eres bueno. Debe tener en cuenta que el sistema llamará al destructor virtual para su puntero, por lo que debe definirlo para cada antepasado. También debe definir la clase base en la sección de herencia como virtual (consulte esto para más detalles).

Stepan Dyatkovskiy
fuente