Apakah std :: unique_ptr <T> diperlukan untuk mengetahui definisi lengkap T?

248

Saya memiliki beberapa kode di header yang terlihat seperti ini:

#include <memory>

class Thing;

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

Jika saya menyertakan header ini dalam cpp yang tidak menyertakan Thingdefinisi tipe, maka ini tidak dikompilasi di bawah VS2010-SP1:

1> C: \ Program Files (x86) \ Microsoft Visual Studio 10.0 \ VC \ include \ memory (2067): error C2027: penggunaan tipe 'Hal' yang tidak ditentukan

Ganti std::unique_ptroleh std::shared_ptrdan dikompilasi.

Jadi, saya menduga bahwa itu adalah std::unique_ptrimplementasi VS2010 saat ini yang membutuhkan definisi penuh dan itu sepenuhnya tergantung pada implementasi.

Atau itu? Apakah ada sesuatu di dalamnya persyaratan standar yang membuat std::unique_ptrimplementasi tidak mungkin bekerja hanya dengan deklarasi maju? Rasanya aneh karena seharusnya hanya memegang pointer Thing, bukan?

Klaim
sumber
20
Penjelasan terbaik tentang kapan Anda perlu dan tidak perlu jenis lengkap dengan C ++ 0x smart pointer adalah Howard Hinnant "Jenis tidak lengkap dan shared_ptr/ unique_ptr" Tabel di bagian akhir harus menjawab pertanyaan Anda.
James McNellis
17
Terima kasih untuk penunjuknya James. Saya lupa di mana saya meletakkan meja itu! :-)
Howard Hinnant
5
@JamesMcNellis Tautan ke situs web Howard Hinnant sedang down. Ini adalah versi web.archive.org . Bagaimanapun, dia menjawabnya dengan sempurna di bawah ini dengan konten yang sama :-)
Ela782
Penjelasan lain yang baik diberikan dalam Butir 22 dari C ++ modern Efektif Scott Meyers.
Fred Schoen

Jawaban:

328

Diadopsi dari sini .

Sebagian besar templat di pustaka standar C ++ mengharuskan mereka dipakai dengan tipe lengkap. Namun shared_ptrdan unique_ptrmerupakan pengecualian parsial . Beberapa, tetapi tidak semua anggota mereka dapat dipakai dengan tipe tidak lengkap. Motivasi untuk ini adalah untuk mendukung idiom seperti jerawat menggunakan pointer pintar, dan tanpa risiko perilaku yang tidak terdefinisi.

Perilaku tidak terdefinisi dapat terjadi ketika Anda memiliki tipe yang tidak lengkap dan Anda memanggilnya delete:

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

Kode di atas adalah hukum. Itu akan dikompilasi. Kompiler Anda mungkin atau mungkin tidak memancarkan peringatan untuk kode di atas seperti di atas. Ketika dijalankan, hal-hal buruk mungkin akan terjadi. Jika Anda sangat beruntung program Anda akan macet. Namun hasil yang lebih mungkin adalah bahwa program Anda akan secara diam-diam membocorkan memori karena ~A()tidak akan dipanggil.

Menggunakan auto_ptr<A>contoh di atas tidak membantu. Anda masih mendapatkan perilaku tidak terdefinisi yang sama seolah-olah Anda menggunakan pointer mentah.

Meskipun demikian, menggunakan kelas yang tidak lengkap di tempat-tempat tertentu sangat berguna! Di sinilah shared_ptrdan unique_ptrmembantu. Penggunaan salah satu dari pointer cerdas ini akan memungkinkan Anda untuk pergi dengan tipe yang tidak lengkap, kecuali jika perlu untuk memiliki tipe yang lengkap. Dan yang paling penting, ketika perlu memiliki tipe yang lengkap, Anda mendapatkan kesalahan waktu kompilasi jika Anda mencoba menggunakan smart pointer dengan tipe yang tidak lengkap pada saat itu.

Tidak ada lagi perilaku yang tidak terdefinisi:

Jika kode Anda dikompilasi, maka Anda telah menggunakan tipe lengkap di mana pun Anda perlu.

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

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

shared_ptrdan unique_ptrmembutuhkan jenis yang lengkap di tempat yang berbeda. Alasannya tidak jelas, berkaitan dengan deleter dinamis vs deleter statis. Alasan tepatnya tidak penting. Faktanya, dalam kebanyakan kode tidak terlalu penting bagi Anda untuk mengetahui dengan tepat di mana jenis yang lengkap diperlukan. Hanya kode, dan jika Anda salah, kompiler akan memberi tahu Anda.

