Bir std :: unique_ptr üyesiyle özel bir siliciyi nasıl kullanabilirim?


133

Unique_ptr üyesi olan bir sınıfım var.

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

Bar, create () işlevi ve destroy () işlevi olan üçüncü taraf bir sınıftır.

Onunla std::unique_ptrbağımsız bir işlevde kullanmak isteseydim şunları yapabilirdim:

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

Bunu std::unique_ptrbir sınıfın üyesi olarak yapmanın bir yolu var mı ?

Yanıtlar:


133

Varsayarsak createve destroyaşağıdaki imzalarla (OP kod parçacığı vaka gibi görünüyor olan) serbest işlevleri şunlardır:

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

Sınıfını Fooböyle yazabilirsin

class Foo {

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

    // ...

public:

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

    // ...
};

Burada herhangi bir lambda veya özel silici yazmanıza gerek olmadığına dikkat edin, çünkü destroyzaten bir silme aracıdır.


157
C ++ 11 ilestd::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe

1
Bu çözümün dezavantajı, her birinin ek yükünü iki katına çıkarmasıdır unique_ptr(hepsi işlev işaretçisini gerçek veriye gösteren işaretçi ile birlikte depolamalıdır), her seferinde imha işlevini geçmeyi gerektirir, çünkü şablon satır içi olamaz belirli bir işlevde uzmanlaşır, yalnızca imza) ve işlevi işaretçi aracılığıyla çağırmalıdır (doğrudan aramadan daha maliyetli). Hem Rici hem de Deduplicator'ın yanıtları, bir functor konusunda uzmanlaşarak tüm bu maliyetleri ortadan kaldırır.
ShadowRanger

@ShadowRanger, açık bir şekilde geçseniz de geçmeseniz de her defasında default_delete <T> ve depolanan işlev işaretçisini tanımlamıyor mu?
Herrgott

117

Bunu C ++ 11'de bir lambda kullanarak temiz bir şekilde yapmak mümkündür (G ++ 4.8.2'de test edilmiştir).

Bu yeniden kullanılabilirlik göz önüne alındığında typedef:

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

Yazabilirsin:

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

Örneğin, bir FILE*:

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

Bununla, deneme / yakalama gürültüsüne gerek kalmadan RAII kullanarak olağanüstü güvenli temizlemenin avantajlarını elde edersiniz.


2
Cevap bu olmalı imo. Daha güzel bir çözüm. Veya herhangi bir dezavantaj var mı, örneğin std::function, tanımda ya da benzeri olması gibi?
j00hi

17
@ j00hi, bence bu çözümün gereksiz ek yükü var std::function. Lambda veya kabul edilen cevapta olduğu gibi özel sınıf, bu çözümden farklı olarak satır içine alınabilir. Ancak, tüm uygulamayı özel modülde izole etmek istediğinizde bu yaklaşımın avantajı vardır.
magras

5
Std :: function yapıcısı atarsa ​​bu bellek sızdırır (bu, lambda std :: function nesnesine sığmayacak kadar büyükse olabilir)
StaceyGirl

4
Lambda gerçekten burada gerekli mi? Kuralı izlerse basit deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);olabilir customdeleter(void döndürür ve ham göstericiyi bir argüman olarak kabul eder).
Victor Polevoy

Bu yaklaşımın bir dezavantajı var. std :: function, mümkün olduğunda move yapıcısını kullanmak için gerekli değildir. Bu, std :: move (my_deleted_unique_ptr) yaptığınızda, lambda tarafından kapsanan içeriklerin taşınmak yerine kopyalanacağı anlamına gelir, bu da istediğiniz gibi olabilir veya olmayabilir.
GeniusIsme

71

Yalnızca bir silme sınıfı oluşturmanız gerekir:

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

ve bunu şablon argümanı olarak sağlayın unique_ptr. Yine de kurucularınızda unique_ptr'yi başlatmanız gerekecek:

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

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

Bildiğim kadarıyla, tüm popüler c ++ kitaplıkları bunu doğru bir şekilde uyguluyor; çünkü BarDeleter aslında hiçbir devlet yoktur, bu herhangi bir alanı işgal etmek gerekmez unique_ptr.


8
Bu seçenek diziler, std :: vektör ve diğer koleksiyonlarla çalışan tek seçenektir çünkü sıfır parametresi std :: unique_ptr yapıcısı kullanabilir. diğer yanıtlar, benzersiz bir işaretçi oluştururken bir Deleter örneğinin sağlanması gerektiğinden, bu sıfır parametre yapıcısına erişimi olmayan çözümleri kullanır. Ancak bu çözüm, Deleter sınıfını ( struct BarDeleter) - std::unique_ptr( std::unique_ptr<Bar, BarDeleter>) sağlar.std::unique_ptr kendi başına bir Deleter örneği oluşturmasına . yani aşağıdaki koda izin verilirstd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF

