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

oli*_*ora 11 c++ smart-pointers unique-ptr c++11

当您拥有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
}
Run Code Online (Sandbox Code Playgroud)

这是一个惊喜,我在上面的代码分配具有复制的副作用d2d1.我仔细检查了一下,发现这种行为与[unique.ptr.single.asgn]中的标准一样:

(1) - 要求:如果D不是参考类型,D则应满足MoveAssignable删除者的要求和类型的左值,D不得抛出异常.否则,D是一个参考类型; remove_reference_t<D>应满足CopyAssignable要求,并从类型的左值分配删除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!
Run Code Online (Sandbox Code Playgroud)

对我来说,当前处理独特ptr中的删除引用是违反直觉的,甚至容易出错:

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

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

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

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

IMO最好禁止引用类型的删除并仅接受可移动值类型.

所以我的问题是以下(它看起来像两个问题,对不起):

  • 标准的unique_ptr行为是否有任何理由?

  • 有没有一个很好的例子,有一个引用类型删除器unique_ptr而不是非引用类型(即值类型)是有用的?

Jon*_*ely 13

这是一个功能.

如果您有状态删除器,则可能是状态很重要,并且与将用于删除的指针相关联.这意味着当指针的所有权转移时,应该转移删除状态.

但是如果通过引用存储删除器,则意味着您关心删除器的标识,而不仅仅是它的值(即它的状态),并且更新不unique_ptr应该将引用重新绑定到不同的对象.

所以如果你不想要这个,为什么你甚至通过引用存储删除器?

参考文献的浅层副本甚至意味着什么?在C++中没有这样的东西.如果您不想要引用语义,请不要使用引用.

如果你真的想这样做,那么解决方案很简单:为你的删除器定义赋值,不要改变计数器:

CountingDeleter&
operator=(const CountingDeleter&) noexcept
{ return *this; }
Run Code Online (Sandbox Code Playgroud)

或者,因为你真正关心的是计数器,而不是删除器,将计数器保留在删除器之外,不要使用引用删除器:

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);
Run Code Online (Sandbox Code Playgroud)

  • 如果你不希望你的引用像reference_那样更方便. (2认同)
  • 无论它是否"有用",它是否以这种方式工作,如果不这样做将会非常混乱. (2认同)

Max*_*kin 2

拥有引用数据成员通常会导致令人惊讶的结果,因为分配给引用具有非值语义,因为无法重新分配引用来引用另一个对象。基本上,引用数据成员会破坏赋值运算符的语义。

使用指针成员可以解决这个问题。或者,使用std::reference_wrapper<>std::ref()


为什么它执行引用存储的删除器的深复制而不是浅复制?

它执行成员明智的复制。如果要复制的值是指针,那么它恰好是浅复制。