ฉันจะใช้ deleter ที่กำหนดเองกับสมาชิก std :: unique_ptr ได้อย่างไร


133

ฉันมีชั้นเรียนที่มีสมาชิก unique_ptr

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

Bar เป็นคลาสของบุคคลที่สามที่มีฟังก์ชัน create () และฟังก์ชัน destroy ()

หากฉันต้องการใช้ a std::unique_ptrร่วมกับมันในฟังก์ชันสแตนด์อโลนฉันสามารถทำได้:

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

มีวิธีดำเนินการในstd::unique_ptrฐานะสมาชิกของชั้นเรียนหรือไม่?

คำตอบ:


133

สมมติว่าcreateและdestroyเป็นฟังก์ชันฟรี (ซึ่งดูเหมือนจะเป็นเช่นนั้นจากข้อมูลโค้ดของ OP) พร้อมลายเซ็นต่อไปนี้:

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

คุณสามารถเขียนชั้นเรียนของคุณFooแบบนี้

class Foo {

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

    // ...

public:

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

    // ...
};

สังเกตว่าคุณไม่จำเป็นต้องเขียน lambda หรือ custom deleter ใด ๆ ที่นี่เพราะdestroyเป็น deleter อยู่แล้ว


157
ด้วย C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
โจ

1
ข้อเสียของโซลูชันนี้คือการเพิ่มค่าใช้จ่ายเป็นสองเท่าของทุกๆunique_ptr(พวกเขาทั้งหมดต้องจัดเก็บตัวชี้ฟังก์ชันพร้อมกับตัวชี้ไปยังข้อมูลจริง) ต้องผ่านฟังก์ชันการทำลายทุกครั้งจึงไม่สามารถอินไลน์ได้ (เนื่องจากเทมเพลตไม่สามารถ เชี่ยวชาญในฟังก์ชันเฉพาะลายเซ็นเท่านั้น) และต้องเรียกใช้ฟังก์ชันผ่านตัวชี้ (มีราคาแพงกว่าการโทรโดยตรง) ทั้งคำตอบของriciและDeduplicatorหลีกเลี่ยงค่าใช้จ่ายเหล่านี้ทั้งหมดโดยเชี่ยวชาญเรื่อง functor
ShadowRanger

@ShadowRanger ไม่ได้กำหนดเป็น default_delete <T> และตัวชี้ฟังก์ชันที่จัดเก็บไว้ทุกครั้งไม่ว่าคุณจะส่งผ่านอย่างชัดเจนหรือไม่?
Herrgott

117

สามารถทำได้อย่างหมดจดโดยใช้แลมด้าใน C ++ 11 (ทดสอบใน G ++ 4.8.2)

ให้สิ่งนี้ใช้ซ้ำได้typedef:

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

คุณสามารถเขียน:

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

ตัวอย่างเช่นด้วยFILE*:

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

ด้วยวิธีนี้คุณจะได้รับประโยชน์ของการล้างข้อมูลที่ปลอดภัยโดยใช้ RAII โดยไม่ต้องลอง / จับเสียง


2
นี่น่าจะเป็นคำตอบ imo มันเป็นทางออกที่สวยงามกว่า หรือมีข้อเสียเช่นมีstd::functionในนิยามหรืออย่างนั้นหรือไม่?
j00hi

17
@ j00hi ในความคิดของฉันโซลูชันนี้มีค่าใช้จ่ายที่ไม่จำเป็นเนื่องจากstd::function. แลมบ์ดาหรือคลาสที่กำหนดเองในคำตอบที่ยอมรับสามารถอินไลน์ได้ไม่เหมือนกับโซลูชันนี้ แต่วิธีนี้มีประโยชน์ในกรณีที่คุณต้องการแยกการใช้งานทั้งหมดในโมดูลเฉพาะ
magras

5
สิ่งนี้จะรั่วไหลของหน่วยความจำถ้า std :: function constructor พ่น (ซึ่งอาจเกิดขึ้นถ้า lambda ใหญ่เกินไปที่จะใส่เข้าไปใน std :: function object)
StaceyGirl

4
แลมด้าต้องการที่นี่จริงหรือ? อาจเป็นเรื่องง่ายdeleted_unique_ptr<Foo> foo(new Foo(), customdeleter);ถ้าเป็นcustomdeleterไปตามแบบแผน (ส่งคืนโมฆะและยอมรับตัวชี้ดิบเป็นอาร์กิวเมนต์)
Victor Polevoy