13
Kolay kullanım için bir typedef oluşturardımtypedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
DavidF

@DavidF: Veya aynı avantajlara sahip olan (satır içi silme, her birinde fazladan depolama yok, oluştururken silicinin bir örneğini sağlamaya gerek yok) ve hatırlamaya gerek kalmadan her yerde kullanabilme avantajını ekleyen Deduplicator'ın yaklaşımınıunique_ptr kullanın std::unique_ptr<Bar>özel typedefveya açıkça sağlayıcıyı ikinci şablon parametresini kullanmak için. (Açıkçası, bu iyi bir çözüm, oy verdim, ancak sorunsuz bir çözümün bir adımını durduruyor)
ShadowRanger

22

Siliciyi çalışma zamanında değiştirebilmeniz gerekmedikçe, özel bir silici türü kullanmanızı şiddetle tavsiye ederim. Örneğin, siliciniz için bir işlev işaretçisi kullanıyorsanız sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*),. Başka bir deyişle, baytların yarısıunique_ptr nesnenin boşa harcanır.

Yine de, her işlevi sarmak için özel bir silici yazmak zahmetlidir. Neyse ki, fonksiyon üzerine şablonlu bir tip yazabiliriz:

C ++ 17'den beri:

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()};

C ++ 17'den önce:

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()};

Şık. Bunun, Rici'nin cevabından gelen functor olarak , sadece daha az şablonla , aynı faydaları (yarıya indirilmiş bellek ek yükü, işlev işaretçisi yerine doğrudan işlevi çağırma, potansiyel satır içi işlevi tamamen uzaklaştırma) sağladığında haklı mıyım ?
ShadowRanger

Evet, bu, özel bir silme sınıfının tüm faydalarını sağlamalıdır, çünkü olan deleter_from_fnbudur.
rmcclellan

7

Biliyorsunuz, kodunuzun her yerinde bundan bahsetmeniz gerekeceğinden, özel bir silici kullanmak en iyi yol değildir.
Bunun yerine, ad alanı düzeyindeki sınıflara uzmanlıklar eklemenize izin verildiğinden::std özel türler dahil olduğu ve semantiğe saygı duyduğunuz sürece , şunu yapın:

Uzmanlaşın 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); }
};

Ve belki şunu da yapın 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 };
}

2
Bu konuda çok dikkatli olurdum. Açılmak, stdyepyeni bir solucan kutusu açar. Ayrıca std::make_unique, C ++ 20 sonrasında uzmanlaşmasına izin verilmediğini unutmayın (bu nedenle daha önce yapılmamalıdır) çünkü C ++ 20, içinde stdsınıf şablonu olmayan şeylerin uzmanlaşmasına izin vermez ( std::make_uniquebir işlev şablonu). Ayrıca, geçirilen işaretçi başka bir ayırma işlevinden std::unique_ptr<Bar>ayrılmamışsa muhtemelen UB ile sonuçlanacağınızı unutmayın create().
Justin

Buna izin verildiğine ikna olmadım. Bana öyle geliyor ki, bu uzmanlığın std::default_deleteorijinal şablonun gereksinimlerini karşıladığını kanıtlamak zor . Bunun std::default_delete<Foo>()(p)yazmanın geçerli bir yolu olacağını düşünürdüm delete p;, bu yüzden yazmak delete p;için geçerli olsaydı (yani Footamamlandıysa), bu aynı davranış olmazdı. Ayrıca, delete p;yazmak için geçersizse ( Fooeksikse), bu std::default_delete<Foo>davranışı aynı tutmaktan ziyade için yeni davranış belirtiyor olacaktır .
Justin

make_uniqueUzmanlaşma problemlidir, ama kesinlikle kullandım std::default_deleteaşırı (ile şablon değil enable_ifsadece OpenSSL en gibi C yapılar için, BIGNUMsınıflara olmayacak bir bilinen imha işlevini kullanın) ve bu gibi en kolay yaklaşımla bulunuyor kodunuzun geri kalanı unique_ptr<special_type>, functor türünü şablon olarak Deleterher yere iletmeye gerek kalmadan kullanabilir veya bu sorunu önlemek için söz konusu türe bir ad vermek için typedef/ öğesini kullanabilir using.
ShadowRanger

6

Sadece std::bindbir yok etme fonksiyonunuzla kullanabilirsiniz.

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

Ama tabii ki bir lambda da kullanabilirsiniz.

std::unique_ptr<Bar, std::function<void(Bar*)>> ptr(create(), [](Bar* b){ destroy(b);});
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.