使用引用存储自定义删除器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
的要求,并从类型为{{1}的rvalue中分配删除者不应该抛出异常。 否则,D
是引用类型;D
应满足remove_reference_t<D>
要求,并且从CopyAssignable
类型的左值分配删除者不得抛出异常。(2) - 效果:将所有权从
D
转移到u
,就像调用*this
后跟reset(u.release())
一样。
为了获得我期望的行为(删除器引用的浅表副本),我必须将删除器引用包装到get_deleter() = std::forward<D>(u.get_deleter())
中:
std::reference_wrapper
对我来说,当前处理独特ptr中的删除引用是违反直觉的,甚至容易出错:
当您通过引用而不是值存储删除器时,这主要是因为您希望共享删除器具有一些重要的唯一状态。因此,您不希望共享删除器被覆盖,并且在唯一的ptr分配后其状态将丢失。
预计unique_ptr的赋值非常大,特别是如果删除器是引用。但不是这样,你可以复制删除器,这可能是(出乎意料)昂贵的。
分配后,指针将绑定到原始删除器的副本,而不是原始删除器本身。如果删除者的身份很重要,这可能会导致一些意想不到的副作用。
此外,当前行为阻止对删除器使用const引用,因为您无法复制到const对象中。
IMO最好禁止引用类型的删除并仅接受可移动的值类型。
所以我的问题如下(看起来好像是两个问题,对不起):
标准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!
的行为是否有任何理由?
有没有一个很好的例子,在unique_ptr
而不是非参考类型(即值类型)中有一个引用类型删除器是有用的?
答案 0 :(得分:13)
这是一个功能。
如果您有状态删除器,则可能是状态很重要,并且与将用于删除的指针相关联。这意味着当指针的所有权转移时,应该转移删除状态。
但是如果你通过引用存储删除器,则意味着你关心删除器的身份,而不仅仅是它的值(即它的状态),并且更新unique_ptr
不应该将引用重新绑定到不同的对象
所以如果你不想要这个,为什么你甚至通过引用存储一个删除器?
参考的浅层副本甚至是什么意思?在C ++中没有这样的东西。如果您不想要引用语义,请不要使用引用。
如果你真的想这样做,那么解决方案很简单:为你的删除者定义一个不改变计数器的作业:
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);
答案 1 :(得分:2)
具有引用数据成员通常会导致令人惊讶的结果,因为分配给引用具有非值语义,因为无法重新分配引用以引用另一个对象。基本上,引用数据成员会破坏赋值运算符的语义。
使用指针成员来修复它。或者,使用std::reference_wrapper<>
和std::ref()
。
为什么它执行由引用存储的删除器的深层副本而不仅仅是浅层副本?
执行成员复制。如果要复制的值是指针,则恰好是浅拷贝。
答案 2 :(得分:0)
初始化后无法重新引用参考。它作为所指的对象以各种方式起作用。这包括任务。
因为引用充当它引用的对象,所以复制引用是你在普通类中得到的,赋值操作符被实现为每个成员赋值的序列。