Как использовать настраиваемое средство удаления с членом std :: unique_ptr?

133

У меня есть класс с членом unique_ptr.

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

Bar - это сторонний класс, который имеет функцию create () и функцию destroy ().

Если бы я хотел использовать std::unique_ptrс ним в отдельной функции, я мог бы сделать:

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

Есть ли способ сделать это std::unique_ptrкак член класса?

huitlarc
источник

Ответы:

134

Предполагая, что createи destroyявляются бесплатными функциями (что похоже на фрагмент кода OP) со следующими подписями:

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

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

class Foo {

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

    // ...

public:

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

    // ...
};

Обратите внимание, что вам не нужно писать здесь лямбда-выражение или настраиваемый удалитель, потому что destroyэто уже удалитель.

Кассио Нери
источник
157
С C ++ 11std::unique_ptr<Bar, decltype(&destroy)> ptr_;
Джо,
2
Обратной стороной этого решения является то, что оно удваивает накладные расходы каждого unique_ptr(все они должны хранить указатель функции вместе с указателем на фактические данные), требует каждый раз передавать функцию уничтожения, она не может быть встроена (поскольку шаблон не может специализируются на конкретной функции, только на сигнатуре) и должны вызывать функцию через указатель (более затратный, чем прямой вызов). Оба RICi и Deduplicator игровая ответы избежать всех этих расходов, специализируясь на функтора.
ShadowRanger
@ShadowRanger, разве он не определен для default_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 без необходимости использования шума try / catch.

Дрю Ноукс
источник
2
Это должен быть ответ, имо. Это более красивое решение. Или есть какие-то недостатки, например, наличие std::functionв определении и т.п.?
j00hi 08
17
@ j00hi, на мой взгляд, это решение имеет ненужные накладные расходы из-за std::function. В отличие от этого решения, лямбда или пользовательский класс, как в принятом ответе, могут быть встроены. Но этот подход имеет преимущество в том случае, если вы хотите изолировать всю реализацию в выделенном модуле.
magras
5
Это приведет к утечке памяти, если конструктор std :: function выбросит (что может произойти, если лямбда слишком велика, чтобы поместиться внутри объекта std :: function)
StaceyGirl
4
Неужели здесь требуется лямбда? Это может быть просто, deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);если customdeleterследовать соглашению (он возвращает void и принимает в качестве аргумента необработанный указатель).
Виктор Полевой
У этого подхода есть один недостаток. std :: function не требуется использовать конструктор перемещения, когда это возможно. Это означает, что когда вы используете 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.

RICi
источник
8
эта опция - единственная, которая работает с массивами, std :: vector и другими коллекциями, поскольку она может использовать конструктор std :: unique_ptr с нулевым параметром. в других ответах используются решения, которые не имеют доступа к этому конструктору с нулевым параметром, поскольку при создании уникального указателя должен быть предоставлен экземпляр Deleter. Но это решение предоставляет класс Deleter ( struct BarDeleter) to std::unique_ptr( std::unique_ptr<Bar, BarDeleter>), который позволяет std::unique_ptrконструктору самостоятельно создавать экземпляр Deleter. т.е. разрешен следующий кодstd::unique_ptr<Bar, BarDeleter> bar[10];
DavidF 08
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 };
}
Deduplicator
источник
2
Я был бы очень осторожен с этим. Открытие stdоткрывает совершенно новую банку червей. Также обратите внимание, что специализация std::make_uniqueне разрешена в C ++ 20 (поэтому не должна выполняться раньше), потому что C ++ 20 запрещает специализацию вещей, stdкоторые не являются шаблонами классов ( std::make_uniqueэто шаблон функции). Обратите внимание, что вы также, вероятно, получите UB, если указатель, переданный в, std::unique_ptr<Bar>был выделен не из create(), а из какой-либо другой функции распределения.
Justin
Я не уверен, что это разрешено. Мне кажется, что сложно доказать, что эта специализация 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);});
mkaes
источник