Bagaimana cara menggunakan deleter khusus dengan anggota std :: unique_ptr?

133

Saya memiliki kelas dengan anggota unique_ptr.

class Foo {
private:
    std::unique_ptr<Bar> bar;
    ...
};

Bilah adalah kelas pihak ketiga yang memiliki fungsi create () dan fungsi destroy ().

Jika saya ingin menggunakan std::unique_ptrdengan itu dalam fungsi yang berdiri sendiri yang bisa saya lakukan:

void foo() {
    std::unique_ptr<Bar, void(*)(Bar*)> bar(create(), [](Bar* b){ destroy(b); });
    ...
}

Apakah ada cara untuk melakukan ini dengan std::unique_ptrsebagai anggota kelas?

huitlarc
sumber

Jawaban:

133

Dengan asumsi itu createdan destroymerupakan fungsi bebas (yang tampaknya merupakan kasus dari cuplikan kode OP) dengan tanda tangan berikut:

Bar* create();
void destroy(Bar*);

Anda dapat menulis kelas Fooseperti ini

class Foo {

    std::unique_ptr<Bar, void(*)(Bar*)> ptr_;

    // ...

public:

    Foo() : ptr_(create(), destroy) { /* ... */ }

    // ...
};

Perhatikan bahwa Anda tidak perlu menulis lambda atau custom deleter di sini karena destroysudah menjadi deleter.

Cassio Neri
sumber
156
Dengan C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe
1
Kelemahan dari solusi ini adalah menggandakan overhead setiap unique_ptr(mereka semua harus menyimpan pointer fungsi bersama dengan pointer ke data aktual), membutuhkan melewati fungsi penghancuran setiap kali, itu tidak dapat inline (karena template tidak bisa mengkhususkan diri pada fungsi spesifik, hanya tanda tangan), dan harus memanggil fungsi melalui pointer (lebih mahal daripada panggilan langsung). Kedua jawaban rici dan Deduplicator menghindari semua biaya ini dengan mengkhususkan diri pada functor.
ShadowRanger
@ShadowRanger bukankah itu didefinisikan ke default_delete <T> dan fungsi pointer disimpan setiap kali Anda lulus secara eksplisit atau tidak?
Herrgott
117

Dimungkinkan untuk melakukan ini dengan bersih menggunakan lambda di C ++ 11 (diuji dalam G ++ 4.8.2).

Mengingat ini dapat digunakan kembali typedef:

template<typename T>
using deleted_unique_ptr = std::unique_ptr<T,std::function<void(T*)>>;

Kamu bisa menulis:

deleted_unique_ptr<Foo> foo(new Foo(), [](Foo* f) { customdeleter(f); });

Misalnya, dengan FILE*:

deleted_unique_ptr<FILE> file(
    fopen("file.txt", "r"),
    [](FILE* f) { fclose(f); });

Dengan ini Anda mendapatkan manfaat pembersihan dengan pengecualian-aman menggunakan RAII, tanpa perlu mencoba / menangkap kebisingan.

Drew Noakes
sumber
2
Ini harus menjadi jawabannya, imo. Ini solusi yang lebih indah. Atau apakah ada kerugian, seperti misalnya memiliki std::functiondalam definisi atau sejenisnya?
j00hi
17
@ j00hi, menurut saya solusi ini memiliki overhead yang tidak perlu karena std::function. Lambda atau kelas khusus seperti dalam jawaban yang diterima dapat diuraikan tidak seperti solusi ini. Tetapi pendekatan ini memiliki keuntungan jika Anda ingin mengisolasi semua implementasi dalam modul khusus.
magras
5
Ini akan membocorkan memori jika std :: function constructor throws (yang mungkin terjadi jika lambda terlalu besar untuk masuk ke dalam std :: function object)
StaceyGirl
4
Apakah lambda benar-benar dibutuhkan di sini? Ini bisa sederhana deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);jika customdeletermengikuti konvensi (mengembalikan konvensi dan menerima pointer mentah sebagai argumen).
Victor Polevoy
Ada satu kelemahan dari pendekatan ini. std :: function tidak diperlukan untuk menggunakan move constructor jika memungkinkan. Ini berarti bahwa ketika Anda std :: move (my_deleted_unique_ptr), konten yang dilampirkan oleh lambda mungkin akan disalin alih-alih dipindahkan, yang mungkin atau mungkin bukan yang Anda inginkan.
GeniusIsme
70

Anda hanya perlu membuat kelas deleter:

struct BarDeleter {
  void operator()(Bar* b) { destroy(b); }
};

dan berikan sebagai argumen templat dari unique_ptr. Anda masih harus menginisialisasi unique_ptr di konstruktor Anda:

class Foo {
  public:
    Foo() : bar(create()), ... { ... }

  private:
    std::unique_ptr<Bar, BarDeleter> bar;
    ...
};

Sejauh yang saya tahu, semua pustaka c ++ populer mengimplementasikan ini dengan benar; Karena BarDeleter tidak benar - benar memiliki keadaan apa pun, tidak perlu menempati ruang apa pun di Internet unique_ptr.

