unique_ptr的赋值运算符复制由引用存储的删除器。它是一个功能还是一个错误?

时间:2016-02-10 14:04:50

标签: c++ c++11 smart-pointers 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的要求,并从类型为{{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中的删除引用是违反直觉的,甚至容易出错:

  1. 当您通过引用而不是值存储删除器时,这主要是因为您希望共享删除器具有一些重要的唯一状态。因此,您不希望共享删除器被覆盖,并且在唯一的ptr分配后其状态将丢失。

  2. 预计unique_ptr的赋值非常大,特别是如果删除器是引用。但不是这样,你可以复制删除器,这可能是(出乎意料)昂贵的。

  3. 分配后,指针将绑定到原始删除器的副本,而不是原始删除器本身。如果删除者的身份很重要,这可能会导致一些意想不到的副作用。

  4. 此外,当前行为阻止对删除器使用const引用,因为您无法复制到const对象中。

  5. 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而不是非参考类型(即值类型)中有一个引用类型删除器是有用的?

3 个答案:

答案 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)

初始化后无法重新引用参考。它作为所指的对象以各种方式起作用。这包括任务。

因为引用充当它引用的对象,所以复制引用是你在普通类中得到的,赋值操作符被实现为每个成员赋值的序列。