std :: unique_ptr dengan tipe tidak lengkap tidak akan dikompilasi

203

Saya menggunakan idiom-jerawat dengan std::unique_ptr:

class window {
  window(const rectangle& rect);

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

Namun, saya mendapatkan kesalahan kompilasi tentang penggunaan tipe yang tidak lengkap, pada baris 304 di <memory>:

Aplikasi ' sizeof' ke jenis yang tidak lengkap ' uixx::window::window_impl' tidak valid

Sejauh yang saya tahu, std::unique_ptrharus bisa digunakan dengan tipe yang tidak lengkap. Apakah ini bug di libc ++ atau saya melakukan sesuatu yang salah di sini?


sumber
Tautan referensi untuk persyaratan kelengkapan: stackoverflow.com/a/6089065/576911
Howard Hinnant
1
Jerawat sering dibuat dan tidak dimodifikasi sejak saat itu. Saya biasanya menggunakan std :: shared_ptr <const window_impl>
mfnx
Terkait: Saya ingin tahu mengapa ini bekerja di MSVC, dan bagaimana mencegahnya agar tidak berfungsi (sehingga saya tidak merusak kompilasi rekan-rekan GCC saya).
Len

Jawaban:

258

Berikut adalah beberapa contoh std::unique_ptrdengan tipe tidak lengkap. Masalahnya terletak pada kehancuran.

Jika Anda menggunakan pimpl unique_ptr, Anda harus mendeklarasikan sebuah 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
};

karena jika tidak kompiler menghasilkan yang default, dan perlu deklarasi lengkap foo::impluntuk ini.

Jika Anda memiliki konstruktor template, maka Anda kacau, bahkan jika Anda tidak membangun impl_anggota:

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

Pada lingkup namespace, menggunakan unique_ptrtidak akan berfungsi:

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

karena kompiler harus tahu di sini cara menghancurkan objek durasi statis ini. Solusi adalah:

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;
Alexandre C.
sumber
3
Saya menemukan solusi pertama Anda (menambahkan foo destructor) memungkinkan deklarasi kelas itu sendiri untuk dikompilasi, tetapi mendeklarasikan objek jenis itu di mana saja menghasilkan kesalahan asli ("aplikasi 'sizeof' ...") yang tidak valid.
Jeff Trull
38
jawaban yang sangat baik, hanya untuk diperhatikan; kita masih dapat menggunakan konstruktor / destruktor default dengan menempatkan misalnya foo::~foo() = default;dalam file src
assem
2
Salah satu cara untuk hidup dengan konstruktor template adalah dengan mendeklarasikan tetapi tidak mendefinisikan konstruktor dalam tubuh kelas, mendefinisikannya di suatu tempat definisi impl lengkap terlihat dan secara eksplisit instantiate semua instantiasi yang diperlukan di sana.
enobayram
2
Bisakah Anda menjelaskan bagaimana ini akan bekerja dalam beberapa kasus dan tidak pada yang lain? Saya telah menggunakan idiom jerawat dengan unique_ptr dan kelas tanpa destruktor, dan dalam proyek lain kode saya gagal dikompilasi dengan kesalahan OP yang disebutkan ..
Curious
1
Tampaknya jika nilai default untuk unique_ptr diatur ke {nullptr} di file header kelas dengan gaya c ++ 11, deklarasi lengkap juga diperlukan karena alasan di atas.
feirainy
53

Seperti yang disebutkan oleh Alexandre C. , masalahnya windowadalah destruktor didefinisikan secara implisit di tempat-tempat di mana tipe window_implmasih belum lengkap. Selain solusinya, solusi lain yang saya gunakan adalah mendeklarasikan fungsi Deleter di header:

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

Perhatikan bahwa menggunakan fungsi Deleter kustom menghalangi penggunaan std::make_unique(tersedia dari C ++ 14), seperti yang sudah dibahas di sini .

Fernando Costa Bertoldi
sumber
6
Ini adalah solusi yang tepat sejauh yang saya ketahui. Itu tidak unik untuk menggunakan pimpl-idiom, itu masalah umum dengan menggunakan std :: unique_ptr dengan kelas yang tidak lengkap. Deleter default yang digunakan oleh std :: unique_ptr <X> mencoba melakukan "hapus X", yang tidak bisa dilakukan jika X adalah deklarasi maju. Dengan menentukan fungsi deleter, Anda bisa meletakkan fungsi itu di file sumber di mana kelas X didefinisikan sepenuhnya. File sumber lain kemudian dapat menggunakan std :: unique_ptr <X, DeleterFunc> meskipun X hanyalah deklarasi maju selama mereka ditautkan dengan file sumber yang mengandung DeleterFunc.
sheltond
1
Ini adalah solusi yang baik ketika Anda harus memiliki definisi fungsi sebaris membuat turunan dari jenis "Foo" Anda (misalnya metode "getInstance" statis yang merujuk konstruktor dan destruktor), dan Anda tidak ingin memindahkan ini ke dalam file implementasi seperti yang disarankan @ adspx5.
GameSalutes
20

