Std :: unique_ptr <T> è necessario per conoscere la definizione completa di T?


248

Ho un po 'di codice in un'intestazione che assomiglia a questo:

#include <memory>

class Thing;

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

Se includo questa intestazione in un cpp che non include la Thingdefinizione del tipo, questo non viene compilato in VS2010-SP1:

1> C: \ Programmi (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): errore C2027: utilizzo di tipo indefinito 'Thing'

Sostituisci std::unique_ptrcon std::shared_ptre compila.

Quindi, immagino che sia l'attuale std::unique_ptrimplementazione del VS2010 che richiede la definizione completa ed è totalmente dipendente dall'implementazione.

O è? C'è qualcosa nei suoi requisiti standard che rende impossibile che std::unique_ptrl'implementazione funzioni solo con una dichiarazione anticipata? Sembra strano come dovrebbe solo tenere un puntatore a Thing, non dovrebbe?


20
La migliore spiegazione di quando si fa e non è necessario un tipo completo con i puntatori intelligenti C ++ 0x è "Tipi incompleti e shared_ptr/ unique_ptr" di Howard Hinnant La tabella alla fine dovrebbe rispondere alla tua domanda.
James McNellis,

17
Grazie per l'indicatore James. Avevo dimenticato dove avevo messo quel tavolo! :-)
Howard Hinnant


5
@JamesMcNellis Il link al sito Web di Howard Hinnant è inattivo. Ecco la versione web.archive.org di esso. In ogni caso, ha risposto perfettamente sotto con lo stesso contenuto :-)
Ela782

Un'altra buona spiegazione è data nell'articolo 22 dell'Efficace C ++ moderno di Scott Meyers.
Fred Schoen,

Risposte:


328

Adottato da qui .

La maggior parte dei modelli nella libreria standard C ++ richiede che vengano istanziati con tipi completi. Tuttavia shared_ptre unique_ptrsono eccezioni parziali . Alcuni, ma non tutti i loro membri, possono essere istanziati con tipi incompleti. La motivazione di ciò è supportare idiomi come il pimpl usando i puntatori intelligenti e senza rischiare comportamenti indefiniti.

Un comportamento indefinito può verificarsi quando si dispone di un tipo incompleto e lo si chiama delete:

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

Quanto sopra è un codice legale. Si compilerà. Il compilatore può o meno emettere un avviso per il codice sopra come quello sopra. Quando viene eseguito, probabilmente accadranno cose brutte. Se sei molto fortunato, il tuo programma andrà in crash. Tuttavia, un risultato più probabile è che il tuo programma perderà silenziosamente memoria come ~A()non verrà chiamato.

L'uso auto_ptr<A>nell'esempio sopra non aiuta. Ottieni ancora lo stesso comportamento indefinito come se avessi usato un puntatore non elaborato.

Tuttavia, l'uso di lezioni incomplete in determinati luoghi è molto utile! Questo è dove shared_ptre unique_ptraiuto. L'uso di uno di questi suggerimenti intelligenti ti permetterà di cavartela con un tipo incompleto, tranne dove è necessario avere un tipo completo. E, soprattutto, quando è necessario disporre di un tipo completo, si ottiene un errore di compilazione se si tenta di utilizzare il puntatore intelligente con un tipo incompleto in quel punto.

Niente più comportamenti indefiniti:

Se il tuo codice viene compilato, hai utilizzato un tipo completo ovunque sia necessario.

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

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

shared_ptre unique_ptrrichiede un tipo completo in luoghi diversi. Le ragioni sono oscure, hanno a che fare con un deleter dinamico rispetto a un deleter statico. Le ragioni precise non sono importanti. In effetti, nella maggior parte dei codici non è molto importante sapere esattamente dove è richiesto un tipo completo. Basta codificare e, se sbagli, il compilatore te lo dirà.

Tuttavia, nel caso in cui ti sia utile, ecco una tabella che documenta diversi membri shared_ptre unique_ptrrispetto ai requisiti di completezza. Se il membro richiede un tipo completo, la voce ha una "C", altrimenti la voce della tabella viene riempita con "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        |
+------------------------+---------------+---------------+

Qualsiasi operazione che richiede conversioni di puntatori richiede tipi completi per entrambi unique_ptre shared_ptr.

Il unique_ptr<A>{A*}costruttore può cavarsela con un incompleto Asolo se al compilatore non è richiesto di impostare una chiamata ~unique_ptr<A>(). Ad esempio se metti l' unique_ptrheap, puoi farcela con un incompleto A. Maggiori dettagli su questo punto sono disponibili nella risposta di BarryTheHatchet qui .


3
Risposta eccellente. Lo farei +5 se potessi. Sono sicuro che mi riferirò a questo nel mio prossimo progetto, in cui sto tentando di sfruttare appieno i suggerimenti intelligenti.
Matthias,

4
se uno può spiegare cosa significa la tabella, suppongo che aiuterà più persone
Ghita,

8
Un'altra nota: un costruttore di classi farà riferimento ai distruttori dei suoi membri (nel caso in cui venga generata un'eccezione, è necessario chiamare quei distruttori). Quindi, sebbene il distruttore di unique_ptr abbia bisogno di un tipo completo, non è sufficiente avere un distruttore definito dall'utente in una classe, ma ha anche bisogno di un costruttore.
Johannes Schaub - litb

