std :: unique_ptr z niekompletnym typem nie zostanie skompilowany


202

Używam idiomu pimpl z std::unique_ptr:

class window {
  window(const rectangle& rect);

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

Jednak pojawia się błąd kompilacji dotyczący użycia niekompletnego typu w wierszu 304 w <memory>:

Nieprawidłowe zastosowanie „ sizeof” do niekompletnego typu uixx::window::window_impl

O ile mi wiadomo, std::unique_ptrpowinno być możliwe użycie niekompletnego typu. Czy to błąd w libc ++, czy robię tu coś złego?


Link referencyjny do wymagań kompletności: stackoverflow.com/a/6089065/576911
Howard Hinnant

1
Od tego czasu pimpl jest często konstruowany i nie modyfikowany. Zwykle używam std :: shared_ptr <const window_impl>
mfnx

Powiązane: Bardzo chciałbym wiedzieć, dlaczego to działa w MSVC i jak temu zapobiec (aby nie łamać kompilacji moich kolegów z GCC).
Len

Odpowiedzi:


258

Oto kilka przykładów std::unique_ptrniepełnych typów. Problem polega na zniszczeniu.

Jeśli używasz pimpl z unique_ptr, musisz zadeklarować destruktor:

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

ponieważ w przeciwnym razie kompilator generuje domyślny i potrzebuje do tego pełnej deklaracji foo::impl.

Jeśli masz konstruktory szablonów, to wkręcasz się, nawet jeśli nie konstruujesz impl_elementu:

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

W zakresie przestrzeni nazw użycie unique_ptrnie będzie działać:

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

ponieważ kompilator musi tutaj wiedzieć, jak zniszczyć ten statyczny obiekt o czasie trwania. Obejście to:

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

3
Uważam, że twoje pierwsze rozwiązanie (dodanie foo destructor) pozwala na kompilację samej deklaracji klasy, ale zadeklarowanie obiektu tego typu w dowolnym miejscu powoduje pierwotny błąd („nieprawidłowe zastosowanie 'sizeof' ...”).
Jeff Trull

38
doskonała odpowiedź, aby zauważyć; nadal możemy użyć domyślnego konstruktora / destruktora, umieszczając np. foo::~foo() = default;w pliku src
asem

2
Jednym ze sposobów na życie z konstruktorami szablonów byłoby zadeklarowanie, ale nie zdefiniowanie konstruktora w ciele klasy, zdefiniowanie go gdzieś, gdzie widoczna jest pełna definicja impl i wyraźne utworzenie tam wszystkich niezbędnych instancji.
enobayram

2
Czy możesz wyjaśnić, jak to by działało w niektórych przypadkach, a nie w innych? Użyłem idiomu pimpl z unikalną_ptr i klasą bez destruktora, aw innym projekcie mój kod nie kompiluje się z podanym błędem OP.
Curious

1
Wygląda na to, że jeśli domyślna wartość parametru Unique_ptr jest ustawiona na {nullptr} w pliku nagłówkowym klasy w stylu c ++ 11, z powyższego powodu potrzebna jest również pełna deklaracja.
feirainy

53

Jak wspomniał Alexandre C. , problem sprowadza się do windowdomyślnego zdefiniowania destruktora w miejscach, w których typ window_implwciąż jest niekompletny. Oprócz jego rozwiązań, innym obejściem, którego użyłem, jest zadeklarowanie funktora Deletera w nagłówku:

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

Zauważ, że użycie niestandardowej funkcji Deletera wyklucza użycie std::make_unique(dostępnego w C ++ 14), jak już tutaj omówiono .


6
Według mnie to prawidłowe rozwiązanie. Nie jest unikalny w użyciu pimpl-idiom, to ogólny problem z użyciem std :: unique_ptr z niekompletnymi klasami. Domyślny usuwacz używany przez std :: unique_ptr <X> próbuje wykonać „usuń X”, czego nie może zrobić, jeśli X jest deklaracją przesyłania dalej. Określając funkcję usuwania, można umieścić tę funkcję w pliku źródłowym, w którym klasa X jest całkowicie zdefiniowana. Inne pliki źródłowe mogą następnie używać std :: unique_ptr <X, DeleterFunc>, nawet jeśli X jest tylko deklaracją do przodu, o ile są one połączone z plikiem źródłowym zawierającym DeleterFunc.
sheltond

1
Jest to dobre obejście, gdy trzeba mieć wbudowaną definicję funkcji tworzącą instancję typu „Foo” (na przykład statyczną metodę „getInstance”, która odwołuje się do konstruktora i destruktora), a nie chce się ich przenosić do pliku implementacji jak sugeruje @ adspx5.
GameSalutes,

20

użyj niestandardowego narzędzia do usuwania

Problem polega na tym, że unique_ptr<T>musi on wywoływać destruktor T::~T()we własnym destruktorze, jego operatorze przypisania ruchu i unique_ptr::reset()funkcji elementu (tylko). Jednak muszą być wywoływane (niejawnie lub jawnie) w kilku sytuacjach PIMPL (już w zewnętrznym instrumencie niszczącym klasy i operatorze przypisania ruchu).

Jak już wskazano w innej odpowiedzi, jeden sposób, aby uniknąć sytuacji jest przeniesienie wszystkich operacji, które wymagają unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)i unique_ptr::reset()do pliku źródłowego, gdzie klasa pimpl pomocnik jest właściwie zdefiniowane.

