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

У меня есть класс с членом 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 в качестве члена класса?


person huitlarc    schedule 27.09.2013    source источник


Ответы (7)


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

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

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

class Foo {

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

    // ...

public:

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

    // ...
};

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

person Cassio Neri    schedule 27.09.2013
comment
С С++ 11 std::unique_ptr<Bar, decltype(&destroy)> ptr_; - person Joe; 26.03.2015
comment
Недостатком этого решения является то, что оно удваивает накладные расходы каждого unique_ptr (все они должны хранить указатель на функцию вместе с указателем на фактические данные), требует передачи функции уничтожения каждый раз, она не может быть встроена (поскольку шаблон может т специализируются на конкретной функции, только сигнатура) и должны вызывать функцию через указатель (более затратно, чем прямой вызов). Оба rici и Deduplicator ответы позволяют избежать всех этих затрат, специализируясь на функторе. - person ShadowRanger; 22.11.2019
comment
@ShadowRanger разве он не определен как default_delete‹T› и хранит указатель на функцию каждый раз, независимо от того, передаете ли вы его явно или нет? - person Herrgott; 24.06.2020

Это можно сделать чисто, используя лямбду в С++ 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, без необходимости пытаться/поймать шум.

person Drew Noakes    schedule 09.10.2014
comment
Это должен быть ответ, имхо. Это более красивое решение. Или есть какие-то недостатки, например. наличие std::function в определении или что-то в этом роде? - person j00hi; 08.07.2015
comment
@ j00hi, на мой взгляд, это решение имеет ненужные накладные расходы из-за std::function. Лямбда или пользовательский класс, как в принятом ответе, могут быть встроены, в отличие от этого решения. Но этот подход имеет преимущество в том случае, если вы хотите изолировать всю реализацию в выделенном модуле. - person magras; 12.10.2015
comment
Это приведет к утечке памяти, если конструктор std::function сработает (что может произойти, если лямбда слишком велика, чтобы поместиться внутри объекта std::function) - person StaceyGirl; 15.03.2016
comment
Действительно ли здесь требуется лямбда? Это может быть просто deleted_unique_ptr<Foo> foo(new Foo(), customdeleter);, если customdeleter следует соглашению (возвращает void и принимает необработанный указатель в качестве аргумента). - person Victor Polevoy; 05.09.2017
comment
У этого подхода есть один недостаток. std::function не обязан использовать конструктор перемещения, когда это возможно. Это означает, что когда вы std::move(my_deleted_unique_ptr), содержимое, заключенное в лямбда, возможно, будет скопировано, а не перемещено, что может быть или не быть тем, что вы хотите. - person GeniusIsme; 27.09.2017
comment
@VictorPolevoy: Согласен; лямбда-обертка полезна, когда она дает вам специализацию по типам (избегая вызова функции через указатель функции и разрешая встраивание из-за полной специализации), но в этом случае лямбда присваивается std::function, что устраняет оба преимущества; он не может быть встроен и должен вызываться динамически (поскольку такая же специализация используется для любого удаления с той же сигнатурой). - person ShadowRanger; 21.11.2019
comment
@GeniusIsme: я бы не считал это настоящей проблемой; эта лямбда (и я полагаю, что большинство лямбд, используемых для этой цели) не имеет захватов, поэтому в любом случае нет состояния для копирования/перемещения. - person ShadowRanger; 22.11.2019
comment
@ShadowRanger: меня это уже укусило. Это были не совсем пользовательские программы удаления unique_ptr, но они включали перемещение std::function и деструкторов. Просто предупреждаю. - person GeniusIsme; 03.12.2019

Вам просто нужно создать класс удаления:

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.

person rici    schedule 27.09.2013
comment
эта опция является единственной, которая работает с массивами, 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]; - person DavidF; 08.07.2016
comment
Я бы создал typedef для удобства использования typedef std::unique_ptr<Bar, BarDeleter> UniqueBarPtr - person DavidF; 08.07.2016
comment
@DavidF: Или используйте подход дедупликатора, который имеет те же преимущества (встраивание удаления, отсутствие дополнительного хранилища для каждого unique_ptr, нет необходимости для предоставления экземпляра средства удаления при построении) и добавляет преимущество возможности использовать std::unique_ptr<Bar> где угодно без необходимости помнить об использовании специального typedef или явно указывать второй параметр шаблона. (Чтобы было ясно, это хорошее решение, я проголосовал за него, но оно останавливается на один шаг от бесшовного решения) - person ShadowRanger; 22.11.2019

Если вам не нужно менять средство удаления во время выполнения, я настоятельно рекомендую использовать настраиваемый тип средства удаления. Например, если вы используете указатель на функцию для удаления, sizeof(unique_ptr<T, fptr>) == 2 * sizeof(T*). Другими словами, половина байтов объекта unique_ptr тратится впустую.

Однако написание пользовательского удаления для переноса каждой функции — проблема. К счастью, мы можем написать тип, основанный на функции:

Начиная с С++ 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()};

