Як використовувати користувацький делетер з членом std :: unique_ptr?


133

У мене клас з унікальним членом_ptr.

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

Штрих - це сторонній клас, який має функцію create () та знищити ().

Якби я хотів використовувати це std::unique_ptrз ним у самостійній функції, я міг би:

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

Чи є спосіб це зробити std::unique_ptrяк член класу?

Відповіді:


133

Припускаючи, що це createі destroyє вільні функції (що, мабуть, відбувається з фрагмента коду ОП) з такими підписами:

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

Ви можете написати свій клас Fooтак

class Foo {

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

    // ...

public:

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

    // ...
};

Зауважте, що вам не потрібно писати тут ніякого лямбда чи спеціального делетера, оскільки destroyвін вже є.


156
З C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Джо

1
Недоліком цього рішення є те, що воно подвоює накладні витрати кожного unique_ptr(всі вони повинні зберігати вказівник функції разом із вказівником на фактичні дані), вимагає щоразу передавати функцію знищення, вона не може вбудовуватися (оскільки шаблон не може спеціалізуються на конкретній функції, тільки підпис), і повинні викликати функцію через покажчик (дорожче, ніж прямий дзвінок). Обидва RICi і Deduplicator ігрова відповіді уникнути всіх цих витрат, спеціалізуючись на функтора.
ShadowRanger

@ShadowRanger Чи не визначено він за замовчуванням_delete <T> і зберігається вказівник функції щоразу, передаючи його явно чи ні?
Геррготт

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 :: функції кине (що може статися, якщо лямбда занадто великий, щоб вміститися всередині std :: функціонального об'єкта)
StaceyGirl

4
Чи справді потрібна лямбда? Це може бути просто, deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);якщо customdeleterслідує умові (він повертає недійсність і приймає необроблений покажчик як аргумент).
Віктор Полевой

У цього підходу є один недолік. std :: функція не потрібно використовувати конструктор переміщення, коли це можливо. Це означає, що коли ви std :: move (my_deleted_unique_ptr), вміст, закладений лямбда, можливо, буде скопійовано замість переміщеного, що може бути чи не тим, що ви хочете.
GeniusIsme

71

Вам просто потрібно створити клас видалення:

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, не потрібно надавати примірник делетера при його створенні), і додає перевагу можливості користуватися std::unique_ptr<Bar>будь-де без необхідності пам’ятати використовувати спеціальний typedefабо явно постачальник другого параметра шаблону. (Щоб було зрозуміло, це хороше рішення, я проголосував, але це зупиняється на один крок від безшовного рішення)
ShadowRanger

22

Якщо вам не потрібно мати змогу змінити делетер під час виконання, я настійно рекомендую використовувати користувальницький тип делетера. Наприклад, якщо використовувати покажчик на функцію для вашого Deleter, sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*). Іншими словами, половина байтів unique_ptrоб'єкта витрачається даремно.

Хоча написання користувальницького делетера для завершення кожної функції є проблемою. На щастя, ми можемо написати тип шаблону функції:

Оскільки 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()};

Ніфт. Чи я правда, що цим досягаються ті ж переваги (вдвічі більше пам'яті, виклик функції безпосередньо, а не через покажчик функції, потенційний вбудований функція виклику повністю), як і функтор з відповіді rici , лише з меншою кількістю котла?
ShadowRanger

Так, це повинно забезпечити всі переваги користувацького класу делетерів, оскільки саме так і deleter_from_fnє.
rmcclellan

7

Знаєте, користувацький делетер - це не найкращий спосіб, оскільки вам доведеться згадувати це у всьому коді.
Натомість, як вам дозволяється додавати спеціалізації до класів на рівні простору імен ::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>без необхідності передавати тип функтора як шаблон у 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);});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.