rici
sumber
8
opsi ini adalah satu-satunya yang bekerja dengan array, std :: vector dan koleksi lainnya karena dapat menggunakan parameter nol std :: unique_ptr constructor. jawaban lain menggunakan solusi yang tidak memiliki akses ke konstruktor parameter nol ini karena instance Deleter harus disediakan saat membuat pointer unik. Tetapi solusi ini menyediakan kelas Deleter ( struct BarDeleter) ke std::unique_ptr( std::unique_ptr<Bar, BarDeleter>) yang memungkinkan std::unique_ptrkonstruktor membuat instance Deleter sendiri. yaitu kode berikut diizinkanstd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF
12
Saya akan membuat typedef agar mudah digunakantypedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
DavidF
@ DavidvidF: Atau gunakan pendekatan Deduplicator , yang memiliki kelebihan yang sama (menghapus penghapusan, tidak ada penyimpanan tambahan pada masing-masing unique_ptr, tidak perlu memberikan instance dari deleter saat membangun), dan menambahkan manfaat dapat digunakan di std::unique_ptr<Bar>mana saja tanpa perlu mengingat untuk menggunakan typedefpenyedia khusus atau secara eksplisit parameter templat kedua. (Untuk lebih jelasnya, ini adalah solusi yang baik, saya memberikan suara, tetapi itu menghentikan satu langkah di belakang dari solusi yang mulus)
ShadowRanger
22

Kecuali Anda harus dapat mengubah deleter saat runtime, saya akan sangat menyarankan menggunakan tipe deleter khusus. Misalnya, jika menggunakan pointer fungsi untuk deleter Anda sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*),. Dengan kata lain, setengah dari byte unique_ptrobjek terbuang sia-sia.

Menulis custom deleter untuk membungkus setiap fungsi memang merepotkan. Untungnya, kita dapat menulis sebuah tipe yang ditempel pada fungsi:

Sejak C ++ 17:

template <auto fn>
using deleter_from_fn = std::integral_constant<decltype(fn), fn>;

template <typename T, auto fn>
using my_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;

// usage:
my_unique_ptr<Bar, destroy> p{create()};

Sebelum ke C ++ 17:

template <typename D, D fn>
using deleter_from_fn = std::integral_constant<D, fn>;

template <typename T, typename D, D fn>
using my_unique_ptr = std::unique_ptr<T, deleter_from_fn<D, fn>>;

// usage:
my_unique_ptr<Bar, decltype(destroy), destroy> p{create()};
Justin
sumber
Bagus. Apakah saya benar bahwa ini mencapai manfaat yang sama (overhead memori yang dibelah dua, memanggil fungsi secara langsung daripada melalui penunjuk fungsi, fungsi inlining potensial memanggil sepenuhnya) sebagai functor dari jawaban Rici , hanya dengan lebih sedikit boilerplate?
ShadowRanger
Ya, ini harus memberikan semua manfaat dari kelas deleter khusus, karena deleter_from_fnmemang begitu.
rmcclellan
6

Anda cukup menggunakan std::binddengan fungsi penghancuran Anda.

std::unique_ptr<Bar, std::function<void(Bar*)>> bar(create(), std::bind(&destroy,
    std::placeholders::_1));

Tapi tentu saja Anda juga bisa menggunakan lambda.

std::unique_ptr<Bar, std::function<void(Bar*)>> ptr(create(), [](Bar* b){ destroy(b);});
mkaes
sumber
6

Anda tahu, menggunakan custom deleter bukanlah cara terbaik, karena Anda harus menyebutkannya di seluruh kode Anda.
Alih-alih, karena Anda diizinkan untuk menambahkan spesialisasi ke kelas tingkat namespace ::stdselama jenis kustom terlibat dan Anda menghormati semantik, lakukan itu:

Spesialisasi std::default_delete:

template <>
struct ::std::default_delete<Bar> {
    default_delete() = default;
    template <class U, class = std::enable_if_t<std::is_convertible<U*, Bar*>()>>
    constexpr default_delete(default_delete<U>) noexcept {}
    void operator()(Bar* p) const noexcept { destroy(p); }
};

Dan mungkin juga melakukan std::make_unique():

template <>
inline ::std::unique_ptr<Bar> ::std::make_unique<Bar>() {
    auto p = create();
    if (!p) throw std::runtime_error("Could not `create()` a new `Bar`.");
    return { p };
}
Deduplicator
sumber
2
Saya akan sangat berhati-hati dengan ini. Membuka stdmembuka kaleng cacing yang sama sekali baru. Perhatikan juga bahwa spesialisasi dari std::make_uniquetidak diperbolehkan posting C ++ 20 (sehingga tidak boleh dilakukan sebelumnya) karena C ++ 20 melarang spesialisasi hal-hal stdyang bukan templat kelas ( std::make_uniqueadalah templat fungsi). Perhatikan bahwa Anda mungkin juga akan berakhir dengan UB jika pointer yang masuk std::unique_ptr<Bar>tidak dialokasikan dari create(), tetapi dari beberapa fungsi alokasi lainnya.
Justin
Saya tidak yakin ini diizinkan. Sepertinya saya sulit membuktikan bahwa spesialisasi ini std::default_deletememenuhi persyaratan templat asli. Saya akan membayangkan itu std::default_delete<Foo>()(p)akan menjadi cara yang valid untuk menulis delete p;, jadi jika delete p;akan valid untuk menulis (yaitu jika Foosudah lengkap), ini tidak akan menjadi perilaku yang sama. Selain itu, jika delete p;tidak valid untuk menulis ( Footidak lengkap), ini akan menentukan perilaku baru untuk std::default_delete<Foo>, daripada menjaga perilaku yang sama.
Justin
The make_uniquespesialisasi bermasalah, tapi aku pasti menggunakan std::default_deletekelebihan (tidak templated dengan enable_if, hanya untuk C struct seperti OpenSSL BIGNUMyang menggunakan fungsi kerusakan diketahui, di mana subclassing tidak akan terjadi), dan itu jauh pendekatan yang paling mudah, karena sisa kode Anda hanya dapat digunakan unique_ptr<special_type>tanpa harus melewati tipe functor sebagai templated Deleterall over, atau menggunakan typedef/ usinguntuk memberi nama pada tipe kata untuk menghindari masalah itu.
ShadowRanger