Mengapa shared_ptr <void> legal, sedangkan unique_ptr <void> memiliki format yang salah?

100

Pertanyaannya benar-benar cocok dengan judulnya: Saya penasaran ingin tahu apa alasan teknis dari perbedaan ini, tapi juga alasannya?

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;
Ad N
sumber

Jawaban:

120

Itu karena std::shared_ptrmengimplementasikan penghapusan tipe, sedangkan std::unique_ptrtidak.


Sejak std::shared_ptrmengimplementasikan penghapusan tipe, itu juga mendukung properti menarik lainnya , yaitu. itu tidak membutuhkan tipe deleter sebagai argumen tipe template untuk template kelas. Lihat deklarasi mereka:

template<class T,class Deleter = std::default_delete<T> > 
class unique_ptr;

yang memiliki Deleterparameter tipe, sedangkan

template<class T> 
class shared_ptr;

tidak memilikinya.

Sekarang pertanyaannya adalah, mengapa shared_ptrmenerapkan penghapusan tipe? Ya, ia melakukannya, karena harus mendukung penghitungan referensi, dan untuk mendukungnya, ia harus mengalokasikan memori dari heap dan karena harus mengalokasikan memori, ia melangkah lebih jauh dan mengimplementasikan penghapusan tipe - yang membutuhkan tumpukan alokasi juga. Jadi pada dasarnya itu hanya menjadi oportunis!

Karena tipe-erasure, std::shared_ptrmampu mendukung dua hal:

  • Ia dapat menyimpan objek jenis apa pun void*, namun ia masih dapat menghapus objek yang sedang dihancurkan dengan benar dengan menjalankan destruktornya dengan benar .
  • Tipe deleter tidak diteruskan sebagai argumen tipe ke templat kelas, yang berarti sedikit kebebasan tanpa mengorbankan keamanan tipe .

Baik. Itu semua tentang cara std::shared_ptrkerjanya.

Sekarang pertanyaannya adalah, dapatkah std::unique_ptrmenyimpan objek sebagai void* ? Jawabannya adalah, ya - asalkan Anda memberikan deleter yang sesuai sebagai argumen. Inilah salah satu demonstrasi tersebut:

int main()
{
    auto deleter = [](void const * data ) {
        int const * p = static_cast<int const*>(data);
        std::cout << *p << " located at " << p <<  " is being deleted";
        delete p;
    };

    std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);

} //p will be deleted here, both p ;-)

Output ( demo online ):

959 located at 0x18aec20 is being deleted

Anda mengajukan pertanyaan yang sangat menarik di komentar:

Dalam kasus saya, saya akan memerlukan penghapus penghapus tipe, tetapi tampaknya mungkin juga (dengan biaya alokasi tumpukan). Pada dasarnya, apakah ini berarti sebenarnya ada tempat khusus untuk tipe ketiga penunjuk cerdas: penunjuk cerdas kepemilikan eksklusif dengan penghapusan jenis.

yang @Steve Jessop menyarankan solusi berikut,

Saya belum pernah benar-benar mencoba ini, tetapi mungkin Anda dapat mencapai itu dengan menggunakan yang sesuai std::functionsebagai tipe deleter dengan unique_ptr? Misalkan itu benar-benar berfungsi maka Anda sudah selesai, kepemilikan eksklusif dan penghapus tipe-terhapus.

Mengikuti saran ini, saya menerapkan ini (meskipun tidak memanfaatkan std::functionkarena tampaknya tidak perlu):

using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
    return unique_void_ptr(ptr, [](void const * data) {
         T const * p = static_cast<T const*>(data);
         std::cout << "{" << *p << "} located at [" << p <<  "] is being deleted.\n";
         delete p;
    });
}

int main()
{
    auto p1 = unique_void(new int(959));
    auto p2 = unique_void(new double(595.5));
    auto p3 = unique_void(new std::string("Hello World"));
}  

Output ( demo online ):

{Hello World} located at [0x2364c60] is being deleted.
{595.5} located at [0x2364c40] is being deleted.
{959} located at [0x2364c20] is being deleted.

Semoga membantu.

Nawaz
sumber
13
Jawaban bagus, +1. Tetapi Anda dapat membuatnya lebih baik dengan secara eksplisit menyebutkan bahwa a std::unique_ptr<void, D>masih mungkin dengan menyediakan yang sesuai D.
Angew tidak lagi bangga dengan SO
1
@Angrew: Bagus, Anda menemukan pertanyaan mendasar yang sebenarnya tidak tertulis dalam pertanyaan saya;)
Ad N
@Nawaz: Terima kasih. Dalam kasus saya, saya akan memerlukan penghapus penghapus tipe, tetapi tampaknya mungkin juga (dengan biaya alokasi tumpukan). Pada dasarnya, apakah ini berarti sebenarnya ada tempat khusus untuk tipe ketiga dari penunjuk cerdas: penunjuk cerdas kepemilikan eksklusif dengan penghapusan jenis?
Ad N
8
@AdN: Saya belum pernah benar-benar mencoba ini, tetapi mungkin Anda dapat melakukannya dengan menggunakan yang sesuai std::functionsebagai tipe deleter dengan unique_ptr? Misalkan itu benar-benar berfungsi maka Anda sudah selesai, kepemilikan eksklusif dan penghapus tipe-terhapus.
Steve Jessop
Nit tata bahasa: "mengapa kata kerja X Y?" harus "mengapa tidak X kerja Y?" dalam Bahasa Inggris.
zwol
7

Salah satu alasannya adalah di salah satu dari banyak kasus penggunaan a shared_ptr- yaitu sebagai indikator seumur hidup atau sentinel.

Ini disebutkan dalam dokumentasi pendorong asli:

auto register_callback(std::function<void()> closure, std::shared_ptr<void> pv)
{
    auto closure_target = { closure, std::weak_ptr<void>(pv) };
    ...
    // store the target somewhere, and later....
}

void call_closure(closure_target target)
{
    // test whether target of the closure still exists
    auto lock = target.sentinel.lock();
    if (lock) {
        // if so, call the closure
        target.closure();
    }
}

Dimana closure_targetsesuatu seperti ini:

struct closure_target {
    std::function<void()> closure;
    std::weak_ptr<void> sentinel;
};

Penelepon akan mendaftarkan panggilan balik seperti ini:

struct active_object : std::enable_shared_from_this<active_object>
{
    void start() {
      event_emitter_.register_callback([this] { this->on_callback(); }, 
                                       shared_from_this());
    }

    void on_callback()
    {
        // this is only ever called if we still exist 
    }
};

karena shared_ptr<X>selalu dapat diubah shared_ptr<void>, event_emitter sekarang dapat dengan senang hati tidak menyadari jenis objek yang dipanggil kembali.

Pengaturan ini melepaskan pelanggan ke event emitter dari kewajiban menangani kasus penyeberangan (bagaimana jika callback masuk dalam antrian, menunggu untuk ditindaklanjuti sementara active_object menghilang?), Dan juga berarti tidak perlu menyinkronkan berhenti berlangganan. weak_ptr<void>::lockadalah operasi tersinkronisasi.

Richard Hodges
sumber