Menggunakan pointer cerdas untuk anggota kelas

159

Saya mengalami kesulitan memahami penggunaan pointer cerdas sebagai anggota kelas di C ++ 11. Saya telah membaca banyak tentang pointer cerdas dan saya pikir saya mengerti bagaimana unique_ptrdan shared_ptr/ weak_ptrsecara umum bekerja. Yang tidak saya mengerti adalah penggunaan sebenarnya. Sepertinya semua orang merekomendasikan menggunakan unique_ptrsebagai cara untuk pergi hampir sepanjang waktu. Tetapi bagaimana saya mengimplementasikan sesuatu seperti ini:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Katakanlah saya ingin mengganti pointer dengan pointer pintar. A unique_ptrtidak akan bekerja karena getDevice(), kan? Jadi saat itulah saya menggunakan shared_ptrdan weak_ptr? Tidak bisa menggunakan unique_ptr? Menurut saya untuk kebanyakan kasus shared_ptrlebih masuk akal kecuali saya menggunakan pointer dalam lingkup yang sangat kecil?

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Apakah itu cara untuk pergi? Terima kasih banyak!

michaelk
sumber
4
Ini membantu untuk menjadi sangat jelas untuk seumur hidup, kepemilikan dan kemungkinan nol. Misalnya, setelah diteruskan deviceke konstruktor settings, apakah Anda ingin tetap dapat merujuknya dalam lingkup panggilan, atau hanya melalui settings? Jika yang terakhir, unique_ptrberguna. Juga, apakah Anda memiliki skenario di mana nilai baliknya getDevice()adalah null. Jika tidak, cukup kembalikan referensi.
Keith
2
Ya, a shared_ptrbenar dalam 8/10 kasus. 2/10 lainnya dibagi antara unique_ptrdan weak_ptr. Juga, weak_ptrumumnya digunakan untuk memecah referensi melingkar; Saya tidak yakin penggunaan Anda akan dianggap benar.
Collin Dauphinee
2
Pertama-tama, kepemilikan apa yang Anda inginkan untuk deviceanggota data? Anda pertama-tama harus memutuskan itu.
juanchopanza
1
Ok, saya mengerti bahwa sebagai penelepon saya dapat menggunakan unique_ptralih - alih dan menyerahkan kepemilikan ketika memanggil konstruktor, jika saya tahu saya tidak akan memerlukannya lagi untuk saat ini. Tetapi sebagai perancang Settingskelas saya tidak tahu apakah penelepon ingin menyimpan referensi juga. Mungkin perangkat itu akan digunakan di banyak tempat. Ok, mungkin itu maksud Anda. Dalam hal ini, saya tidak akan menjadi pemilik tunggal dan saat itulah saya akan menggunakan shared_ptr, saya kira. Dan: poin yang sangat pintar menggantikan pointer, tetapi bukan referensi, bukan?
michaelk
this-> device = device; Juga gunakan daftar inisialisasi.
Nils

Jawaban:

202

A unique_ptrtidak akan bekerja karena getDevice(), kan?

Tidak, belum tentu. Yang penting di sini adalah menentukan kebijakan kepemilikan yang sesuai untuk Deviceobjek Anda , yaitu siapa yang akan menjadi pemilik objek yang ditunjuk oleh pointer (pintar) Anda.

Apakah itu akan menjadi contoh Settingsobjek saja ? Apakah Deviceobjek harus dihancurkan secara otomatis ketika Settingsobjek dihancurkan, atau haruskah itu hidup lebih lama dari objek itu?

Dalam kasus pertama, std::unique_ptradalah apa yang Anda butuhkan, karena itu membuat Settingssatu-satunya (unik) pemilik objek runcing, dan satu-satunya objek yang bertanggung jawab atas kehancurannya.

