Почему shared_ptr <void> законно, а unique_ptr <void> неправильно сформирован?

99

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

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;
Ad N
источник

Ответы:

120

Это потому, что std::shared_ptrреализуют стирание типа, а std::unique_ptrне нет.


Поскольку std::shared_ptrреализует стирание типов, он также поддерживает еще одно интересное свойство, а именно. ему не нужен тип удалителя в качестве аргумента типа шаблона для шаблона класса. Посмотрите их декларации:

template<class T,class Deleter = std::default_delete<T> > 
class unique_ptr;

который имеет Deleterпараметр типа, а

template<class T> 
class shared_ptr;

его нет.

Теперь вопрос в том, зачем shared_ptrреализовано стирание типов? Что ж, он делает это, потому что он должен поддерживать подсчет ссылок, и для поддержки этого он должен выделять память из кучи, и, поскольку он все равно должен выделять память, он идет еще на один шаг и реализует стирание типов - для чего нужна куча распределение тоже. Так что по сути это просто оппортунист!

Из-за стирания типа std::shared_ptrможет поддерживать две вещи:

  • Он может хранить объекты любого типа void*, но все же может правильно удалять объекты при уничтожении , правильно вызывая их деструктор .
  • Тип удалителя не передается как аргумент типа в шаблон класса, что означает некоторую свободу без ущерба для безопасности типов .

Хорошо. Вот и все о том, как std::shared_ptrработает.

Теперь вопрос в том, можно ли std::unique_ptrхранить объекты как void* ? Что ж, ответ - да - при условии, что вы передадите в качестве аргумента подходящий удалитель. Вот одна из таких демонстраций:

int main()
{
    auto deleter = [](void const * data ) {
        int const * p = static_cast<int const*>(data);
        std::cout << *p << " located at " << p <<  " is being deleted";
        delete p;
    };

    std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);

} //p will be deleted here, both p ;-)

Вывод ( онлайн-демонстрация ):

959 located at 0x18aec20 is being deleted

В комментарии вы задали очень интересный вопрос:

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

на который @Steve Jessop предложил следующее решение,

Я никогда не пробовал этого, но, может быть, вы могли бы добиться этого, используя подходящий std::functionтип удаления с unique_ptr? Предположим, что это действительно работает, тогда у вас все готово, эксклюзивное право собственности и удаление типа со стиранием.

Следуя этому предложению, я реализовал это (хотя оно не используется, std::functionпоскольку не кажется необходимым):

using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
    return unique_void_ptr(ptr, [](void const * data) {
         T const * p = static_cast<T const*>(data);
         std::cout << "{" << *p << "} located at [" << p <<  "] is being deleted.\n";
         delete p;
    });
}

int main()
{
    auto p1 = unique_void(new int(959));
    auto p2 = unique_void(new double(595.5));
    auto p3 = unique_void(new std::string("Hello World"));
}  

Вывод ( онлайн-демонстрация ):

{Hello World} located at [0x2364c60] is being deleted.
{595.5} located at [0x2364c40] is being deleted.
{959} located at [0x2364c20] is being deleted.

Надеюсь, это поможет.

Наваз
источник
13
Хороший ответ, +1. Но вы могли бы сделать его еще лучше, явно указав, что по- std::unique_ptr<void, D>прежнему возможно, предоставив подходящий D.
Энгью больше не гордится SO
1
@Angrew: Отлично, вы нашли настоящий основной вопрос, который не был написан в моем вопросе;)
Объявление N
@Nawaz: Спасибо. В моем случае мне понадобится удалитель стирания типа, но это тоже кажется возможным (за счет выделения некоторой кучи). По сути, означает ли это, что на самом деле существует ниша для третьего типа интеллектуального указателя: интеллектуального указателя исключительного владения со стиранием типа?
Объявление N
8
@AdN: Я никогда не пробовал этого, но, может быть, вы могли бы добиться этого, используя подходящий std::functionтип удаления с unique_ptr? Предположим, что это действительно работает, тогда у вас все готово, эксклюзивное право собственности и удаление типа со стиранием.
Стив Джессоп
Грамматическая гнида: "почему X глаголов Y?" должен быть «почему делает X глагол Y?» по-английски.
zwol 02
7

Одно из объяснений заключается в одном из многих случаев использования a, shared_ptrа именно в качестве индикатора срока службы или дозорного.

Об этом упоминалось в исходной документации по ускорению:

auto register_callback(std::function<void()> closure, std::shared_ptr<void> pv)
{
    auto closure_target = { closure, std::weak_ptr<void>(pv) };
    ...
    // store the target somewhere, and later....
}

void call_closure(closure_target target)
{
    // test whether target of the closure still exists
    auto lock = target.sentinel.lock();
    if (lock) {
        // if so, call the closure
        target.closure();
    }
}

Где closure_targetчто-то вроде этого:

struct closure_target {
    std::function<void()> closure;
    std::weak_ptr<void> sentinel;
};

Вызывающий абонент регистрирует обратный вызов примерно так:

struct active_object : std::enable_shared_from_this<active_object>
{
    void start() {
      event_emitter_.register_callback([this] { this->on_callback(); }, 
                                       shared_from_this());
    }

    void on_callback()
    {
        // this is only ever called if we still exist 
    }
};

поскольку shared_ptr<X>он всегда конвертируется shared_ptr<void>, event_emitter теперь может быть в блаженном неведении о типе объекта, в который он вызывает.

Эта договоренность освобождает подписчиков эмиттера событий от обязанности обрабатывать случаи пересечения (что, если обратный вызов находится в очереди, ожидая выполнения действия, пока active_object уйдет?), А также означает, что нет необходимости синхронизировать отписку. weak_ptr<void>::lockэто синхронизированная операция.

Ричард Ходжес
источник