Làm cách nào để sử dụng một deleter tùy chỉnh với thành viên std :: unique_ptr?


133

Tôi có một lớp học với một thành viên unique_ptr.

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

Bar là lớp bên thứ ba có chức năng tạo () và chức năng hủy ().

Nếu tôi muốn sử dụng một std::unique_ptrvới nó trong một chức năng độc lập, tôi có thể làm:

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

Có cách nào để làm điều này với std::unique_ptrtư cách là thành viên của một lớp không?

Câu trả lời:


133

Giả sử rằng createdestroylà các hàm miễn phí (dường như là trường hợp từ đoạn mã của OP) với các chữ ký sau:

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

Bạn có thể viết lớp của bạn Foonhư thế này

class Foo {

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

    // ...

public:

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

    // ...
};

Lưu ý rằng bạn không cần phải viết bất kỳ lambda hoặc deleter tùy chỉnh nào ở đây vì destroyđã là một deleter.


156
Với C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Joe

1
Nhược điểm của giải pháp này là nó tăng gấp đôi chi phí của mọi thứ unique_ptr(tất cả chúng phải lưu con trỏ hàm cùng với con trỏ vào dữ liệu thực tế), yêu cầu chuyển hàm hủy mỗi lần, nó không thể nội tuyến (vì mẫu không thể chuyên về chức năng cụ thể, chỉ có chữ ký) và phải gọi hàm thông qua con trỏ (tốn kém hơn so với gọi trực tiếp). Cả câu trả lời của riciDed repeatator đều tránh tất cả các chi phí này bằng cách dành riêng cho một functor.
ShadowRanger

@ShadowRanger không được xác định là default_delete <T> và con trỏ hàm được lưu trữ mọi lúc cho dù bạn có vượt qua nó một cách rõ ràng hay không?
Herrgott

117

Có thể thực hiện việc này một cách sạch sẽ bằng lambda trong C ++ 11 (được thử nghiệm trong G ++ 4.8.2).

Đưa ra điều này có thể tái sử dụng typedef:

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

Bạn có thể viết:

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

Ví dụ: với một FILE*:

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

Với điều này, bạn sẽ có được những lợi ích của việc dọn dẹp an toàn ngoại lệ bằng RAII, mà không cần phải thử / bắt tiếng ồn.


2
Đây sẽ là câu trả lời, imo. Đó là một giải pháp đẹp hơn. Hoặc có bất kỳ nhược điểm nào, ví dụ như có std::functiontrong định nghĩa hoặc tương tự?
j00hi

17
@ j00hi, theo tôi giải pháp này có chi phí không cần thiết vì std::function. Lambda hoặc lớp tùy chỉnh như trong câu trả lời được chấp nhận có thể được nội tuyến không giống như giải pháp này. Nhưng phương pháp này có lợi thế trong trường hợp khi bạn muốn cô lập tất cả việc thực hiện trong mô-đun chuyên dụng.
magras

5
Điều này sẽ rò rỉ bộ nhớ nếu std :: function constructor ném (điều này có thể xảy ra nếu lambda quá lớn để phù hợp với bên trong std :: object object)
StaceyGirl 15/03/2016

4
Là lambda thực sự yêu cầu ở đây? Nó có thể đơn giản deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);nếu customdeletertuân theo quy ước (nó trả về void và chấp nhận con trỏ thô làm đối số).
Victor Polevoy

Có một nhược điểm của phương pháp này. Hàm std :: không bắt buộc phải sử dụng hàm tạo di chuyển bất cứ khi nào có thể. Điều này có nghĩa là khi bạn std :: move (my_delatted_unique_ptr), nội dung được bao quanh bởi lambda sẽ có thể được sao chép thay vì di chuyển, có thể hoặc không phải là điều bạn muốn.
GeniusIsme

70

Bạn chỉ cần tạo một lớp deleter:

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

và cung cấp nó như là đối số mẫu của unique_ptr. Bạn vẫn sẽ phải khởi tạo unique_ptr trong các hàm tạo của mình:

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

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

Theo tôi biết, tất cả các thư viện c ++ phổ biến đều thực hiện điều này một cách chính xác; vì BarDeleter thực tế không có bất kỳ trạng thái nào, nó không cần chiếm bất kỳ khoảng trống nào trong unique_ptr.


8
tùy chọn này là duy nhất hoạt động với các mảng, std :: vector và các bộ sưu tập khác vì nó có thể sử dụng tham số std :: unique_ptr. các câu trả lời khác sử dụng các giải pháp không có quyền truy cập vào hàm tạo tham số zero này vì phải cung cấp một đối tượng Deleter khi xây dựng một con trỏ duy nhất. Nhưng giải pháp này cung cấp một lớp Deleter ( struct BarDeleter) đến std::unique_ptr( std::unique_ptr<Bar, BarDeleter>) cho phép hàm std::unique_ptrtạo tự tạo một cá thể Deleter. tức là mã sau đây được cho phépstd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF

