Czy std :: unique_ptr <T> jest wymagane, aby znać pełną definicję T?

248

Mam trochę kodu w nagłówku, który wygląda następująco:

#include <memory>

class Thing;

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

Jeśli dołączę ten nagłówek do cpp, który nie zawiera Thingdefinicji typu, wówczas nie kompiluje się w VS2010-SP1:

1> C: \ Program Files (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): błąd C2027: użycie nieokreślonego typu „Rzecz”

Zamień std::unique_ptrna std::shared_ptri kompiluje.

Zgaduję więc, że obecna std::unique_ptrimplementacja VS2010 wymaga pełnej definicji i jest całkowicie zależna od implementacji.

Albo to jest? Czy w jego standardowych wymaganiach jest coś, co uniemożliwia std::unique_ptrimplementacji działanie tylko z deklaracją forward? To dziwne, ponieważ powinno zawierać tylko wskaźnik Thing, prawda?

Klaim
źródło
20
Najlepszym wyjaśnieniem, kiedy potrzebujesz i nie potrzebujesz pełnego typu za pomocą inteligentnych wskaźników C ++ 0x, jest „Niepełne typyshared_ptrunique_ptr Howarda Hinnanta i / . Tabela na końcu powinna odpowiedzieć na twoje pytanie.
James McNellis
17
Dzięki za wskazówkę James. Zapomniałem, gdzie postawiłem ten stół! :-)
Howard Hinnant
5
@JamesMcNellis Link do strony Howarda Hinnanta nie działa. Oto wersja web.archive.org . W każdym razie odpowiedział na to idealnie poniżej z tą samą treścią :-)
Ela782
Inne dobre wyjaśnienie znajduje się w punkcie 22 „Effective modern C ++” Scotta Meyersa.
Fred Schoen,

Odpowiedzi:

328

Przyjęto stąd .

Większość szablonów w standardowej bibliotece C ++ wymaga, aby były tworzone z kompletnymi typami. Jednak shared_ptri unique_ptrto częściowe wyjątki. Niektóre, ale nie wszyscy ich członkowie, mogą być tworzone z niekompletnymi typami. Motywacją do tego jest wspieranie idiomów takich jak pimpl przy użyciu inteligentnych wskaźników i bez ryzyka nieokreślonego zachowania.

Nieokreślone zachowanie może wystąpić, gdy masz niekompletny typ i wywołujesz deletego:

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

Powyżej jest kodeks prawny. To się skompiluje. Twój kompilator może emitować ostrzeżenie o powyższym kodzie, jak wyżej. Kiedy to nastąpi, prawdopodobnie zdarzą się złe rzeczy. Jeśli masz szczęście, Twój program się zawiesi. Jednak bardziej prawdopodobne jest to, że twój program po cichu wyciek pamięci, ponieważ ~A()nie zostanie wywołany.

Użycie auto_ptr<A>w powyższym przykładzie nie pomaga. Nadal masz takie samo niezdefiniowane zachowanie, jakbyś używał surowego wskaźnika.

Niemniej jednak używanie niekompletnych klas w niektórych miejscach jest bardzo przydatne! To gdzie shared_ptri unique_ptrpomoc. Użycie jednego z tych inteligentnych wskaźników pozwoli ci uciec z niekompletnym typem, z wyjątkiem przypadków, gdy konieczne jest posiadanie pełnego typu. A co najważniejsze, gdy konieczne jest posiadanie pełnego typu, pojawia się błąd czasu kompilacji, jeśli spróbujesz użyć inteligentnego wskaźnika z niekompletnym typem w tym momencie.

Nigdy więcej nieokreślonego zachowania:

Jeśli Twój kod się kompiluje, to wszędzie tam, gdzie potrzebujesz, używałeś pełnego typu.

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

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

shared_ptri unique_ptrwymagają pełnego typu w różnych miejscach. Przyczyny są niejasne, związane z dynamicznym usuwaniem w porównaniu z usuwaniem statycznym. Dokładne powody nie są ważne. W rzeczywistości w większości kodów nie jest tak naprawdę ważne, aby wiedzieć dokładnie, gdzie wymagany jest pełny typ. Po prostu koduj, a jeśli się pomylisz, kompilator powie ci.

