Почему shared_ptr <void> юридический, а unique_ptr <void> плохо сформирован?

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

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;

Ответ 1

Это потому, что 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

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

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

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

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

Следуя этому предложению, я реализовал это,

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

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

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
    return unique_void_ptr(ptr, &deleter<T>);
}

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.

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

Ответ 2

Одно из обоснований заключается в одном из многих случаев использования 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 теперь может быть блаженно не осведомлен о типе объекта, на который он обращается.

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