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

201

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 erhalte jedoch einen Kompilierungsfehler 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ß, std::unique_ptrsollte mit einem unvollständigen Typ verwendet werden können. Ist das ein Fehler in libc ++ oder mache ich hier etwas falsch?


quelle
Referenzlink für Vollständigkeitsanforderungen: stackoverflow.com/a/6089065/576911
Howard Hinnant
1
Ein Pickel wird seitdem oft konstruiert und nicht modifiziert. Ich benutze normalerweise ein std :: shared_ptr <const window_impl>
mfnx
Verwandte Themen: Ich würde sehr gerne wissen, warum dies in MSVC funktioniert und wie verhindert werden kann, dass es funktioniert (damit ich die Zusammenstellungen meiner GCC-Kollegen nicht beschädige).
Len

Antworten:

257

Hier sind einige Beispiele für std::unique_ptrunvollständige Typen. Das Problem liegt in der Zerstörung.

Wenn Sie pimpl mit verwenden unique_ptr, 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 sonst der Compiler einen Standard generiert und dafür eine vollständige Deklaration benötigt foo::impl.

Wenn Sie Vorlagenkonstruktoren haben, sind Sie fertig, auch wenn Sie das Element nicht erstellen impl_:

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 unique_ptrfunktioniert die Verwendung auch nicht:

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

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

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;
Alexandre C.
quelle
3
Ich finde, Ihre erste Lösung (Hinzufügen des foo- Destruktors) ermöglicht das Kompilieren der Klassendeklaration selbst, aber das Deklarieren eines Objekts dieses Typs an einer beliebigen Stelle führt zum ursprünglichen Fehler ("ungültige Anwendung von 'sizeof' ...").
Jeff Trull
37
ausgezeichnete Antwort, nur um zu beachten; Wir können immer noch den Standardkonstruktor / Destruktor verwenden, indem wir zB foo::~foo() = default;in die src-Datei setzen
Assem
2
Eine Möglichkeit, mit Vorlagenkonstruktoren zu leben, besteht darin, den Konstruktor im Klassenkörper zu deklarieren, aber nicht zu definieren, ihn irgendwo zu definieren, wo die vollständige impl-Definition angezeigt wird, und alle erforderlichen Instanziierungen dort explizit zu instanziieren.
Enobayram
2
Können Sie erklären, wie dies in einigen Fällen funktionieren würde und in anderen nicht? Ich habe das Pimpl-Idiom mit einem unique_ptr und einer Klasse ohne Destruktor verwendet, und in einem anderen Projekt kann mein Code nicht mit dem erwähnten Fehler OP kompiliert werden.
Neugierig
1
Wenn der Standardwert für unique_ptr in der Header-Datei der Klasse im C ++ 11-Stil auf {nullptr} gesetzt ist, ist aus dem oben genannten Grund auch eine vollständige Deklaration erforderlich.
Feirainy
53

Wie Alexandre C. erwähnte, besteht das Problem darin window, dass der Destruktor implizit an Stellen definiert wird, an denen der Typ window_implnoch unvollständig ist. Zusätzlich zu seinen Lösungen besteht eine weitere Problemumgehung darin, einen Deleter-Funktor im Header 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;
}

Beachten Sie, dass Sie eine benutzerdefinierte Deleter Funktion schließt die Verwendung von mit std::make_unique(von C ++ 14), wie bereits diskutiert hier .

Fernando Costa Bertoldi
quelle
6
Dies ist für mich die richtige Lösung. Es ist nicht nur die Verwendung des pimpl-Idioms, sondern ein allgemeines Problem bei der Verwendung von std :: unique_ptr mit unvollständigen Klassen. Der von std :: unique_ptr <X> verwendete Standardlöscher versucht, "X löschen" auszuführen. Dies ist nicht möglich, wenn X eine Vorwärtsdeklaration ist. Durch Angabe einer Löschfunktion können Sie diese Funktion in eine Quelldatei einfügen, in der die Klasse X vollständig definiert ist. Andere Quelldateien können dann std :: unique_ptr <X, DeleterFunc> verwenden, obwohl X nur eine Vorwärtsdeklaration ist, solange sie mit der Quelldatei verknüpft sind, die DeleterFunc enthält.
Sheltond
1
Dies ist eine gute Problemumgehung, wenn Sie eine Inline-Funktionsdefinition benötigen, die eine Instanz Ihres Typs "Foo" erstellt (z. B. eine statische Methode "getInstance", die auf Konstruktor und Destruktor verweist), und diese nicht in eine Implementierungsdatei verschieben möchten wie @ adspx5 vorschlägt.
GameSalutes
20