Jednak w przypadku jest to pomocne dla Ciebie, tutaj znajduje się tabela, która dokumentuje kilku członków shared_ptr, a unique_ptrw odniesieniu do wymogów kompletności. Jeśli element wymaga pełnego typu, wówczas wpis ma „C”, w przeciwnym razie wpis w tabeli jest wypełniony „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        |
+------------------------+---------------+---------------+

Wszelkie operacje wymagające konwersji wskaźnika wymagają kompletnych typów dla obu unique_ptri shared_ptr.

unique_ptr<A>{A*}Konstruktor może uciec z niekompletną Atylko wtedy, gdy kompilator nie jest wymagane, aby skonfigurować połączenie do ~unique_ptr<A>(). Na przykład, jeśli umieścisz unique_ptrstos na stosie, możesz uciec z niekompletnym A. Więcej szczegółów na ten temat można znaleźć w odpowiedzi BarryTheHatchet tutaj .

Howard Hinnant
źródło
3
Doskonała odpowiedź. Dałbym to +5, gdybym mógł. Jestem pewien, że powrócę do tego w moim następnym projekcie, w którym próbuję w pełni wykorzystać inteligentne wskaźniki.
Matthias
4
jeśli można wyjaśnić, co oznacza tabela, myślę, że pomoże to większej liczbie osób
Ghita
8
Jeszcze jedna uwaga: konstruktor klasy będzie odwoływał się do destruktorów swoich członków (w przypadku zgłoszenia wyjątku należy wywołać te destruktory). Więc chociaż destruktor Unique_ptr potrzebuje kompletnego typu, nie wystarczy mieć zdefiniowany przez użytkownika destruktor w klasie - potrzebuje również konstruktora.
Johannes Schaub - litb
7
@ Mehrdad: Ta decyzja została podjęta dla C ++ 98, co jest przed moim czasem. Uważam jednak, że decyzja ta wynikała z troski o możliwość implementacji i trudności w specyfikacji (tj. Dokładnie, które części kontenera wymagają lub nie wymagają pełnego typu). Nawet dzisiaj, z 15-letnim doświadczeniem od C ++ 98, nie byłoby trywialnym zadaniem zarówno rozluźnienie specyfikacji kontenera w tym obszarze, jak i upewnienie się, że nie zakazujesz ważnych technik implementacji lub optymalizacji. Myślę , że można to zrobić. Ja wiem, że byłoby dużo pracy. Mam świadomość, że jedna osoba podjęła próbę.
Howard Hinnant,
9
Ponieważ powyższe komentarze nie są oczywiste, dla każdego, kto ma ten problem, ponieważ definiują one unique_ptrjako zmienną składową klasy, po prostu jawnie zadeklaruj destruktor (i konstruktor) w deklaracji klasy (w pliku nagłówkowym) i przejdź do ich zdefiniowania w pliku źródłowym (i umieść nagłówek z pełną deklaracją klasy wskazanej w pliku źródłowym), aby uniemożliwić kompilatorowi automatyczne wstawianie konstruktora lub destruktora w pliku nagłówkowym (co powoduje błąd). stackoverflow.com/a/13414884/368896 również pomaga mi o tym przypomnieć.
Dan Nissenbaum
42

Kompilator potrzebuje definicji Thing, aby wygenerować domyślny destruktor dla MyClass. Jeśli jawnie zadeklarujesz destruktor i przeniesiesz jego (pustą) implementację do pliku CPP, kod powinien się skompilować.

Igor Nazarenko
źródło
5
Myślę, że jest to idealna okazja do użycia domyślnej funkcji. MyClass::~MyClass() = default;w pliku implementacyjnym wydaje się, że mniej prawdopodobne jest, że zostanie przypadkowo usunięty później przez kogoś, kto podejrzewa, że ​​ciało dystrybutora zostało usunięte, a nie celowo pozostawione puste.
Dennis Zickefoose
@Dennis Zickefoose: Niestety OP używa VC ++, a VC ++ nie obsługuje jeszcze członków klasy defaulted i deleted.
ildjarn
6
+1 za przeniesienie drzwi do pliku .cpp. Wygląda na to, że MyClass::~MyClass() = defaultnie przenosi go do pliku implementacji w Clang. (jeszcze?)
Eonil,
Musisz także przenieść implementację konstruktora do pliku CPP, przynajmniej na VS 2017. Zobacz na przykład tę odpowiedź: stackoverflow.com/a/27624369/5124002
jciloa
15