7
@Mehrdad: questa decisione è stata presa per C ++ 98, che è prima del mio tempo. Tuttavia, credo che la decisione sia venuta da una preoccupazione per l'implementazione e la difficoltà di specifica (cioè esattamente quali parti di un contenitore fanno o non richiedono un tipo completo). Ancora oggi, con 15 anni di esperienza dal C ++ 98, sarebbe un compito non banale sia rilassare le specifiche del contenitore in quest'area, sia assicurarsi che non vietiate importanti tecniche di implementazione o ottimizzazioni. Io penso che potrebbe essere fatto. Io so che sarebbe stato un sacco di lavoro. Sono a conoscenza di una persona che sta tentando.
Howard Hinnant,

9
Perché non è evidente dalle osservazioni di cui sopra, per chiunque abbia questo problema perché definiscono una unique_ptrcome variabile membro di una classe, solo in modo esplicito dichiarare un distruttore (e costruttore) nella dichiarazione della classe (nel file di intestazione) e procedere a definire le loro nel file di origine (e inserisci l'intestazione con la dichiarazione completa della classe puntata nel file di origine) per impedire al compilatore di incorporare automaticamente il costruttore o il distruttore nel file di intestazione (che attiva l'errore). stackoverflow.com/a/13414884/368896 mi aiuta anche a ricordarmelo.
Dan Nissenbaum,

42

Il compilatore necessita della definizione di Thing per generare il distruttore predefinito per MyClass. Se dichiari esplicitamente il distruttore e ne sposti l'implementazione (vuota) nel file CPP, il codice dovrebbe essere compilato.


5
Penso che questa sia l'occasione perfetta per usare una funzione predefinita. MyClass::~MyClass() = default;nel file di implementazione sembra meno probabile che venga rimosso inavvertitamente più avanti lungo la strada da qualcuno che presume che il corpo del distruttore sia stato cancellato piuttosto che lasciato deliberatamente in bianco.
Dennis Zickefoose,

@Dennis Zickefoose: Sfortunatamente l'OP utilizza VC ++ e VC ++ non supporta ancora i membri delle classi defaulted ed delete.
ildjarn,

6
+1 per come spostare la porta nel file .cpp. Inoltre sembra MyClass::~MyClass() = defaultnon spostarlo nel file di implementazione su Clang. (ancora?)
Eonil,

È inoltre necessario spostare l'attuazione del costruttore per il file CPP, almeno su VS 2017. Si veda ad esempio questa risposta: stackoverflow.com/a/27624369/5124002
jciloa

15

Questo non dipende dall'implementazione. Il motivo per cui funziona è perché shared_ptrdetermina il distruttore corretto da chiamare in fase di esecuzione - non fa parte della firma del tipo. Tuttavia, unique_ptril distruttore fa parte del suo tipo e deve essere noto al momento della compilazione.


8

Sembra che le risposte attuali non stiano esattamente chiarendo perché il costruttore predefinito (o il distruttore) sia un problema, ma quelle vuote dichiarate in cpp non lo sono.

Ecco cosa sta succedendo:

Se la classe esterna (cioè MyClass) non ha costruttore o distruttore, il compilatore genera quelli predefiniti. Il problema è che il compilatore inserisce essenzialmente il costruttore / distruttore vuoto predefinito nel file .hpp. Ciò significa che il codice per il contructor / destructor predefinito viene compilato insieme al file binario dell'eseguibile host, non insieme ai file binari della libreria. Tuttavia, queste definizioni non possono realmente costruire le classi parziali. Quindi, quando il linker va nel binario della tua libreria e cerca di ottenere il costruttore / distruttore, non ne trova nessuno e ottieni un errore. Se il codice del costruttore / distruttore era nel tuo .cpp, il tuo binario della libreria ha quello disponibile per il collegamento.

Questo non ha nulla a che fare con l'utilizzo di unique_ptr o shared_ptr e altre risposte sembrano essere possibili confondendo bug nel vecchio VC ++ per l'implementazione di unique_ptr (VC ++ 2015 funziona bene sulla mia macchina).

Così morale della storia è che la tua intestazione deve rimanere libera da qualsiasi definizione di costruttore / distruttore. Può contenere solo la loro dichiarazione. Ad esempio, ~MyClass()=default;in hpp non funzionerà. Se si consente al compilatore di inserire un costruttore o distruttore predefinito, verrà visualizzato un errore del linker.

Un'altra nota a margine: se stai ancora riscontrando questo errore anche dopo aver costruito il costruttore e il distruttore nel file cpp, molto probabilmente il motivo è che la tua libreria non viene compilata correttamente. Ad esempio, una volta ho semplicemente cambiato il tipo di progetto dalla console alla libreria in VC ++ e ho riscontrato questo errore perché VC ++ non ha aggiunto il simbolo del preprocessore _LIB e ha prodotto esattamente lo stesso messaggio di errore.


Grazie! Quella era una spiegazione molto succinta di una stranezza incredibilmente oscura in C ++. Mi ha salvato un sacco di problemi.
JPNotADragon

5

Solo per completezza:

Intestazione: Ah

class B; // forward declaration

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

Fonte A.cpp:

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

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

La definizione di classe B deve essere vista da costruttore, distruttore e tutto ciò che potrebbe implicitamente eliminare B. (Sebbene il costruttore non compaia nell'elenco sopra, in VS2017 anche il costruttore ha bisogno della definizione di B. E questo ha senso se si considera che in caso di un'eccezione nel costruttore, unique_ptr viene nuovamente distrutto.)


1

La definizione completa della Cosa è richiesta al momento dell'istanza del modello. Questo è il motivo esatto per cui viene compilato il linguaggio del brufolo.

Se non fosse possibile, le persone non farebbero domande come questa .



-7

Come per me,

QList<QSharedPointer<ControllerBase>> controllers;

Includi solo l'intestazione ...

#include <QSharedPointer>

Risposta non correlata e non pertinente alla domanda.
Mikus,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.