Di bawah asumsi ini, getDevice()harus mengembalikan pointer mengamati sederhana (pointer mengamati adalah pointer yang tidak membuat objek runcing tetap hidup). Jenis paling sederhana dari mengamati pointer adalah pointer mentah:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ CATATAN 1: Anda mungkin bertanya-tanya mengapa saya menggunakan pointer mentah di sini, ketika semua orang terus mengatakan bahwa pointer mentah itu buruk, tidak aman, dan berbahaya. Sebenarnya, itu adalah peringatan yang berharga, tetapi penting untuk menempatkannya dalam konteks yang benar: pointer mentah buruk ketika digunakan untuk melakukan manajemen memori manual , yaitu mengalokasikan dan membatalkan alokasi objek melalui newdan delete. Ketika digunakan semata-mata sebagai sarana untuk mencapai semantik referensi dan mengoper pointer yang tidak memiliki, mengamati, tidak ada yang secara intrinsik berbahaya dalam pointer mentah, kecuali mungkin karena fakta bahwa seseorang harus berhati-hati untuk tidak merujuk pada pointer yang menggantung. - CATATAN AKHIR 1 ]

[ CATATAN 2: Seperti yang muncul dalam komentar, dalam kasus khusus ini di mana kepemilikan adalah unik dan objek yang dimiliki selalu dijamin untuk hadir (yaitu anggota data internal devicetidak akan pernah ada nullptr), fungsi getDevice()dapat (dan mungkin harus) kembalikan referensi daripada sebuah pointer. Meskipun ini benar, saya memutuskan untuk mengembalikan pointer mentah di sini karena saya maksudkan ini untuk menjadi jawaban singkat bahwa seseorang dapat menggeneralisasi ke kasus di mana devicebisa nullptr, dan untuk menunjukkan bahwa pointer mentah tidak masalah selama orang tidak menggunakannya untuk manajemen memori manual. - END END 2 ]


Situasinya sangat berbeda, tentu saja, jika Settingsobjek Anda seharusnya tidak memiliki kepemilikan eksklusif perangkat. Ini bisa menjadi kasus, misalnya, jika penghancuran Settingsobjek tidak boleh menyiratkan penghancuran Deviceobjek yang runcing juga.

Ini adalah sesuatu yang hanya bisa Anda ketahui sebagai perancang program Anda; Dari contoh yang Anda berikan, sulit bagi saya untuk mengatakan apakah ini masalahnya atau tidak.

Untuk membantu Anda mengetahuinya, Anda dapat bertanya pada diri sendiri apakah ada objek lain selain dari Settingsyang berhak untuk menjaga Deviceobjek tetap hidup selama mereka memegang pointer ke sana, alih-alih hanya menjadi pengamat pasif. Jika memang demikian masalahnya, maka Anda memerlukan kebijakan kepemilikan bersama , yang std::shared_ptrmenawarkan:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Perhatikan, itu weak_ptradalah pointer yang mengamati , bukan pointer yang memiliki - dengan kata lain, itu tidak membuat objek runcing tetap hidup jika semua pointer memiliki lainnya ke objek runcing keluar dari ruang lingkup.

Keuntungan dari weak_ptratas pointer mentah biasa adalah bahwa Anda dapat dengan aman mengatakan apakah weak_ptrini menggantung atau tidak (yakni apakah itu menunjuk ke sebuah objek yang valid, atau jika objek awalnya menunjuk telah dihancurkan). Ini dapat dilakukan dengan memanggil expired()fungsi anggota pada weak_ptrobjek.