Namun, jika berguna bagi Anda, berikut adalah tabel yang mendokumentasikan beberapa anggota shared_ptrdan unique_ptrberkenaan dengan persyaratan kelengkapan. Jika anggota membutuhkan tipe yang lengkap, maka entri memiliki "C", jika tidak, entri tabel diisi dengan "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        |
+------------------------+---------------+---------------+

Operasi apa pun yang memerlukan konversi pointer memerlukan tipe yang lengkap untuk keduanya unique_ptrdan shared_ptr.

The unique_ptr<A>{A*}konstruktor bisa lolos dengan lengkap Ahanya jika compiler tidak diperlukan untuk membuat sebuah panggilan untuk ~unique_ptr<A>(). Misalnya jika Anda meletakkannya unique_ptrdi tumpukan, Anda bisa lolos dengan tidak lengkap A. Rincian lebih lanjut tentang hal ini dapat ditemukan dalam BarryTheHatchet ini jawaban di sini .

Howard Hinnant
sumber
3
Jawaban yang sangat bagus. Saya akan memberi 5 jika saya bisa. Saya yakin saya akan merujuk kembali ke ini dalam proyek saya berikutnya, di mana saya mencoba untuk memanfaatkan sepenuhnya petunjuk pintar.
matthias
4
jika seseorang dapat menjelaskan apa artinya tabel itu, saya kira itu akan membantu lebih banyak orang
Ghita
8
Satu catatan lagi: Konstruktor kelas akan merujuk destruktor dari anggotanya (untuk kasus di mana pengecualian dilemparkan, destruktor tersebut perlu dipanggil). Jadi sementara destructor unique_ptr membutuhkan tipe yang lengkap, itu tidak cukup untuk memiliki destruktor yang ditentukan pengguna di kelas - itu juga membutuhkan konstruktor.
Johannes Schaub - litb
7
@Mehrdad: Keputusan ini dibuat untuk C ++ 98, yang sebelum waktu saya. Namun saya percaya keputusan itu datang dari keprihatinan tentang implementabilitas, dan kesulitan spesifikasi (yaitu bagian mana dari suatu wadah yang tidak atau tidak memerlukan jenis yang lengkap). Bahkan hari ini, dengan 15 tahun pengalaman sejak C ++ 98, akan menjadi tugas yang tidak sepele untuk mengendurkan spesifikasi kontainer di area ini, dan memastikan bahwa Anda tidak melarang teknik implementasi atau optimisasi yang penting. Saya pikir itu bisa dilakukan. Saya tahu itu akan banyak pekerjaan. Saya sadar ada satu orang yang berusaha.
Howard Hinnant
9
Karena itu tidak jelas dari komentar di atas, bagi siapa saja yang memiliki masalah ini karena mereka mendefinisikan unique_ptrsebagai variabel anggota kelas, hanya secara eksplisit menyatakan destructor (dan konstruktor) dalam deklarasi kelas (dalam file header) dan melanjutkan ke mendefinisikan mereka dalam file sumber (dan letakkan header dengan deklarasi lengkap kelas runcing ke dalam file sumber) untuk mencegah kompilator secara otomatis memasukkan konstruktor atau destruktor pada file header (yang memicu kesalahan). stackoverflow.com/a/13414884/368896 juga membantu mengingatkan saya akan hal ini.
Dan Nissenbaum
42

Kompiler memerlukan definisi Hal untuk menghasilkan destruktor default untuk MyClass. Jika Anda secara eksplisit mendeklarasikan destructor dan memindahkan implementasinya (kosong) ke file CPP, kode tersebut harus dikompilasi.

Igor Nazarenko
sumber
5
Saya pikir ini adalah kesempatan sempurna untuk menggunakan fungsi default. MyClass::~MyClass() = default;dalam file implementasi tampaknya kurang mungkin dihapus secara tidak sengaja nanti di jalan oleh seseorang yang menganggap tubuh destuctor telah dihapus daripada sengaja dibiarkan kosong.
Dennis Zickefoose
@ Dennis Zickefoose: Sayangnya OP menggunakan VC ++, dan VC ++ belum mendukung anggota kelas defaulted dan deleted.
ildjarn
6
+1 untuk cara memindahkan pintu ke file .cpp. Juga sepertinya MyClass::~MyClass() = defaulttidak memindahkannya ke file implementasi di Dentang. (belum?)
Eonil
Anda juga perlu memindahkan implementasi konstruktor ke file CPP, setidaknya pada VS 2017. Lihat misalnya jawaban ini: stackoverflow.com/a/27624369/5124002
jciloa
15

