std :: unique_ptr con un tipo incompleto non verrà compilato


203

Sto usando l'idioma pimpl con std::unique_ptr:

class window {
  window(const rectangle& rect);

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

Tuttavia, viene visualizzato un errore di compilazione relativo all'uso di un tipo incompleto, nella riga 304 in <memory>:

Applicazione non valida di " sizeof" a un tipo incompleto " uixx::window::window_impl"

Per quanto ne so, std::unique_ptrdovrebbe essere in grado di essere utilizzato con un tipo incompleto. È un bug in libc ++ o sto facendo qualcosa di sbagliato qui?


Link di riferimento per i requisiti di completezza: stackoverflow.com/a/6089065/576911
Howard Hinnant

1
Un brufolo viene spesso costruito e non modificato da allora. Di solito uso uno std :: shared_ptr <const window_impl>
mfnx

Correlati: Mi piacerebbe molto sapere perché funziona in MSVC e come impedirne il funzionamento (in modo da non interrompere le compilation dei miei colleghi GCC).
Len

Risposte:


258

Ecco alcuni esempi di std::unique_ptrtipi incompleti. Il problema sta nella distruzione.

Se usi pimpl con unique_ptr, devi dichiarare un distruttore:

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

perché altrimenti il ​​compilatore ne genera uno predefinito e per questo ha bisogno di una dichiarazione completa foo::impl.

Se hai costruttori di modelli, sei fregato, anche se non costruisci il impl_membro:

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

Nell'ambito dello spazio dei nomi, l'utilizzo unique_ptrnon funzionerà neanche:

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

poiché il compilatore deve sapere qui come distruggere questo oggetto di durata statica. Una soluzione alternativa è:

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

3
Trovo che la tua prima soluzione (aggiungendo il foo destructor) consenta la compilazione della stessa dichiarazione di classe, ma dichiarare un oggetto di quel tipo ovunque si traduca nell'errore originale ("applicazione non valida di 'sizeof' ...").
Jeff Trull,

38
risposta eccellente, solo da notare; possiamo ancora usare il costruttore / distruttore predefinito inserendo ad es. foo::~foo() = default;nel file src
assem

2
Un modo di convivere con i costruttori di template sarebbe quello di dichiarare ma non definire il costruttore nel corpo della classe, definirlo da qualche parte in cui si vede la definizione completa di impl e istanziare esplicitamente tutte le istanze necessarie lì.
enobayram,

2
Potresti spiegare come funzionerebbe in alcuni casi e non in altri? Ho usato l'idioma del pimpl con un unique_ptr e una classe senza distruttore, e in un altro progetto il mio codice non riesce a compilare con l'errore OP menzionato ..
Curioso

1
Sembra che se il valore predefinito per unique_ptr sia impostato su {nullptr} nel file di intestazione della classe con stile c ++ 11, è necessaria anche una dichiarazione completa per il motivo precedente.
feirainy

53

Come accennato da Alexandre C. , il problema deriva dal fatto che windowil distruttore viene implicitamente definito in luoghi in cui il tipo di window_implè ancora incompleto. Oltre alle sue soluzioni, un'altra soluzione alternativa che ho usato è dichiarare un funzione Deleter nell'intestazione:

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

Si noti che l'utilizzo di una funzione personalizzata Deleter preclude l'uso di std::make_unique(disponibile da C ++ 14), come già discusso qui .


6
Questa è la soluzione corretta per quanto mi riguarda. Non è univoco nell'uso dell'idioma pimpl, è un problema generale nell'uso di std :: unique_ptr con classi incomplete. Il deleter predefinito utilizzato da std :: unique_ptr <X> tenta di eseguire "elimina X", cosa che non può fare se X è una dichiarazione diretta. Specificando una funzione di cancellazione, è possibile inserire tale funzione in un file di origine in cui la classe X è completamente definita. Altri file di origine possono quindi utilizzare std :: unique_ptr <X, DeleterFunc> anche se X è solo una dichiarazione in avanti purché siano collegati al file di origine contenente DeleterFunc.
sheltond

1
Questa è una buona soluzione quando devi avere una definizione di funzione inline che crea un'istanza del tuo tipo "Foo" (ad esempio un metodo statico "getInstance" che fa riferimento a costruttore e distruttore) e non vuoi spostarli in un file di implementazione come suggerisce @ adspx5.
GameSalutes

20

usa un deleter personalizzato

Il problema è che unique_ptr<T>deve chiamare il distruttore T::~T()nel suo proprio distruttore, nel suo operatore di assegnazione dei movimenti e nella unique_ptr::reset()funzione membro (solo). Tuttavia, questi devono essere chiamati (implicitamente o esplicitamente) in diverse situazioni PIMPL (già nel distruttore della classe esterna e nell'operatore di assegnazione dei movimenti).

Come già sottolineato in un'altra risposta, un modo per evitare che è quello di spostare tutte le operazioni che richiedono unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)e unique_ptr::reset()nel file di origine in cui la classe Pimpl helper è in realtà definita.

Tuttavia, questo è piuttosto scomodo e sfida il punto stesso del pimpl idoim in una certa misura. Una soluzione molto più pulita che evita tutto ciò che serve per usare un deleter personalizzato e spostare la sua definizione solo nel file sorgente in cui vive la classe helper brufolo. Qui c'è un semplice esempio:

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

Invece di una classe di deleter separata, puoi anche usare una funzione libera o un staticmembro fooin congiunzione con una lambda:

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

15

Probabilmente hai dei corpi funzione all'interno del file .h all'interno della classe che usa un tipo incompleto.

Assicurati che nella finestra di .h per la classe ci sia solo una dichiarazione di funzione. Tutti i corpi funzione per window devono essere nel file .cpp. E anche per window_impl ...

A proposito, devi aggiungere esplicitamente la dichiarazione del distruttore per la classe windows nel tuo file .h.

Ma NON PUOI inserire il corpo del dtor vuoto nel tuo file di intestazione:

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

Deve essere solo una dichiarazione:

  class window {
    virtual ~window();
  }

Questa era anche la mia soluzione. Molto più conciso. Basta avere il costruttore / distruttore dichiarato nell'intestazione e definito nel file cpp.
Kris Morness,

2

Per aggiungere alle risposte dell'altro sul deleter personalizzato, nella nostra "libreria di utilità" interna ho aggiunto un'intestazione helper per implementare questo modello comune ( std::unique_ptrdi tipo incompleto, noto solo ad alcune TU per evitare, ad esempio, lunghi tempi di compilazione o fornire solo una maniglia opaca per i clienti).

Fornisce lo scaffolding comune per questo modello: una classe di deleter personalizzata che richiama una funzione di deleter definita esternamente, un alias di tipo per a unique_ptrcon questa classe di deleter e una macro per dichiarare la funzione di deleter in una TU che ha una definizione completa del genere. Penso che questo abbia qualche utilità generale, quindi eccolo qui:

#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

Potrebbe non essere la soluzione migliore, ma a volte puoi usare shared_ptr invece. Se ovviamente è un po 'eccessivo, ma ... per quanto riguarda unique_ptr, forse aspetterò 10 anni in più fino a quando i produttori di standard C ++ decideranno di utilizzare lambda come deleter.

Un altro lato. Per il tuo codice può succedere che in fase di distruzione window_impl sarà incompleto. Questo potrebbe essere un motivo di comportamento indefinito. Vedi questo: Perché, davvero, eliminare un tipo incompleto è un comportamento indefinito?

Quindi, se possibile, definirei un oggetto molto base per tutti i tuoi oggetti, con il distruttore virtuale. E sei quasi bravo. Devi solo tenere presente che il sistema chiamerà distruttore virtuale per il tuo puntatore, quindi dovresti definirlo per ogni antenato. Dovresti anche definire la classe base nella sezione ereditarietà come virtuale (vedi questo per i dettagli).

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.