Сравнение равенства std::weak_ptr

Я хочу сравнить два std::weak_ptr или один std::weak_ptr и один std::shared_ptr на равенство.

Я хочу знать, является ли объект, на который указывает каждый из weak_ptr/shared_ptr, одинаковым. Сравнение должно давать отрицательные результаты не только в том случае, если адреса не совпадают, но и в том случае, если базовый объект был удален, а затем случайно восстановлен с тем же адресом.

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

auto s1 = std::make_shared<int>(43);
std::weak_ptr<int> w1(s1);

s1.reset();

auto s2 = std::make_shared<int>(41);
std::weak_ptr<int> w2(s2);

assert(!equals(w1,w2));

Шаблоны weak_ptr не предоставляют операторов равенства, и, как я понял, это для уважительная причина.

Таким образом, наивная реализация будет выглядеть так:

template <typename T, typename U>
inline bool naive_equals(const std::weak_ptr<T>& t, const std::weak_ptr<U>& u)
{
    return !t.expired() && t.lock() == u.lock();
}

template <typename T, typename U>
inline bool naive_equals(const std::weak_ptr<T>& t, const std::shared_ptr<U>& u)
{
    return !t.expired() && t.lock() == u;
}

Если срок действия первого weak_ptr истек, он возвращает 0. Если нет, я обновляю weak_ptr до shared_ptr и сравниваю адреса.

Проблема в том, что я должен заблокировать weak_ptr дважды (один раз)! Боюсь, это занимает слишком много времени.

Я придумал это:

template <typename T, typename U>
inline bool equals(const std::weak_ptr<T>& t, const std::weak_ptr<U>& u)
{
    return !t.owner_before(u) && !u.owner_before(t);
}


template <typename T, typename U>
inline bool equals(const std::weak_ptr<T>& t, const std::shared_ptr<U>& u)
{
    return !t.owner_before(u) && !u.owner_before(t);
}

Который проверяет, не находится ли блок владельца u «до» t и t не перед u, поэтому t == u.

Это работает так, как я задумал? Всегда ли два weak_ptr, созданные из разных shared_ptr, сравниваются как неравные таким образом? Или я что-то пропустил?

Изменить. Почему я вообще хочу это сделать? Я хочу иметь контейнер с общими указателями, и я хочу раздавать ссылки на объекты в нем. Я не могу использовать итераторы, так как они могут быть признаны недействительными. Я мог бы раздавать (целочисленные) идентификаторы, но это приводит к проблемам с уникальностью, требует типа карты и усложняет операции поиска/вставки/удаления. Идея состоит в том, чтобы использовать std::set и выдавать сами указатели (заключенные в класс-оболочку) в качестве ключей, чтобы клиенты могли использовать weak_ptr для доступа к объектам в наборе.


person fat-lobyte    schedule 06.09.2012    source источник
comment
как я понял это по уважительной причине. Если вы понимаете, что это по уважительной причине, то зачем вам хотите это делать?   -  person Nicol Bolas    schedule 06.09.2012
comment
Я только что столкнулся с этой проблемой в параллельном приложении, и хорошо, что owner_before() существует. В моем случае сравнение блоков управления было единственным разумным ответом.   -  person vinipsmaker    schedule 08.10.2020


Ответы (1)


Полностью переписывая этот ответ, потому что я совершенно неправильно понял. Это сложная вещь, чтобы сделать правильно!

Обычная реализация std::weak_ptr и std::shared_ptr, соответствующая стандарту, заключается в наличии двух объектов кучи: управляемого объекта и блока управления. Каждый общий указатель, ссылающийся на один и тот же объект, содержит указатель на объект и на управляющий блок, а также каждый слабый указатель. Блок управления ведет учет количества общих указателей и количества слабых указателей и освобождает управляемый объект, когда количество общих указателей достигает 0; сам управляющий блок освобождается, когда количество слабых указателей также достигает 0.

Это усложняется тем фактом, что указатель объекта в общем или слабом указателе может указывать на подобъект фактического управляемого объекта, например. базовый класс, член или даже другой объект кучи, принадлежащий управляемому объекту.