แนวทางนี้มีข้อเสียอย่างหนึ่ง std :: ไม่จำเป็นต้องใช้ฟังก์ชัน move constructor ทุกครั้งที่ทำได้ ซึ่งหมายความว่าเมื่อคุณ std :: move (my_deleted_unique_ptr) เนื้อหาที่ล้อมรอบด้วยแลมบ์ดาอาจถูกคัดลอกแทนการย้ายซึ่งอาจใช่หรือไม่ใช่สิ่งที่คุณต้องการ
GeniusIsme

71

คุณเพียงแค่ต้องสร้างคลาส deleter:

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

unique_ptrและให้มันเป็นอาร์กิวเมนต์แม่แบบของ คุณยังคงต้องเริ่มต้น unique_ptr ในตัวสร้างของคุณ:

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

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

เท่าที่ฉันรู้ไลบรารี c ++ ยอดนิยมทั้งหมดใช้สิ่งนี้อย่างถูกต้อง เนื่องจากBarDeleter ไม่มีสถานะใด ๆ จริงจึงไม่จำเป็นต้องใช้พื้นที่ใด ๆ ในไฟล์unique_ptr.


8
ตัวเลือกนี้เป็นตัวเลือกเดียวที่ใช้ได้กับอาร์เรย์ std :: vector และคอลเล็กชันอื่น ๆ เนื่องจากสามารถใช้ตัวสร้างพารามิเตอร์ที่เป็นศูนย์ std :: unique_ptr คำตอบอื่น ๆ ใช้โซลูชันที่ไม่มีสิทธิ์เข้าถึงตัวสร้างพารามิเตอร์ศูนย์นี้เนื่องจากต้องระบุอินสแตนซ์ Deleter เมื่อสร้างตัวชี้ที่ไม่ซ้ำกัน แต่โซลูชันนี้จัดเตรียมคลาส Deleter ( struct BarDeleter) ถึงstd::unique_ptr( std::unique_ptr<Bar, BarDeleter>) ซึ่งอนุญาตให้ตัวstd::unique_ptrสร้างสร้างอินสแตนซ์ Deleter ด้วยตัวเอง กล่าวคืออนุญาตให้ใช้รหัสต่อไปนี้std::unique_ptr<Bar, BarDeleter> bar[10];
DavidF

13
ฉันจะสร้าง typedef เพื่อให้ใช้งานง่ายtypedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
DavidF

@DavidF: หรือใช้วิธีการของ Deduplicatorซึ่งมีข้อดีเหมือนกัน (การลบแบบอินไลน์ไม่มีพื้นที่จัดเก็บเพิ่มเติมในแต่ละunique_ptrอันไม่จำเป็นต้องให้อินสแตนซ์ของ deleter เมื่อสร้าง) และเพิ่มประโยชน์ในการใช้งานstd::unique_ptr<Bar>ได้ทุกที่โดยไม่จำเป็นต้องจำ เพื่อใช้typedefพารามิเตอร์เทมเพลตที่สองหรือผู้ให้บริการพิเศษอย่างชัดเจน (เพื่อให้ชัดเจนว่านี่เป็นทางออกที่ดีฉันโหวตให้ แต่มันก็หยุดหนึ่งขั้นตอนอายของโซลูชันที่ไร้รอยต่อ)
ShadowRanger

22

ถ้าคุณไม่จำเป็นต้องสามารถเปลี่ยน deleter ในรันไทม์ได้ฉันขอแนะนำอย่างยิ่งให้ใช้ประเภท deleter ที่กำหนดเอง ตัวอย่างเช่นถ้าใช้ตัวชี้ฟังก์ชันสำหรับ Deleter sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*)ของคุณ กล่าวอีกนัยหนึ่งครึ่งหนึ่งของไบต์ของunique_ptrวัตถุจะเสียไป

การเขียน deleter ที่กำหนดเองเพื่อตัดทุกฟังก์ชันเป็นเรื่องที่น่ารำคาญ โชคดีที่เราสามารถเขียนประเภทแม่แบบในฟังก์ชัน:

ตั้งแต่ 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()};

ก่อน 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()};