Ini tidak tergantung pada implementasi. Alasan kerjanya adalah karena shared_ptrmenentukan destructor yang tepat untuk dipanggil saat run-time - itu bukan bagian dari tipe signature. Namun, unique_ptrdestructor adalah bagian dari tipenya, dan harus diketahui pada saat kompilasi.

Anak anjing
sumber
8

Sepertinya jawaban saat ini tidak benar-benar memaku mengapa konstruktor default (atau destruktor) adalah masalah tetapi yang kosong dinyatakan dalam cpp tidak.

Inilah yang terjadi:

Jika kelas luar (yaitu MyClass) tidak memiliki konstruktor atau destruktor maka kompiler menghasilkan yang standar. Masalah dengan ini adalah bahwa kompiler pada dasarnya menyisipkan konstruktor / destruktor kosong default dalam file .hpp. Ini berarti bahwa kode untuk contructor / destructor default akan dikompilasi bersama dengan biner yang dapat dieksekusi host, tidak bersama dengan biner perpustakaan Anda. Namun definisi ini tidak dapat benar-benar membangun kelas parsial. Jadi ketika linker masuk ke biner perpustakaan Anda dan mencoba untuk mendapatkan konstruktor / destruktor, itu tidak menemukan apapun dan Anda mendapatkan kesalahan. Jika kode konstruktor / destruktor ada di .cpp Anda maka biner perpustakaan Anda memiliki yang tersedia untuk ditautkan.

Ini tidak ada hubungannya dengan menggunakan unique_ptr atau shared_ptr dan jawaban lain tampaknya mungkin membingungkan bug di VC ++ lama untuk implementasi unique_ptr (VC ++ 2015 berfungsi dengan baik di mesin saya).

Jadi moral dari cerita ini adalah bahwa tajuk Anda harus tetap bebas dari definisi konstruktor / destruktor. Itu hanya dapat berisi deklarasi mereka. Misalnya, ~MyClass()=default;dalam hpp tidak akan berfungsi. Jika Anda mengizinkan kompiler untuk memasukkan konstruktor atau destruktor default, Anda akan mendapatkan kesalahan linker.

Catatan sisi lain: Jika Anda masih mendapatkan kesalahan ini bahkan setelah Anda memiliki konstruktor dan destruktor dalam file cpp maka kemungkinan besar alasannya adalah bahwa perpustakaan Anda tidak dapat dikompilasi dengan benar. Sebagai contoh, suatu kali saya hanya mengubah jenis proyek dari Console ke Library di VC ++ dan saya mendapatkan kesalahan ini karena VC ++ tidak menambahkan simbol preprocessor _LIB dan yang menghasilkan pesan kesalahan yang sama persis.

Shital Shah
sumber
Terima kasih! Itu adalah penjelasan yang sangat singkat tentang kekhasan C ++ yang sangat tidak jelas. Menyelamatkan saya banyak masalah.
JPNotADragon
5

Hanya untuk kelengkapan:

Header: Ah

class B; // forward declaration

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

Sumber A.cpp:

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

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

Definisi kelas B harus dilihat oleh konstruktor, destruktor, dan apa pun yang mungkin secara implisit menghapus B. (Meskipun konstruktor tidak muncul dalam daftar di atas, dalam VS2017 bahkan konstruktor memerlukan definisi B. Dan ini masuk akal ketika mempertimbangkan bahwa dalam kasus pengecualian pada constructor, unique_ptr dihancurkan lagi.)

Joachim
sumber
1

Definisi penuh dari Hal ini diperlukan pada titik instantiasi template. Ini adalah alasan yang tepat mengapa idiom pompl mengkompilasi.

Jika tidak mungkin, orang tidak akan bertanya seperti ini .

BЈовић
sumber
-2

Jawaban sederhananya adalah gunakan shared_ptr saja.

deltanin
sumber
-7

Bagi saya,

QList<QSharedPointer<ControllerBase>> controllers;

Cukup sertakan tajuk ...

#include <QSharedPointer>
Sanbrother
sumber
Jawaban tidak terkait dan tidak relevan dengan pertanyaan.
Mikus