Отображение случая, когда у вас есть 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
, а не без ссылки (т.е. тип значения)?