Verwenden Sie einen benutzerdefinierten Löscher

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

Wie bereits in einer anderen Antwort darauf hingewiesen, ein Weg , dies zu vermeiden , sich zu bewegen , alle Operationen , die erfordern unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)und unique_ptr::reset()in die Quelldatei , in der die Pimpl Helfer Klasse tatsächlich definiert.

Dies ist jedoch ziemlich unpraktisch und widerspricht bis zu einem gewissen Grad dem eigentlichen Punkt des Pickel-Idoims. Eine viel sauberere Lösung, die alles vermeidet, einen benutzerdefinierten Löscher zu verwenden und seine Definition nur in die Quelldatei zu verschieben, in der sich die Pickel-Hilfsklasse befindet. 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 staticMitglied von fooin 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;
};
Walter
quelle
15

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

Stellen Sie sicher, dass Sie in Ihrem .h für Klassenfenster nur eine Funktionsdeklaration haben. Alle Funktionskörper für Fenster müssen sich in der CPP-Datei befinden. Und auch für window_impl ...

Übrigens müssen Sie Ihrer .h-Datei explizit eine Destruktordeklaration für die Windows-Klasse hinzufügen.

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

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

Muss nur eine Erklärung sein:

  class window {
    virtual ~window();
  }
adspx5
quelle
Dies war auch meine Lösung. Viel prägnanter. Lassen Sie einfach Ihren Konstruktor / Destruktor im Header deklarieren und in der CPP-Datei definieren.
Kris Morness
2

Um die Antworten des anderen über den benutzerdefinierten Löscher zu ergänzen, habe ich in unserer internen "Dienstprogrammbibliothek" einen Hilfskopf hinzugefügt, um dieses allgemeine Muster zu implementieren ( std::unique_ptrvon einem unvollständigen Typ, der nur einigen TU bekannt ist, um z. B. lange Kompilierungszeiten zu vermeiden oder bereitzustellen nur ein undurchsichtiger Griff für Kunden).

Es bietet das allgemeine Gerüst für dieses Muster: eine benutzerdefinierte Löschklasse, die eine extern definierte Löschfunktion aufruft, einen Typalias für a unique_ptrmit dieser Löschklasse und ein Makro zum Deklarieren der Löschfunktion in einer TU, die eine vollständige Definition der enthält Art. Ich denke, dass dies einen allgemeinen Nutzen hat, also hier ist es:

#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
quelle
1

Möglicherweise nicht die beste Lösung, aber manchmal können Sie stattdessen shared_ptr verwenden. Wenn es natürlich ein bisschen übertrieben ist, aber ... was unique_ptr betrifft, werde ich vielleicht noch 10 Jahre warten, bis sich C ++ - Standardhersteller dafür entscheiden, Lambda als Deleter zu verwenden.

Andere Seite. Gemäß Ihrem 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 dies: Warum ist das Löschen eines unvollständigen Typs wirklich ein undefiniertes Verhalten?

Wenn möglich, würde ich für alle Ihre Objekte mit einem virtuellen Destruktor ein sehr einfaches Objekt definieren. Und du bist fast gut. Sie sollten nur bedenken, dass das System den virtuellen Destruktor für Ihren Zeiger aufruft, also sollten Sie ihn für jeden Vorfahren definieren. Sie sollten auch Basisklasse in Vererbungsabschnitt als virtuelle (siehe definieren diese für weitere Details).

Stepan Dyatkovskiy
quelle