До С++ 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()};
person Justin    schedule 10.07.2018
comment
Изящный. Правильно ли я понимаю, что это дает те же преимущества (уменьшение затрат памяти вдвое, вызов функции напрямую, а не через указатель функции, потенциальный вызов встроенной функции полностью), что и функтор из ответ Ричи, просто с меньшим количеством шаблонов? - person ShadowRanger; 22.11.2019
comment
Да, это должно предоставить все преимущества пользовательского класса удаления, поскольку это то, что deleter_from_fn. - person rmcclellan; 03.02.2020
comment
// stackoverflow.com/questions/19053351/ // stackoverflow.com/questions/38456127/ #if ((defined(_MSVC_LANG) && _MSVC_LANG ›= 201703L) || __cplusplus ›= 201703L) //здесь специфичные для C++17 вещи // my_unique_ptr‹Bar, destroy› p{create()}; #define MY_UNIQUE_PTR(T, D) my_unique_ptr‹T, D› #else // my_unique_ptr‹Bar, decltype(destroy), destroy› p{create()}; #define MY_UNIQUE_PTR(T, D) my_unique_ptr‹T, decltype(&D), D› #endif - person samm; 01.06.2021

Вы знаете, что использование пользовательского удаления — не лучший способ, так как вам придется упоминать его во всем коде.
Вместо этого поскольку вам разрешено добавлять специализации к классам уровня пространства имен в ::std при условии, что задействованы пользовательские типы и вы соблюдаете семантику, сделайте следующее:

Специализируйте std::default_delete:

template <>
struct ::std::default_delete<Bar> {
    default_delete() = default;
    template <class U>
    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 };
}
person Deduplicator    schedule 20.06.2018
comment
Я был бы очень осторожен с этим. Открытие std открывает целую новую банку червей. Также обратите внимание, что специализация std::make_unique не разрешена после C++20 (поэтому этого не следует делать раньше), потому что C++20 запрещает специализацию вещей в std, которые не являются шаблонами классов (std::make_unique — это шаблон функции). Обратите внимание, что вы также, вероятно, получите UB, если указатель, переданный в std::unique_ptr<Bar>, был выделен не из create(), а из какой-то другой функции распределения. - person Justin; 10.07.2018
comment
Я не уверен, что это разрешено. Мне кажется, трудно доказать, что эта специализация std::default_delete соответствует требованиям исходного шаблона. Я бы предположил, что std::default_delete<Foo>()(p) будет допустимым способом записи delete p;, поэтому, если delete p; будет допустимым для записи (т. Е. Если Foo завершено), это не будет таким же поведением. Кроме того, если бы delete p; нельзя было записать (Foo неполный), это означало бы указание нового поведения для std::default_delete<Foo>, а не сохранение поведения прежним. - person Justin; 11.07.2018
comment
Специализация make_unique проблематична, но я определенно использовал перегрузку std::default_delete (не шаблонную с enable_if, только для структур C, таких как BIGNUM OpenSSL, которые используют известную функцию уничтожения, где создание подклассов не произойдет), и это, безусловно, самый простой подход, так как остальная часть вашего кода может просто использовать unique_ptr<special_type> без необходимости передавать тип функтора как шаблонный Deleter повсюду, а также использовать typedef/using для присвоения имени указанному типу, чтобы избежать этой проблемы. - person ShadowRanger; 21.11.2019
comment
Это может быть самым простым, но это также неопределенное поведение. Такая специализация недопустима, поскольку не соответствует требованиям для специализированного типа. Короче говоря, специализация std::default_delete допустима только в том случае, если ваша специализация вызывает delete для данного указателя. Да, он имеет ограниченное применение, помимо ведения журнала или аналогичных целей. - person spectras; 09.04.2021

Вы можете просто использовать 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);});
person mkaes    schedule 27.09.2013
comment
Оба подхода тратят место для экземпляра удаления внутри объекта unique_ptr. Такое состояние не нужно. - person Kuba hasn't forgotten Monica; 28.11.2020

С лямбда вы можете получить тот же размер, что и обычный std::unique_ptr. Сравните размеры:

plain: 8
lambda: 8
fpointer: 16
std::function: 40

Что является результатом следующего. (Я объявил лямбду вне области действия класса. Не уверен, что вы можете охватить ее внутри класса.)

#include <iostream>
#include <memory>
#include <functional>

struct Bar {};
void destroy(Bar* b) {}
Bar* create() { return 0; }

auto lambda_destroyer = [](Bar* b) {destroy(b);};

class Foo {
    
    std::unique_ptr<Bar, decltype(lambda_destroyer)> ptr_;

public:

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

int main()
{
    std::cout << "plain: "         << sizeof (std::unique_ptr<Bar>) << std::endl
              << "lambda: "        << sizeof (std::unique_ptr<Bar, decltype(lambda_destroyer)>) << std::endl
              << "fpointer: "      << sizeof (std::unique_ptr<Bar, void(*)(Bar*)>) << std::endl
              << "std::function: " << sizeof (std::unique_ptr<Bar, std::function<void(Bar*)>>) << std::endl;
}
person johv    schedule 14.05.2021