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