Apakah deleter shared_ptr disimpan di memori dialokasikan oleh pengalokasi khusus?

22

Katakanlah saya punya shared_ptrdengan pengalokasi khusus dan deleter khusus.

Saya tidak dapat menemukan apa pun dalam standar yang berbicara tentang di mana deleter harus disimpan: ia tidak mengatakan bahwa pengalokasi kustom akan digunakan untuk memori deleter, dan tidak mengatakan bahwa itu tidak akan terjadi.

Apakah ini tidak ditentukan atau saya hanya melewatkan sesuatu?

Lightness Races di Orbit
sumber

Jawaban:

11

util.smartptr.shared.const / 9 di C ++ 11:

Efek: Membangun objek shared_ptr yang memiliki objek p dan deleter d. Konstruktor kedua dan keempat harus menggunakan salinan a untuk mengalokasikan memori untuk penggunaan internal.

Konstruktor kedua dan keempat memiliki prototipe ini:

template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template<class D, class A> shared_ptr(nullptr_t p, D d, A a);

Dalam konsep terbaru, util.smartptr.shared.const / 10 setara dengan tujuan kami:

Efek: Membangun objek shared_ptr yang memiliki objek p dan deleter d. Ketika T bukan tipe array, konstruktor pertama dan kedua mengaktifkan shared_from_ini dengan hal. Konstruktor kedua dan keempat harus menggunakan salinan a untuk mengalokasikan memori untuk penggunaan internal. Jika pengecualian dilemparkan, d (p) disebut.

Jadi pengalokasi digunakan jika ada kebutuhan untuk mengalokasikannya dalam memori yang dialokasikan. Berdasarkan standar saat ini dan pada laporan cacat yang relevan, alokasi tidak wajib tetapi diambil alih oleh komite.

  • Meskipun antarmuka shared_ptrmemungkinkan implementasi di mana tidak pernah ada blok kontrol dan semua shared_ptrdan weak_ptrdimasukkan ke dalam daftar tertaut, tidak ada implementasi seperti itu dalam praktiknya. Selain itu, kata-katanya telah dimodifikasi dengan asumsi, misalnya, bahwa kata use_countitu dibagikan.

  • Deleter diperlukan untuk hanya bergerak yang dapat dibangun. Dengan demikian, tidak mungkin untuk memiliki beberapa salinan di shared_ptr.

Orang dapat membayangkan implementasi yang menempatkan deleter dalam yang dirancang khusus shared_ptrdan memindahkannya ketika spesial shared_ptrdihapus. Walaupun implementasinya tampak sesuai, ini juga aneh, terutama karena blok kontrol mungkin diperlukan untuk jumlah penggunaan (mungkin mungkin tetapi bahkan lebih aneh untuk melakukan hal yang sama dengan jumlah penggunaan).

DRs yang relevan yang saya temukan: 545 , 575 , 2434 (yang mengakui bahwa semua implementasi menggunakan blok kontrol dan tampaknya menyiratkan bahwa kendala multi-threading agak mengamanatkannya), 2802 (yang mengharuskan deleter hanya bergerak konstruktif dan dengan demikian mencegah implementasi di mana deleter disalin di antara beberapa file shared_ptr).

Pemrogram
sumber
2
"untuk mengalokasikan memori untuk penggunaan internal" Bagaimana jika implementasi tidak akan mengalokasikan memori untuk penggunaan internal untuk memulai? Itu bisa menggunakan anggota.
LF
1
@ LF Tidak bisa, antarmuka tidak memungkinkan untuk itu.
Pemrogram
Secara teoritis, masih bisa menggunakan semacam "optimasi deleter kecil", kan?
LF
Yang aneh adalah bahwa saya tidak dapat menemukan apa pun tentang menggunakan pengalokasi yang sama (salinan a) untuk membatalkan alokasi memori itu. Yang menyiratkan beberapa penyimpanan salinan itu a. Tidak ada informasi tentang itu di [util.smartptr.shared.dest].
Daniel Langr
1
@DanielsaysreinstateMonica, saya bertanya-tanya apakah di util.smartptr.shared / 1: "Templat class shared_ptr menyimpan sebuah pointer, biasanya diperoleh melalui yang baru. Shared_ptr mengimplementasikan semantik kepemilikan bersama; pemilik pointer yang tersisa bertanggung jawab untuk menghancurkan objek, atau melepaskan sumber daya yang terkait dengan pointer yang disimpan. " yang melepaskan sumber daya terkait dengan pointer yang disimpan tidak dimaksudkan untuk itu. Tetapi blok kontrol harus bertahan juga sampai pointer lemah terakhir dihapus.
Pemrogram
4

Dari std :: shared_ptr kami memiliki:

Blok kontrol adalah objek yang dialokasikan secara dinamis yang menampung:

  • baik pointer ke objek yang dikelola atau objek yang dikelola itu sendiri;
  • deleter (tipe-terhapus);
  • pengalokasi (jenis dihapus);
  • jumlah shared_ptrs yang memiliki objek yang dikelola;
  • jumlah lemah_ptr yang merujuk ke objek yang dikelola.