To nie zależy od implementacji. Powodem tego jest to, że shared_ptrokreśla właściwy destruktor, który ma zostać wywołany w czasie wykonywania - nie jest to część podpisu typu. Jednak unique_ptrdestruktor jest częścią tego typu i musi być znany w czasie kompilacji.

Szczeniak
źródło
8

Wygląda na to, że obecne odpowiedzi nie dokładnie wyjaśniają, dlaczego domyślny konstruktor (lub destruktor) jest problemem, ale puste deklarowane w cpp nie są.

Oto co się dzieje:

Jeśli klasa zewnętrzna (tj. MyClass) nie ma konstruktora lub destruktora, kompilator generuje te domyślne. Problem polega na tym, że kompilator zasadniczo wstawia domyślny pusty konstruktor / destruktor do pliku .hpp. Oznacza to, że kod domyślnego contructor / destructor jest kompilowany wraz z plikiem binarnym pliku wykonywalnego hosta, a nie wraz z plikami binarnymi biblioteki. Jednak te definicje nie mogą tak naprawdę konstruować klas częściowych. Kiedy więc linker wchodzi do pliku binarnego biblioteki i próbuje uzyskać konstruktor / destruktor, nie może go znaleźć i pojawia się błąd. Jeśli kod konstruktora / destruktora znajdował się w twoim pliku .cpp, to plik binarny biblioteki ma ten dostępny do łączenia.

Nie ma to nic wspólnego z używaniem unikalnych_ptrów lub współużytkowanych_ptrów, a inne odpowiedzi wydają się być mylącym błędem w starym VC ++ dla implementacji unikatowych_ptr (VC ++ 2015 działa dobrze na moim komputerze).

Morał tej historii jest taki, że twój nagłówek musi pozostać wolny od definicji konstruktora / destruktora. Może zawierać tylko ich deklarację. Na przykład ~MyClass()=default;w hpp nie będzie działać. Jeśli zezwolisz kompilatorowi na wstawienie domyślnego konstruktora lub destruktora, pojawi się błąd linkera.

Jeszcze jedna uwaga: jeśli nadal pojawia się ten błąd, nawet jeśli w pliku cpp znajduje się konstruktor i destruktor, najprawdopodobniej przyczyną jest niewłaściwa kompilacja biblioteki. Na przykład, pewnego razu po prostu zmieniłem typ projektu z Konsoli na Bibliotekę w VC ++ i dostałem ten błąd, ponieważ VC ++ nie dodał symbolu preprocesora _LIB, co spowodowało wyświetlenie tego samego komunikatu o błędzie.

Shital Shah
źródło
Dziękuję Ci! To było bardzo zwięzłe wyjaśnienie niesamowicie niejasnego dziwactwa C ++. Zaoszczędził mi wielu kłopotów.
JPNotADragon
5

Dla kompletności:

Nagłówek: Ah

class B; // forward declaration

class A
{
    std::unique_ptr<B> ptr_;  // ok!  
public:
    A();
    ~A();
    // ...
};

Źródło A.cpp:

class B {  ...  }; // class definition

A::A() { ... }
A::~A() { ... }

Definicja klasy B musi być postrzegana przez konstruktor, destruktor i wszystko, co może domyślnie usunąć B. (Chociaż konstruktor nie pojawia się na powyższej liście, nawet w VS2017 nawet konstruktor potrzebuje definicji B. A to ma sens, biorąc pod uwagę że w przypadku wyjątku w konstruktorze unikalna_ptr jest ponownie niszczona).

Joachim
źródło
1

Pełna definicja Rzeczy jest wymagana w momencie tworzenia szablonu. To jest dokładnie powód, dla którego kompiluje się idiom pimpl.

Gdyby to nie było możliwe, ludzie nie będą zadawać pytania, takie jak ten .

BЈовић
źródło
-2

Prostą odpowiedzią jest użycie zamiast tego shared_ptr.

deltanina
źródło
-7

Jak dla mnie,

QList<QSharedPointer<ControllerBase>> controllers;

Wystarczy dołączyć nagłówek ...

#include <QSharedPointer>
Sanbrother
źródło
Odpowiedź nie jest związana i nie dotyczy pytania.
Mikus