web-dev-qa-db-de.com

std :: unique_ptr mit einem unvollständigen Typ wird nicht kompiliert

Ich benutze das Pimpl-Idiom mit std::unique_ptr:

class window {
  window(const rectangle& rect);

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

Ich bekomme jedoch einen Kompilierfehler bezüglich der Verwendung eines unvollständigen Typs in Zeile 304 in <memory>

Ungültige Anwendung von 'sizeof' auf einen unvollständigen Typ 'uixx::window::window_impl'

Soweit ich weiß, sollte std::unique_ptr mit einem unvollständigen Typ verwendet werden können. Ist das ein Fehler in libc ++ oder mache ich hier etwas falsch?

153
user1203803

Hier sind einige Beispiele für std::unique_ptr mit unvollständigen Typen. Das Problem liegt in der Zerstörung.

Wenn Sie Pimpl mit unique_ptr verwenden, müssen Sie einen Destruktor deklarieren:

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

da der Compiler sonst einen Standardwert generiert und dafür eine vollständige Deklaration von foo::impl benötigt.

Wenn Sie über Vorlagenkonstruktoren verfügen, sind Sie vermasselt, auch wenn Sie das Member impl_ nicht erstellen:

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

Im Namespace-Bereich funktioniert unique_ptr ebenfalls nicht:

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

da der Compiler hier wissen muss, wie man dieses statische Duration-Objekt zerstört. Eine Problemumgehung ist:

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;
196
Alexandre C.

Wie Alexandre C. erwähnt, liegt das Problem darin, dass der Destruktor von window implizit an Stellen definiert wird, an denen der Typ von window_impl noch unvollständig ist. Neben seinen Lösungen besteht eine weitere Problemumgehung darin, einen Deleter-Functor in der Kopfzeile zu deklarieren:

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

einen benutzerdefinierten Deleter verwenden

Das Problem ist, dass unique_ptr<T> den Destruktor T::~T() in seinem eigenen Destruktor, seinem Verschiebungszuweisungsoperator und der unique_ptr::reset()-Memberfunktion (nur) aufrufen muss. Diese müssen jedoch (implizit oder explizit) in mehreren PIMPL-Situationen aufgerufen werden (bereits im Destruktor der äußeren Klasse und im Zuweisungsoperator für Umzüge).

Wie bereits in einer anderen Antwort dargelegt, besteht eine Möglichkeit, dies zu vermeiden, darin, alle -Operationen, für die unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&) und unique_ptr::reset() erforderlich sind, in die Quelldatei zu verschieben, in der die Pimpl-Helferklasse tatsächlich definiert ist.

Dies ist jedoch ziemlich unpraktisch und trotzt bis zu einem gewissen Grad dem Punkt des Pimpl-Idoims. Eine viel sauberere Lösung, die alles vermeidet, ist ein custom deleter zu verwenden und seine Definition nur in die Quelldatei zu verschieben, in der die Pickel-Helper-Klasse lebt. Hier ist ein einfaches Beispiel:

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

Anstelle einer separaten Deleter-Klasse können Sie auch eine freie Funktion oder ein static-Member von foo in Verbindung mit einem Lambda verwenden:

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

Wahrscheinlich haben Sie einige Funktionskörper in der .h-Datei in der Klasse, die einen unvollständigen Typ verwenden.

Stellen Sie sicher, dass Sie in Ihrem .h für Klassenfenster nur Funktionsdeklarationen haben. Alle Funktionskörper für Fenster müssen in einer .cpp-Datei sein. Und auch für window_impl ...

Übrigens müssen Sie die Destruktor-Deklaration für die Windows-Klasse in Ihrer .h-Datei explizit hinzufügen.

Sie können jedoch KEINEN leeren Körper in Ihre Header-Datei einfügen:

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

Muss nur eine Erklärung sein:

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

Vielleicht nicht die beste Lösung, aber manchmal können Sie stattdessen shared_ptr verwenden. Wenn es natürlich ein Overkill ist, aber ... wie für unique_ptr, werde ich vielleicht 10 Jahre bis zum C++ - Standard warten wird sich entscheiden, Lambda als Deleter zu verwenden.

Eine andere Seite ... Durch Ihren Code kann es vorkommen, dass window_impl in der Zerstörungsphase unvollständig ist. Dies könnte ein Grund für undefiniertes Verhalten sein. __ Siehe dazu: Warum ist das Löschen eines unvollständigen Typs wirklich undefiniertes Verhalten?

Wenn es möglich ist, würde ich für alle Ihre Objekte ein sehr einfaches Objekt definieren, mit einem virtuellen Destruktor. Und du bist fast gut. Sie sollten nur bedenken, dass das System einen virtuellen Destruktor für Ihren Zeiger aufruft. Sie sollten es also für jeden Vorfahren definieren. Sie sollten die Basisklasse im Vererbungsabschnitt auch als virtuell definieren (siehe this für Details).

0

Um den Antworten des anderen über den benutzerdefinierten Deleter hinzuzufügen, habe ich in unserer internen "Dienstprogrammbibliothek" einen Helper-Header hinzugefügt, um dieses allgemeine Muster (std::unique_ptr eines unvollständigen Typs) zu implementieren, der nur einigen TU bekannt ist, um zB lange zu vermeiden Kompilierungszeiten oder nur ein undurchsichtiges Handle für Kunden).

Es stellt das übliche Gerüst für dieses Muster bereit: eine benutzerdefinierte Deleter-Klasse, die eine extern definierte Deleter-Funktion aufruft, einen Typalias für einen unique_ptr mit dieser Deleter-Klasse und ein Makro zum Deklarieren der Deleter-Funktion in einer TU mit eine vollständige Definition des Typs. Ich denke, dass dies einen allgemeinen Nutzen hat, also hier:

#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
0
Matteo Italia