gunakan deleter khusus

Masalahnya adalah bahwa unique_ptr<T>harus memanggil destructor T::~T()di destructor sendiri, operator penugasan langkahnya, dan unique_ptr::reset()fungsi anggota (hanya). Namun, ini harus dipanggil (secara implisit atau eksplisit) dalam beberapa situasi PIMPL (sudah ada di destruktor kelas luar dan operator penugasan pemindahan).

Seperti sudah ditunjukkan dalam jawaban lain, salah satu cara untuk menghindari itu adalah untuk memindahkan semua operasi yang membutuhkan unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)dan unique_ptr::reset()ke dalam file sumber di mana kelas pimpl helper sebenarnya didefinisikan.

Namun, ini agak merepotkan dan menentang titik pimpl idoim sampai batas tertentu. Solusi yang jauh lebih bersih yang menghindari semua itu adalah dengan menggunakan custom deleter dan hanya memindahkan definisinya ke file sumber tempat tinggal kelas pembantu jerawat. Ini adalah contoh sederhana:

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

Alih-alih kelas deleter yang terpisah, Anda juga dapat menggunakan fungsi gratis atau staticanggota foodalam hubungannya dengan lambda:

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

Mungkin Anda memiliki beberapa fungsi tubuh dalam file .h dalam kelas yang menggunakan tipe tidak lengkap.

Pastikan bahwa dalam .h Anda untuk jendela kelas Anda hanya memiliki deklarasi fungsi. Semua badan fungsi untuk jendela harus dalam file .cpp. Dan untuk window_impl juga ...

Btw, Anda harus secara eksplisit menambahkan deklarasi destructor untuk kelas windows di file .h Anda.

Tapi Anda TIDAK BISA memasukkan tubuh dokumen kosong ke file header Anda:

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

Harus hanya deklarasi:

  class window {
    virtual ~window();
  }
adspx5
sumber
Ini juga solusi saya. Jauh lebih ringkas. Hanya saja konstruktor / destruktor Anda dinyatakan dalam header dan didefinisikan dalam file cpp.
Kris Morness
2

Untuk menambah balasan orang lain tentang deleter khusus, di "perpustakaan utilitas" internal kami, saya menambahkan header pembantu untuk menerapkan pola umum ini ( std::unique_ptrdari tipe yang tidak lengkap, hanya diketahui oleh beberapa TU untuk mis. Menghindari waktu kompilasi yang lama atau untuk menyediakan hanya pegangan buram untuk klien).

Ini menyediakan perancah umum untuk pola ini: kelas deleter kustom yang memanggil fungsi deleter yang ditentukan secara eksternal, tipe alias untuk dengan unique_ptrdengan kelas deleter ini, dan makro untuk mendeklarasikan fungsi deleter dalam TU yang memiliki definisi lengkap dari Tipe. Saya pikir ini memiliki kegunaan umum, jadi ini dia:

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

Mungkin bukan solusi terbaik, tetapi terkadang Anda dapat menggunakan shared_ptr sebagai gantinya. Jika tentu saja ini agak berlebihan, tapi ... seperti untuk unique_ptr, saya mungkin akan menunggu 10 tahun lagi sampai pembuat standar C ++ akan memutuskan untuk menggunakan lambda sebagai deleter.

Sisi lain. Per kode Anda itu mungkin terjadi, bahwa pada tahap kehancuran window_impl tidak akan lengkap. Ini bisa menjadi alasan perilaku yang tidak terdefinisi. Lihat ini: Mengapa, sungguh, menghapus tipe yang tidak lengkap adalah perilaku yang tidak terdefinisi?

Jadi, jika mungkin saya akan mendefinisikan objek yang sangat dasar untuk semua objek Anda, dengan destruktor virtual. Dan kamu hampir baik. Anda hanya perlu mengingat bahwa sistem akan memanggil penghancur virtual untuk pointer Anda, jadi Anda harus mendefinisikannya untuk setiap leluhur. Anda juga harus mendefinisikan kelas dasar di bagian warisan sebagai virtual (lihat ini untuk detailnya).

Stepan Dyatkovskiy
sumber