Andy Prowl
sumber
4
@LKK: Ya, benar. A weak_ptrselalu merupakan alternatif untuk pointer pengamatan mentah. Ini lebih aman dalam arti, karena Anda dapat memeriksa apakah itu menggantung sebelum dereferensi, tetapi juga dilengkapi dengan beberapa overhead. Jika Anda dapat dengan mudah menjamin bahwa Anda tidak akan melakukan dereference pada pointer yang tergantung, maka Anda akan baik-baik saja dengan mengamati pointer mentah
Andy Prowl
6
Dalam kasus pertama mungkin akan lebih baik untuk membiarkan getDevice()mengembalikan referensi, bukan? Jadi penelepon tidak perlu memeriksa nullptr.
vobject
5
@ Chico: Tidak yakin apa yang Anda maksud. auto myDevice = settings.getDevice()akan membuat instance baru dari tipe yang Devicedipanggil myDevicedan menyalin-membangunnya dari yang direferensikan oleh referensi yang getDevice()mengembalikan. Jika Anda ingin myDevicemenjadi referensi, Anda perlu melakukannya auto& myDevice = settings.getDevice(). Jadi kecuali saya kehilangan sesuatu, kami kembali ke situasi yang sama tanpa menggunakan auto.
Andy Prowl
2
@ Performa: Karena Anda tidak ingin memberikan kepemilikan objek - menyerahkan yang dapat dimodifikasi unique_ptrke klien membuka kemungkinan bahwa klien akan pindah dari sana, sehingga memperoleh kepemilikan dan meninggalkan Anda dengan pointer nol (unik).
Andy Prowl
7
@ Performance: Walaupun itu akan mencegah klien dari bergerak (kecuali jika klien adalah ilmuwan gila yang tertarik pada const_casts), saya pribadi tidak akan melakukannya. Ini memperlihatkan detail implementasi, yaitu kenyataan bahwa kepemilikan adalah unik dan diwujudkan melalui a unique_ptr. Saya melihat hal-hal seperti ini: jika Anda ingin / perlu melewati / mengembalikan kepemilikan, meneruskan / mengembalikan pointer pintar ( unique_ptratau shared_ptr, tergantung pada jenis kepemilikan). Jika Anda tidak ingin / perlu meneruskan / mengembalikan kepemilikan, gunakan constpointer atau referensi (dengan kualifikasi yang tepat ), sebagian besar tergantung pada apakah argumennya bisa nol atau tidak.
Andy Prowl
0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptrhanya digunakan untuk loop referensi. Grafik ketergantungan harus grafik yang diarahkan acyclic. Dalam pointer bersama ada 2 jumlah referensi: 1 untuk shared_ptrs, dan 1 untuk semua pointer ( shared_ptrdan weak_ptr). Ketika semua shared_ptrs dihapus, pointer dihapus. Ketika pointer diperlukan dari weak_ptr, lockharus digunakan untuk mendapatkan pointer, jika ada.

Naszta
sumber
Jadi, jika saya memahami jawaban Anda dengan benar, smart pointer dapat menggantikan pointer mentah, tetapi belum tentu referensi?
michaelk
Apakah sebenarnya ada dua jumlah referensi dalam shared_ptr? Bisakah Anda jelaskan mengapa? Sejauh yang saya mengerti, weak_ptrtidak harus dihitung karena hanya membuat yang baru shared_ptrketika beroperasi pada objek (jika objek yang mendasarinya masih ada).
Björn Pollex
@ BjörnPollex: Saya membuat contoh singkat untuk Anda: tautan . Saya belum mengimplementasikan semuanya hanya copy constructor dan lock. versi boost juga aman untuk penghitungan referensi ( deletedisebut hanya sekali).
Naszta
@Naszta: Contoh Anda menunjukkan bahwa adalah mungkin untuk menerapkan ini menggunakan dua jumlah referensi, tetapi jawaban Anda menunjukkan bahwa ini diperlukan , yang saya tidak percaya. Bisakah Anda menjelaskan ini dalam jawaban Anda?
Björn Pollex
1
@ BjörnPollex, untuk weak_ptr::lock()mengetahui apakah objek telah kadaluwarsa, ia harus memeriksa "blok kontrol" yang berisi jumlah referensi pertama dan penunjuk ke objek, sehingga blok kontrol tidak boleh dihancurkan sementara ada weak_ptrbenda yang masih digunakan, jadi jumlah weak_ptrobjek harus dilacak, yang merupakan jumlah referensi kedua. Objek hancur ketika hitungan ref pertama turun ke nol, blok kontrol hancur ketika hitungan ref kedua turun ke nol.
Jonathan Wakely