Dan dari std :: mengalokasikan_bagikan, kami mendapatkan:

template< class T, class Alloc, class... Args >
shared_ptr<T> allocate_shared( const Alloc& alloc, Args&&... args );

Buat objek bertipe T dan bungkus dengan std :: shared_ptr [...] untuk menggunakan satu alokasi untuk blok kontrol dari pointer bersama dan objek T.

Jadi sepertinya std :: mengalokasikan_bagikan harus mengalokasikan deleterdengan Anda Alloc.

EDIT: Dan dari n4810§20.11.3.6 Pembuatan [util.smartptr. Shared.create]

1 Persyaratan umum yang berlaku untuk semua make_shared, allocate_shared, make_shared_default_init, dan allocate_shared_default_initoverloads, kecuali ditentukan lain, dijelaskan di bawah ini.

[...]

7 Keterangan: (7.1) - Implementasi harus melakukan tidak lebih dari satu alokasi memori. [Catatan: Ini memberikan efisiensi yang setara dengan pointer cerdas intrusif. —Kirim catatan]

[Tekankan semua milikku]

Jadi standar mengatakan yang std::allocate_shared harus digunakan Allocuntuk blok kontrol.

Paul Evans
sumber
1
Maaf dengan cppreference bukan teks normatif. Ini sumber yang bagus, tetapi tidak harus untuk pertanyaan pengacara bahasa .
StoryTeller - Unslander Monica
@ StoryTeller-UnslanderMonica Sepenuhnya setuju - melihat melalui standar terbaru dan tidak dapat menemukan apa pun jadi lanjutkan dengan cppreference.
Paul Evans
Ditemukan n4810dan diperbarui jawabannya.
Paul Evans
1
Namun, ini berbicara tentang make_shared, bukan konstruktor itu sendiri. Tetap saja, saya bisa menggunakan anggota untuk deleter kecil.
LF
3

Saya percaya ini tidak ditentukan.

Berikut spesifikasi konstruktor yang relevan: [util.smartptr.shared.const] / 10

template<class Y, class D> shared_ptr(Y* p, D d);
template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template <class D> shared_ptr(nullptr_t p, D d);
template <class D, class A> shared_ptr(nullptr_t p, D d, A a);

Efek: Membangun shared_­ptrobjek yang memiliki objek pdan deleter d. Ketika Tbukan tipe array, konstruktor pertama dan kedua diaktifkan shared_­from_­thisdengan p. Konstruktor kedua dan keempat harus menggunakan salinan auntuk mengalokasikan memori untuk penggunaan internal . Jika pengecualian dilemparkan, d(p)disebut.

Sekarang, interpretasi saya adalah bahwa ketika implementasi membutuhkan memori untuk penggunaan internal, ia melakukannya dengan menggunakan a. Itu tidak berarti bahwa implementasi harus menggunakan memori ini untuk meletakkan semuanya. Misalnya, anggap ada implementasi aneh ini:

template <typename T>
class shared_ptr : /* ... */ {
    // ...
    std::aligned_storage<16> _Small_deleter;
    // ...
public:
    // ...
    template <class _D, class _A>
    shared_ptr(nullptr_t, _D __d, _A __a) // for example
        : _Allocator_base{__a}
    {
        if constexpr (sizeof(_D) <= 16)
            _Construct_at(&_Small_deleter, std::move(__d));
        else
            // use 'a' to allocate storage for the deleter
    }
// ...
};

Apakah implementasi ini "menggunakan salinan auntuk mengalokasikan memori untuk penggunaan internal"? Ya, benar. Tidak pernah mengalokasikan memori kecuali dengan menggunakan a. Ada banyak masalah dengan implementasi naif ini, tetapi katakanlah ia beralih ke menggunakan pengalokasi dalam semua tetapi kasus paling sederhana di mana shared_ptrdibangun langsung dari pointer dan tidak pernah disalin atau dipindahkan atau direferensikan dan tidak ada komplikasi lainnya. Intinya adalah, hanya karena kita gagal membayangkan implementasi yang valid tidak dengan sendirinya membuktikan bahwa itu tidak dapat secara teoritis ada. Saya tidak mengatakan bahwa implementasi seperti itu sebenarnya dapat ditemukan di dunia nyata, hanya saja standar itu tampaknya tidak secara aktif melarangnya.

LF
sumber
IMO Anda shared_ptruntuk tipe kecil mengalokasikan memori pada stack. Dan tidak memenuhi persyaratan standar
bartop
1
@ Bartop Ini tidak "mengalokasikan" memori pada stack. _Smaller_deleter adalah tanpa syarat bagian dari representasi shared_ptr. Memanggil konstruktor di ruang ini tidak berarti mengalokasikan apa pun. Jika tidak, bahkan memegang pointer ke blok kontrol dianggap sebagai "mengalokasikan memori", kan? :-)
LF
Tetapi deleter tidak diharuskan untuk disalin, jadi bagaimana cara kerjanya?
Nicol Bolas
@NicolBolas Umm ... Gunakan std::move(__d), dan kembali ke allocatesaat salinan diperlukan.
LF