Jest to jednak dość niewygodne i do pewnego stopnia przeczy samemu sensowi idiomu pimpl. Znacznie bardziej przejrzyste rozwiązanie, które pozwala uniknąć korzystania z niestandardowego narzędzia do usuwania i przenosi jego definicję do pliku źródłowego, w którym mieszka klasa pomocnika pryszcza . Oto prosty przykład:

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

Zamiast oddzielnej klasy deletera możesz także użyć funkcji swobodnej lub staticelementu foow połączeniu z lambda:

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

15

Prawdopodobnie masz jakieś ciała funkcji w pliku .h w klasie, która używa niekompletnego typu.

Upewnij się, że w oknie .h dla okna klasy masz tylko deklarację funkcji. Wszystkie ciała funkcji dla okna muszą znajdować się w pliku .cpp. A także dla window_impl ...

Btw, musisz jawnie dodać deklarację destruktora dla klasy Windows w pliku .h.

Ale NIE MOŻESZ umieścić pustego ciała dtor w pliku nagłówka:

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

To musi być tylko deklaracja:

  class window {
    virtual ~window();
  }

To też było moje rozwiązanie. O wiele bardziej zwięzłe. Po prostu zadeklaruj swojego konstruktora / destruktora w nagłówku i zdefiniuj w pliku cpp.
Kris Morness,

2

Aby dodać do odpowiedzi drugiego użytkownika na temat niestandardowego usuwacza, w naszej wewnętrznej „bibliotece narzędzi” dodałem nagłówek pomocnika w celu wdrożenia tego wspólnego wzorca ( std::unique_ptrniepełnego typu, znanego tylko niektórym jednostkom językowym w celu np. Uniknięcia długich czasów kompilacji lub zapewnienia tylko nieprzezroczysty uchwyt dla klientów).

Zapewnia wspólne rusztowanie dla tego wzorca: niestandardową klasę usuwania, która wywołuje zewnętrznie zdefiniowaną funkcję usuwania, alias typu dla unique_ptrtej klasy usuwania oraz makro do deklarowania funkcji usuwania w JT, która ma pełną definicję rodzaj. Myślę, że ma to pewną ogólną przydatność, więc oto:

#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

1

Może nie być najlepszym rozwiązaniem, ale czasami możesz zamiast tego użyć shared_ptr . Jeśli oczywiście jest to trochę przesada, ale ... jeśli chodzi o Unique_ptr, być może poczekam jeszcze 10 lat, aż twórcy standardu C ++ zdecydują się użyć lambdy jako deletera.

Inna strona. Według twojego kodu może się zdarzyć, że na etapie zniszczenia window_impl będzie niekompletny. Może to być przyczyną nieokreślonego zachowania. Zobacz: Dlaczego tak naprawdę usunięcie niekompletnego typu jest niezdefiniowanym zachowaniem?

Więc jeśli to możliwe, zdefiniowałbym bardzo podstawowy obiekt dla wszystkich twoich obiektów za pomocą wirtualnego destruktora. I jesteś prawie dobry. Należy pamiętać, że system wywoła wirtualny destruktor dla twojego wskaźnika, więc powinieneś zdefiniować go dla każdego przodka. Powinieneś również zdefiniować klasę podstawową w sekcji dziedziczenia jako wirtualną (zobacz to, aby uzyskać szczegółowe informacje).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.