12
Tôi sẽ tạo một typedef để dễ sử dụngtypedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr
DavidF

@DavidF: Hoặc sử dụng phương pháp của Ded repeatator , có cùng các ưu điểm (xóa nội tuyến, không lưu trữ thêm trên mỗi unique_ptr, không cần cung cấp phiên bản của trình phân tích khi xây dựng) và thêm lợi ích của việc có thể sử dụng ở std::unique_ptr<Bar>bất cứ đâu mà không cần phải nhớ để sử dụng đặc biệt typedefhoặc cung cấp rõ ràng tham số mẫu thứ hai. (Để rõ ràng, đây là một giải pháp tốt, tôi đã bình chọn, nhưng nó dừng lại một bước ngại ngùng với một giải pháp liền mạch)
ShadowRanger

22

Trừ khi bạn cần có khả năng thay đổi deleter trong thời gian chạy, tôi thực sự khuyên bạn nên sử dụng một loại deleter tùy chỉnh. Ví dụ: nếu sử dụng một con trỏ hàm cho deleter của bạn , sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*). Nói cách khác, một nửa số byte của unique_ptrđối tượng bị lãng phí.

Tuy nhiên, viết một deleter tùy chỉnh để bọc mọi chức năng là một điều phiền phức. Rất may, chúng ta có thể viết một kiểu templated trên hàm:

Kể từ 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()};

Trước 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()};

Tiện lợi Tôi có đúng không khi điều này đạt được những lợi ích tương tự (giảm một nửa chi phí bộ nhớ, gọi hàm trực tiếp thay vì thông qua con trỏ hàm, hàm nội tuyến tiềm năng gọi hoàn toàn) như functor từ câu trả lời của rici , chỉ với ít nồi hơi hơn?
ShadowRanger

Vâng, điều này sẽ cung cấp tất cả các lợi ích của một lớp deleter tùy chỉnh, vì đó là cái gì deleter_from_fn.
rmcclellan

6

Bạn chỉ có thể sử dụng std::bindvới chức năng hủy của bạn.

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

Nhưng tất nhiên bạn cũng có thể sử dụng lambda.

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

6

Bạn biết đấy, sử dụng một deleter tùy chỉnh không phải là cách tốt nhất, vì bạn sẽ phải đề cập đến nó trên toàn bộ mã của bạn.
Thay vào đó, khi bạn được phép thêm các chuyên ngành vào các lớp mức không gian ::stdmiễn là các loại tùy chỉnh có liên quan và bạn tôn trọng ngữ nghĩa, hãy làm điều đó:

Chuyê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); }
};

Và cũng có thể làm 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
Tôi sẽ rất cẩn thận với điều này. Mở stdra mở ra một lon giun hoàn toàn mới. Cũng lưu ý rằng việc chuyên môn hóa std::make_uniquekhông được phép đăng C ++ 20 (do đó không nên thực hiện trước đó) vì C ++ 20 không cho phép chuyên môn hóa những thứ stdkhông phải là mẫu lớp ( std::make_uniquelà mẫu hàm). Lưu ý rằng có lẽ bạn cũng sẽ kết thúc với UB nếu con trỏ được truyền vào std::unique_ptr<Bar>không được phân bổ create(), mà từ một số chức năng phân bổ khác.
Justin

Tôi không tin rằng điều này được cho phép. Đối với tôi có vẻ như rất khó để chứng minh rằng chuyên môn này củastd::default_delete đáp ứng yêu cầu của mẫu ban đầu. Tôi sẽ tưởng tượng rằng đó std::default_delete<Foo>()(p)sẽ là một cách viết hợp lệ delete p;, vì vậy nếu delete p;có thể viết hợp lệ (nghĩa là nếu Foohoàn thành), đây sẽ không phải là hành vi tương tự. Hơn nữa, nếu delete p;không hợp lệ để viết ( Fookhông đầy đủ), điều này sẽ chỉ định hành vi mới cho std::default_delete<Foo>, thay vì giữ hành vi như cũ.
Justin

Các make_uniquechuyên môn là có vấn đề, nhưng tôi đã chắc chắn sử dụng std::default_deletequá tải (không templated với enable_if, chỉ cần cho cấu trúc của C như OpenSSL của BIGNUMsử dụng một chức năng phá hủy được biết đến, nơi subclassing sẽ không xảy ra), và đó là bởi đến nay các phương pháp đơn giản nhất, như phần còn lại của mã của bạn chỉ có thể sử dụng unique_ptr<special_type>mà không cần phải chuyển loại functor như templated Deleterkhắp nơi, cũng không sử dụng typedef/ usingđể đặt tên cho loại đã nói để tránh vấn đề đó.
ShadowRanger
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.