Оператор присваивания unique_ptr копирует удаляемый файл, хранящийся ссылкой. Это особенность или ошибка?

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

struct CountingDeleter
{
    void operator()(std::string *p) {
        ++cntr_;
        delete p;
    }

    unsigned long cntr_ = 0;
};

int main()
{
    CountingDeleter d1{}, d2{};

    {
        std::unique_ptr<std::string, CountingDeleter&>
            p1(new std::string{"first"} , d1),
            p2(new std::string{"second"}, d2);

        p1 = std::move(p2); // does d1 = d2 under cover
    }

    std::cout << "d1 " << d1.cntr_ << "\n"; // output: d1 1
    std::cout << "d2 " << d2.cntr_ << "\n"; // output: d2 0
}

Для меня было неожиданностью, что назначение в коде выше имеет побочный эффект копирования d2 в d1. Я дважды проверил это и обнаружил, что это поведение описано в стандарте в [unique.ptr.single.asgn]:

(1) - Требует: Если D не является ссылочным типом, D должен удовлетворять требованиям MoveAssignable, а присвоение удаляющего из r значения типа D не должно вызывать исключения. В противном случае D является ссылочным типом; remove_reference_t<D> должен удовлетворять требованиям CopyAssignable, а присвоение deleter из lvalue типа D не должно вызывать исключения.

(2) - Эффекты: переносит право собственности с u на *this, как если бы он вызывал reset(u.release()), а затем get_deleter() = std::forward<D>(u.get_deleter()).

Чтобы получить ожидаемое поведение (мелкая копия ссылки на удаление), мне пришлось обернуть ссылку на удаление в std::reference_wrapper:

std::unique_ptr<std::string, std::reference_wrapper<CountingDeleter>>
    p1(new std::string{"first"} , d1),
    p2(new std::string{"second"}, d2);

p1 = std::move(p2); // p1 now stores reference to d2 => no side effects!

Для меня текущая обработка ссылки удаления в уникальном ptr является интуитивно понятной и даже подверженной ошибкам:

  • Когда вы храните делеттер по ссылке, а не по значению, это в основном потому, что вы хотите, чтобы общий делетер с каким-то важным уникальным состоянием. Таким образом, вы не ожидаете, что общий удаленный файл будет перезаписан и его состояние будет потеряно после уникального назначения ptr.

  • Ожидалось, что присвоение unique_ptr является чрезвычайно чипом, особенно если делетер является ссылкой. Но вместо этого вы получаете копирование делетера, что может быть (неожиданно) дорогостоящим.

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

  • Также, текущее поведение предотвращает использование ссылки const для делетера, потому что вы просто не можете копировать в объект const.

IMO было бы лучше запретить удаление ссылочных типов и принимать только подвижные типы значений.

Итак, мой вопрос следующий (это выглядит как два вопроса в одном, извините):

  • Есть ли причина, по которой ведет себя стандартный unique_ptr?

  • Есть ли у кого-нибудь хороший пример, когда полезно иметь ссылочный тип deleter в unique_ptr, а не без ссылки (т.е. тип значения)?

Ответ 1

Это функция.

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

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

Итак, если вы этого не хотите, почему вы даже храните делетера по ссылке?

Что означает мелкая копия ссылки? В С++ нет такой вещи. Если вам не нужна эталонная семантика, не используйте ссылки.

Если вы действительно хотите это сделать, тогда решение прост: определите назначение для вашего дебетователя, чтобы не менять счетчик:

CountingDeleter&
operator=(const CountingDeleter&) noexcept
{ return *this; }

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

struct CountingDeleter
{
    void operator()(std::string *p) {
        ++*cntr_;
        delete p;
    }

    unsigned long* cntr_;
};

unsigned long c1 = 0, c2 = 0;
CountingDeleter d1{&c1}, d2{&c2};

{
    std::unique_ptr<std::string, CountingDeleter>
        p1(new std::string{"first"} , d1),
        p2(new std::string{"second"}, d2);

Ответ 2

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

Использование элементов указателя вместо этого исправляет это. В качестве альтернативы используйте std::reference_wrapper<> и std::ref().


Почему он выполняет глубокую копию удаления, хранящегося по ссылке, а не только мелкой копии?

Он выполняет копию экземпляра. Если копируемое значение является указателем, это будет мелкая копия.

Ответ 3

Ссылка не может быть восстановлена ​​после инициализации. Он действует во всех отношениях как объект, на который он ссылается. И это включает в себя назначение.

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