S0 ----------______       MO <------+
   \__             `----> BC        |
      \_ _______--------> m1        |
     ___X__               m2 --> H  |
S1 -/      \__ __----------------^  |
    \___ _____X__                   |
    ____X________\__                |
W0 /----------------`---> CB -------+  
                          s = 2 
                          w = 1 

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

Семантика owner_before/owner_less заключается в сравнении общих и слабых указателей по адресу их управляющего блока, который гарантированно не изменится, если не будет изменен сам указатель; даже если срок действия слабого указателя истекает из-за того, что все общие указатели были уничтожены, его управляющий блок все еще существует до тех пор, пока не будут уничтожены все слабые указатели.

Таким образом, ваш код equals абсолютно правильный и потокобезопасный.

Проблема в том, что он не совместим с shared_ptr::operator==, потому что он сравнивает указатели объектов, а два общих указателя с одним и тем же блоком управления могут указывать на разные объекты (как указано выше).

Для согласованности с shared_ptr::operator== запись t.lock() == u будет абсолютной нормой; обратите внимание, однако, что если он возвращает true, то все еще не определено, что слабый указатель является слабым указателем другого общего указателя; он может быть указателем-псевдонимом, поэтому срок его действия может истечь в следующем коде.

Однако сравнение управляющих блоков имеет меньше накладных расходов (поскольку не нужно смотреть на управляющий блок) и даст те же результаты, что и ==, если вы не используете указатели-псевдонимы.


Я думаю, что здесь есть что-то вроде недостатка стандарта; добавление owner_equals и owner_hash позволило бы использовать weak_ptr в неупорядоченных контейнерах, а при наличии owner_equals фактически становится разумным сравнивать слабые указатели на равенство, поскольку вы можете безопасно сравнивать указатель блока управления затем указатель объекта, поскольку, если два слабых указателя имеют один и тот же блок управления, тогда вы знаете, что либо оба, либо ни один из них не истек. Что-то для следующей версии стандарта, возможно.

person ecatmur    schedule 06.09.2012
comment
Спасибо, я совершенно забыл о многопоточности, хотя ее определенно нужно учитывать для моего приложения. Что меня действительно интересует, так это сравнение с shared_ptr. Будет ли (w.lock() == s) достаточным для сравнения? w.lock() является атомарным, и после его блокировки ни один из указателей не может истечь. Будет ли это работать? - person fat-lobyte; 07.09.2012
comment
Спасибо за развернутый ответ. Думаю, проверка на равенство адреса блока управления должна быть достаточно эффективной. Поскольку owner_before() имеет перегруженную версию для shared_ptr, я могу использовать последнюю версию для обоих сравнений. - person fat-lobyte; 07.09.2012
comment
Кстати, специалист из Microsoft нашел способ опустить указатель в блоке управления с его оптимизацией «мы-знаем-где-вы-живете», разместив фактический объект в блоке управления. Также ознакомьтесь с слайды, на его 4. слайде есть аккуратная диаграмма обычной реализации. - person fat-lobyte; 07.09.2012
comment
Спасибо, приятного чтения! Оптимизация «мы-знаем-где-вы-живете» не всегда может быть использована, поскольку по-прежнему возможно построить указатель shared_ptr на newed; неверно в новом коде, который должен использовать make_shared, но полезен для повышения безопасности существующего устаревшего кода и интерфейсов. - person ecatmur; 07.09.2012
comment
Я полностью согласен с owner_hash и owner_equals. Я думаю, что-то вроде этого могло бы помочь: template <typename T> class comparable_weak_ptr { const T* m_original; std::weak_ptr<T> m_weak; public: comparable_weak_ptr(std::shared_ptr<T> shr) : m_original(shr.get()), m_weak(std::move(shr)) {} bool operator==(const std::shared_ptr<T>& shr) const { return (shr.get() == nullptr && m_original == nullptr) || shr.get() == m_original && shr == m_weak.lock(); } }; - person Ben; 20.01.2021