ดี ฉันถูกต้องหรือไม่ว่าสิ่งนี้ได้รับประโยชน์เช่นเดียวกัน (ค่าใช้จ่ายของหน่วยความจำลดลงครึ่งหนึ่งฟังก์ชันการโทรโดยตรงแทนที่จะใช้ตัวชี้ฟังก์ชันฟังก์ชันอินไลน์ที่มีศักยภาพจะเรียกออกไปโดยสิ้นเชิง) ในฐานะ functor จากคำตอบของ riciเพียงแค่มีแผ่นสำเร็จรูปน้อยกว่า
ShadowRanger

ใช่สิ่งนี้ควรให้ประโยชน์ทั้งหมดของคลาส deleter ที่กำหนดเองเนื่องจากนั่นคือสิ่งที่deleter_from_fnเป็นอยู่
rmcclellan

7

คุณรู้ไหมว่าการใช้ deleter แบบกำหนดเองไม่ใช่วิธีที่ดีที่สุดเนื่องจากคุณจะต้องพูดถึงโค้ดทั้งหมด
แต่ในขณะที่คุณได้รับอนุญาตให้เพิ่มความเชี่ยวชาญในการเรียน namespace ในระดับ::stdตราบเท่าที่ประเภทที่กำหนดเองที่มีส่วนเกี่ยวข้องและคุณเคารพความหมายที่จะทำอย่างนั้น:

เชี่ยวชาญ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); }
};

และอาจทำ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
ฉันจะระวังเรื่องนี้ให้มาก การเปิดตัวstdหนอนกระป๋องใหม่ทั้งหมด นอกจากนี้โปรดทราบว่าความเชี่ยวชาญของstd::make_uniqueไม่ได้รับอนุญาตให้โพสต์ C ++ 20 (ไม่ควรทำมาก่อน) เนื่องจาก C ++ 20 ไม่อนุญาตให้ใช้ความเชี่ยวชาญของสิ่งstdที่ไม่ใช่เทมเพลตคลาส ( std::make_uniqueเป็นเทมเพลตฟังก์ชัน) โปรดทราบว่าคุณอาจลงเอยด้วย UB หากตัวชี้ที่ส่งผ่านเข้ามาstd::unique_ptr<Bar>ไม่ได้รับการจัดสรรจากcreate()แต่มาจากฟังก์ชันการจัดสรรอื่น ๆ
จัสติน

ฉันไม่มั่นใจว่าสิ่งนี้ได้รับอนุญาต สำหรับฉันแล้วดูเหมือนว่าเป็นการยากที่จะพิสูจน์ว่าความเชี่ยวชาญนี้std::default_deleteตรงตามความต้องการของเทมเพลตดั้งเดิม ฉันคิดว่านั่นstd::default_delete<Foo>()(p)เป็นวิธีที่ถูกต้องในการเขียนdelete p;ดังนั้นหากdelete p;จะเขียนได้ถูกต้อง (เช่นถ้าFooสมบูรณ์) สิ่งนี้จะไม่เป็นพฤติกรรมเดียวกัน นอกจากนี้หากdelete p;เขียนไม่ถูกต้อง ( Fooไม่สมบูรณ์) สิ่งนี้จะเป็นการระบุพฤติกรรมใหม่std::default_delete<Foo>แทนที่จะทำให้พฤติกรรมเหมือนเดิม
จัสติน

ความmake_uniqueเชี่ยวชาญเป็นปัญหา แต่ฉันได้ใช้การstd::default_deleteโอเวอร์โหลดอย่างแน่นอน(ไม่ใช่เทมเพลตenable_ifสำหรับโครงสร้าง C เช่น OpenSSL BIGNUMที่ใช้ฟังก์ชันการทำลายที่รู้จักโดยที่คลาสย่อยจะไม่เกิดขึ้น) และเป็นวิธีที่ง่ายที่สุดเช่น ส่วนที่เหลือของโค้ดของคุณสามารถใช้ได้unique_ptr<special_type>โดยไม่จำเป็นต้องส่งผ่านประเภท functor เป็นเทมเพลตDeleterทั้งหมดหรือใช้typedef/ usingตั้งชื่อให้กับประเภทที่กล่าวเพื่อหลีกเลี่ยงปัญหานั้น
ShadowRanger

6

คุณสามารถใช้std::bindกับฟังก์ชันทำลายของคุณ

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

แต่แน่นอนคุณสามารถใช้แลมด้าได้เช่นกัน

std::unique_ptr<Bar, std::function<void(Bar*)>> ptr(create(), [](Bar* b){ destroy(b);});
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.