如何将unique_ptr参数传递给构造函数或函数?

我很清楚如何处理构造函数或函数中的 unique_ptr 参数 . 考虑这个引用自身的类:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

这是我应该如何编写 unique_ptr 参数的函数?

我需要在调用代码中使用 std::move 吗?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?

回答(6)

2 years ago

以下是将唯一指针作为参数的可能方法,以及它们的相关含义 .

(A)按 Value 计算

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

为了让用户调用它,他们必须执行以下操作之一:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

按值获取唯一指针意味着您将指针的所有权转移到相关的函数/对象/等 . 构建 newBase 后, nextBase 保证为空 . 你甚至不再指向它了.1308489_ t . 没了 .

这是确保的,因为我们按值获取参数 . std::move 实际上并没有移动任何东西;这只是一个花哨的演员 . std::move(nextBase) 返回 Base&& ,它是 nextBase 的r值引用 . 就是这样 .

因为 Base::Base(std::unique_ptr<Base> n) 通过值而不是r值引用来获取其参数,所以C将自动为我们构造临时值 . 它从 Base&& 创建一个 std::unique_ptr<Base> ,我们通过 std::move(nextBase) 给出了该函数 . 正是这个临时的构造实际上将值从 nextBase 移动到函数参数 n 中 .

(B)通过非const l值引用

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

必须在实际的l值(命名变量)上调用它 . 它不能像这样临时调用:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

其含义与非const引用的任何其他用法的含义相同:该函数可能会或可能不会声明指针的所有权 . 鉴于此代码:

Base newBase(nextBase);

无法保证 nextBase 为空 . 它可能是空的;它可能不会 . 这真的取决于 Base::Base(std::unique_ptr<Base> &n) 想要做什么 . 正因为如此,它才会发生;你必须阅读实现(或相关文档) .

因此,我不建议将其作为界面 .

(C)通过const l-value引用

Base(std::unique_ptr<Base> const &n);

我没有显示实现,因为你无法从 const& 移动 . 通过传递 const& ,您说该函数可以通过指针访问 Base ,但它不能将它存储在任何地方 . 它不能声称拥有它 .

这可能很有用 . 不一定适合你的具体情况,但总是很高兴能够指派一个指针,并且知道他们不能(不破坏C的规则,就像没有抛弃 const )声称拥有它 . 他们无法存储它 . 他们可以将它传递给其他人,但其他人必须遵守相同的规则 .

(D)通过r值参考

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

这与“非常量l值参考”情况或多或少相同 . 差异是两件事 .

  • 你可以传递一个临时的:
Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  • 传递非临时参数时必须使用 std::move .

后者确实是问题所在 . 如果你看到这一行:

Base newBase(std::move(nextBase));

您有一个合理的期望,在此行完成后, nextBase 应为空 . 它本应该被移走 . 毕竟,你有 std::move 坐在那里,告诉你运动已经发生 .

问题是它没有 . 不能保证不会被移走 . 它可能已被移除,但您只能通过查看源代码来了解 . 你无法从功能签名中分辨出来 .

建议

  • (A) By Value: 如果您的意思是声明 unique_ptr 的所有权,请按值取值 .

  • (C) By const l-value reference: 如果您的意思是函数只在函数执行期间使用 unique_ptr ,请按 const& . 或者,将 &const& 传递给指向的实际类型,而不是使用 unique_ptr .

  • (D) By r-value reference: 如果某个功能可能会或可能不会声明所有权(取决于内部代码路径),请通过 && 获取 . 但我强烈建议尽可能不要这样做 .

如何操作unique_ptr

你无法复制 unique_ptr . 你只能移动它 . 正确的方法是使用 std::move 标准库函数 .

如果你按 unique_ptr 取值,你可以自由地移动它 . 但是由于 std::move ,运动实际上并没有发生 . 请采取以下声明:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

这真的是两个陈述:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(注意:以上代码在技术上并不适用编译,因为非临时r值引用实际上不是r值 . 它仅用于演示目的) .

temporary 只是 oldPtr 的r值引用 . 它在 newPtr 的构造函数中发生了运动 . unique_ptr 的移动构造函数(一个自带 && 的构造函数)是实际移动的内容 .

如果您有 unique_ptr 值并且想要将其存储在某处,则必须使用 std::move 来执行存储 .

2 years ago

让我试着说明将指针传递给对象的不同可行模式,这些对象的内存由 std::unique_ptr 类模板的实例管理;它也适用于较旧的 std::auto_ptr 类模板(我相信它允许所有使用该唯一指针的功能,但是除此之外还可以接受可修改的左值,而不需要调用 std::move ),并且在某种程度上也可以 std::shared_ptr .

作为讨论的具体示例,我将考虑以下简单列表类型

struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }

此类列表的实例(不允许与其他实例共享部分或为循环)完全由持有初始 list 指针的人拥有 . 如果客户端代码知道它存储的列表永远不会为空,那么它也可以选择直接存储第一个 node 而不是 list . 不需要定义 node 的析构函数:由于其字段的析构函数是自动调用的,因此一旦初始指针或节点的生命周期结束,智能指针析构函数将递归删除整个列表 .

这种递归类型有机会讨论在智能指向普通数据的情况下不太明显的一些情况 . 此外,函数本身偶尔也会(递归地)提供客户端代码的示例 . list 的typedef当然偏向于 unique_ptr ,但是定义可以更改为使用 auto_ptrshared_ptr 而不需要更改为下面所述的内容(特别是关于异常安全性而不需要编写析构函数) .

传递智能指针的模式

模式0:传递指针或引用参数而不是智能指针

如果您的功能与所有权无关,那么这是首选方法:根本不要使用智能指针 . 在这种情况下,您的函数不需要担心谁拥有所指向的对象,或者通过什么方式管理所有权,因此传递原始指针既非常安全,也是最灵活的形式,因为无论所有权如何,客户端都可以始终产生一个原始指针(通过调用 get 方法或来自运算符 & 的地址) .

例如,计算此类列表长度的函数不应该是 list 参数,而是一个原始指针:

size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }

保存变量 list head 的客户端可以将此函数称为 length(head.get()) ,而选择存储表示非空列表的 node n 的客户端可以调用 length(&n) .

如果指针保证为非null(由于列表可能为空,这不是这种情况),可能更喜欢传递引用而不是指针 . 如果函数需要更新节点的内容,而不添加或删除任何节点(后者将涉及所有权),它可能是非 const 的指针/引用 .

属于模式0类别的一个有趣案例是制作列表的(深层)副本;虽然执行此操作的功能当然必须转移它创建的副本的所有权,但它并不关心它正在复制的列表的所有权 . 所以可以定义如下:

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

这个代码值得仔细研究,关于它为什么要编译的问题(初始化列表中对 copy 的递归调用的结果绑定到 unique_ptr<node> 的移动构造函数中的rvalue引用参数,也就是 list ,当初始化时 next 生成的 node 的字段,以及关于为什么它是异常安全的问题(如果在递归分配过程中内存耗尽并且 new 的一些调用抛出 std::bad_alloc ,那么那时指向部分构造的列表的指针是匿名保存在为初始化列表创建的 list 类型的临时文件中,其析构函数将清除该部分列表) . 顺便说一句,人们应该抵制(如我最初所做的那样)被 p 替换的诱惑,毕竟在这一点上已知为null:一个人不能从(原始)指针构造一个智能指针到常量,即使它已知为null .

模式1:按值传递智能指针

将智能指针值作为参数的函数占用了立即指向的对象:调用者持有的智能指针(无论是在命名变量还是匿名临时)被复制到函数入口处的参数值和调用者的指针已变为空(在临时情况下,副本可能已被省略,但在任何情况下,调用者都无法访问指向的对象) . 我想将此模式称为 call by cash :呼叫者为所呼叫的服务付费,并且在呼叫后不会对所有权抱有任何幻想 . 为了清楚说明,如果智能指针保存在变量中(技术上,如果参数是左值),则语言规则要求调用者将参数包装在 std::move 中;在这种情况下(但不适用于下面的模式3),此函数按其名称建议,即将值从变量移动到临时,将变量保留为null .

对于被调用函数无条件地获取(窃取者)指向对象的所有情况,与 std::unique_ptrstd::auto_ptr 一起使用的此模式是将指针与其所有权一起传递的好方法,这避免了任何内存泄漏的风险 . 尽管如此,我认为只有极少数情况下,模式3中的模式3不会优于(模式1) . 因此,我将不提供此模式的使用示例 . (但请参阅下面模式3的 reversed 示例,其中注意到模式1至少也会这样做 . )如果函数接受的参数多于此指针,则可能会发生另外一个technical reason to avoid mode 1(带 std::unique_ptr )或 std::auto_ptr ):由于实际移动操作在通过表达式 std::move(p) 传递指针变量 p 时发生,因此不能假设 p 在评估其他参数(评估的顺序未指定)时保持有用的值,这可能导致微妙的错误;相反,使用模式3确保在函数调用之前不会发生从 p 的移动,因此其他参数可以通过 p 安全地访问值 .

当与 std::shared_ptr 一起使用时,这种模式很有意思,它使用单个函数定义,它允许调用者选择是否在创建函数使用的新共享副本时为自己保留指针的共享副本(这发生在提供了lvalue参数;在调用时使用的共享指针的复制构造函数增加了引用计数),或者只是给函数一个指针的副本而不保留一个或触及引用计数(这在提供rvalue参数时发生,可能是在 std::move 的召唤中包裹的左值 . 例如

void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container

void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
  f(p); // lvalue argument; store pointer in container but keep a copy
  f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
  f(std::move(p)); // xvalue argument; p is transferred to container and left null
}

通过单独定义 void f(const std::shared_ptr<X>& x) (对于左值情况)和 void f(std::shared_ptr<X>&& x) (对于右值情况),可以实现相同的功能,只是第一个版本调用复制语义(使用 x 时使用复制构造/赋值)但第二个版本不同版本移动语义(改为编写 std::move(x) ,如示例代码中所示) . 因此,对于共享指针,模式1可用于避免某些代码重复 .

模式2:通过(可修改的)左值引用传递智能指针

这里的函数只需要一个可修改的智能指针引用,但没有说明它将用它做什么 . 我想称这种方法为 call by card :来电者通过提供信用卡号码确保付款 . 该引用可用于获取指向对象的所有权,但它不必具有 . 此模式需要提供可修改的左值参数,对应于函数的所需效果可能包括在参数变量中保留有用值的事实 . 它希望传递给这样一个函数的具有rvalue表达式的调用者将被强制将其存储在一个命名变量中以便能够进行调用,因为该语言仅提供对常量左值引用的隐式转换(指临时) )来自右边 . (与 std::move 处理的相反情况不同,从 Y&&Y& 的转换,智能指针类型 Y 是不可能的;尽管如此,如果真的需要,可以通过简单的模板函数获得此转换;请参阅https://stackoverflow.com/a/24868376/1436796) . 对于被调用函数打算无条件取得对象所有权,从参数中窃取的情况,提供左值参数的义务是给出错误的信号:在调用之后变量没有任何有用的值 . 因此,模式3在我们的功能中提供了相同的可能性,但要求呼叫者提供右值,应该首选这种用法 .

但是,模式2有一个有效的用例,即可以修改指针的函数,或者以涉及所有权的方式指向的对象 . 例如,将节点作为前缀 list 的函数提供了此类用法的示例:

void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }

显然,强制调用者使用 std::move 是不可取的,因为他们的智能指针在调用之后仍然拥有一个定义良好且非空的列表,尽管它与之前不同 .

同样有趣的是观察如果 prepend 调用由于缺少空闲内存而失败会发生什么 . 然后 new 调用将抛出 std::bad_alloc ;在这个时间点,因为没有 node 可以被分配,所以确定来自 std::move(l) 的传递的右值参考(模式3)还没有被盗,因为这样做是为了构造 nodenext 字段,该字段未能成功分配 . 因此,当抛出错误时,原始智能指针 l 仍保留原始列表;该列表将被智能指针析构函数正确销毁,或者如果 l 由于足够早的 catch 子句而存活,它仍将保留原始列表 .

这是一个建设性的例子;眨眼间this question还可以给出一个更具破坏性的例子来删除包含给定值的第一个节点,如果有的话:

void remove_first(int x, list& l)
{ list* p = &l;
  while ((*p).get()!=nullptr and (*p)->entry!=x)
    p = &(*p)->next;
  if ((*p).get()!=nullptr)
    (*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next); 
}

这里的正确性再次非常微妙 . 值得注意的是,在最终语句中,在 reset (隐式)销毁该节点(当它破坏由 p 持有的旧值时)之前,在要删除的节点内保存的指针 (*p)->next 被取消链接(通过 release ,返回指针但使原始值为空) . ),确保当时只有一个节点被销毁 . (在注释中提到的替代形式中,这个时间将留给 std::unique_ptr 实例 list 的移动赋值运算符的实现内部;标准说20.7.1.2.3; 2该运算符应该“作为”如果通过调用 reset(u.release()) “,那么时间也应该是安全的 . ”

请注意,对于始终非空列表存储本地 node 变量的客户端无法调用 prependremove_first ,这是正确的,因为给定的实现不适用于此类情况 .

模式3:通过(可修改的)右值参考传递智能指针

这是仅仅获取指针所有权时使用的首选模式 . 我想称这种方法为 call by check :来电者必须接受放弃所有权,就像提供现金一样,通过签署支票,但实际提款被推迟,直到被叫功能实际上盗窃指针(就像使用模式2时那样) . "signing of the check"具体地意味着调用者必须在 std::move 中包装一个参数(如在模式1中),如果它是一个左值(如果它是一个右值,"giving up ownership"部分很明显并且不需要单独的代码) .

请注意,技术模式3的行为与模式2完全相同,因此被调用的函数不必承担所有权;但是我会坚持认为,如果所有权转移存在任何不确定性(在正常使用中),模式2应该优先于模式3,因此使用模式3隐含地向呼叫者发出信号,即他们放弃所有权 . 有人可能反驳说,只有模式1参数传递真正表示强制失去对所有者的所有权 . 但是如果客户对被调用函数的意图有任何疑问,她应该知道被调用函数的规范,这应该消除任何疑问 .

很难找到一个涉及我们使用模式3参数传递的 list 类型的典型示例 . 将列表 b 移动到另一个列表 a 的末尾是一个典型示例;但是 a (幸存并保存操作的结果)最好使用模式2:

void append (list& a, list&& b)
{ list* p=&a;
  while ((*p).get()!=nullptr) // find end of list a
    p=&(*p)->next;
  *p = std::move(b); // attach b; the variable b relinquishes ownership here
}

模式3参数传递的纯粹示例是以下列表(及其所有权),并以相反的顺序返回包含相同节点的列表 .

list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
  list result(nullptr);
  while (p.get()!=nullptr)
  { // permute: result --> p->next --> p --> (cycle to result)
    result.swap(p->next);
    result.swap(p);
  }
  return result;
}

可以在 l = reversed(std::move(l)); 中调用此函数将列表反转为自身,但反转列表也可以不同方式使用 .

这里的参数立即被移动到局部变量以提高效率(可以直接在 p 的位置使用参数 l ,但是每次访问它都会涉及额外的间接级别);因此,模式1参数传递的差异是最小的 . 事实上,使用该模式,参数可以直接作为局部变量,从而避免初始移动;这只是一般原则的一个实例,如果通过引用传递的参数仅用于初始化局部变量,那么也可以将值传递给它,并将该参数用作局部变量 .

使用模式3似乎是标准所倡导的,正如所有提供的库函数使用模式3转移智能指针的所有权这一事实所证明的 . 一个特别令人信服的例子是构造函数 std::shared_ptr<T>(auto_ptr<T>&& p) . 该构造函数使用(在 std::tr1 中)来获取可修改的左值引用(就像 auto_ptr<T>& 复制构造函数一样),因此可以使用 inside 中的 auto_ptr<T> 左值 p 来调用,之后 p 已重置为null . 由于参数传递中从模式2到3的变化,现在必须将此旧代码重写为 std::shared_ptr<T> q(std::move(p)) ,然后继续工作 . 我理解委员会不喜欢这里的模式2,但是他们可以选择更改为模式1,通过定义 std::shared_ptr<T>(auto_ptr<T> p) 代替,他们可以确保旧代码无需修改即可工作,因为(与独特指针不同)自动指针可以静默地取消引用一个值(指针对象本身在进程中被重置为null) . 显然委员会更喜欢在模式1上提倡模式3,他们选择主动破坏现有代码,而不是使用模式1,即使已经弃用了 .

何时优先选择模式3而不是模式1

模式1在许多情况下是完全可用的,并且在假设所有权将采取将智能指针移动到局部变量的形式的情况下可能优于模式3,如上面的示例中那样 . 但是,在更一般的情况下,我可以看到两种理由更喜欢模式3:

  • 传递引用比创建临时和nix旧指针(处理现金有点费力)略高一些;在某些情况下,指针可以在实际被盗之前多次传递给另一个函数 . 这种传递通常需要写入 std::move (除非使用模式2),但请注意,这只是一个实际上没有做任何事情的演员(特别是没有解除引用),所以它附加零成本 .

  • 是否可以想象任何事物在函数调用的开始和它(或某些包含的调用)实际将指向的对象移动到另一个数据结构之间抛出异常(并且该异常尚未在函数内部捕获)当使用模式1时,智能指针引用的对象将在 catch 子句处理异常之前被销毁(因为函数参数在堆栈展开期间被破坏),但在使用模式3时则不然 . 在这种情况下,调用者可以选择恢复对象的数据(通过捕获异常) . 请注意,此处的模式1不会导致内存泄漏,但可能导致程序无法恢复的数据丢失,这也可能是不合需要的 .

返回智能指针:始终按值

总结一下关于返回智能指针的一个词,大概指向一个为调用者创建的对象 . 这与将指针传递给函数的情况不同,但为了完整性,我想坚持在这种情况下 always return by value (并且不要在 return 语句中使用 std::move ) . 没有人想要获得可能刚刚被修复的指针的引用 .

2 years ago

是的,如果你在构造函数中使用 unique_ptr by值,则必须这样做 . 明确是一件好事 . 由于 unique_ptr 是不可复制的(私有拷贝ctor),你写的内容应该会给你一个编译器错误 .

2 years ago

Edit: 这个答案是错误的,尽管严格来说代码是有效的 . 我只是把它留在这里因为它下面的讨论太有用了 . 这个答案是我上次编辑时给出的最佳答案:How do I pass a unique_ptr argument to a constructor or a function?

::std::move 的基本思想是那些传递给你的人应该用它来表达他们知道他们所传入的 unique_ptr 将失去所有权的知识 .

这意味着您应该在方法中使用 unique_ptr 的右值引用,而不是 unique_ptr 本身 . 这无论如何都行不通,因为传入一个普通的旧 unique_ptr 将需要制作一个副本,并且在 unique_ptr 的接口中明确禁止 . 有趣的是,使用命名的右值引用会再次将其转换为左值,因此您还需要使用 ::std::move inside .

这意味着您的两个方法应如下所示:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

那么使用这些方法的人会这样做:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

如您所见, ::std::move 表示指针将在最相关且最有帮助的地方失去所有权 . 如果这种情况发生无形,那么使用你的 class 的人会因为没有明显的理由而突然失去所有权将会非常混乱 .

2 years ago

Base(Base::UPtr n):next(std::move(n)) {}

应该好多了

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

void setNext(Base::UPtr n)

应该

void setNext(Base::UPtr&& n)

与同一个身体 .

而且...... evt 中的 evt 是什么?

2 years ago

到最高投票的答案 . 我更喜欢通过右值参考传递 .

我理解通过右值引用传递的问题可能会导致什么问题 . 但是让我们把这个问题分成两个方面:

  • 对于来电者:

我必须编写代码 Base newBase(std::move(<lvalue>))Base newBase(<rvalue>) .

  • for callee:

库作者应该保证,如果想要拥有所有权,它实际上会将unique_ptr移动到初始化成员 .

就这样 .

如果你通过右值引用,它只会调用一个“move”指令,但是如果按值传递,它就是两个 .

是的,如果图书馆作者不是这方面的专家,他可能不会移动unique_ptr来初始化成员,但这是作者的问题,而不是你 . 无论它通过值或右值参考传递什么,您的代码都是一样的!

如果你正在编写一个库,现在你知道你应该保证它,所以只需这样做,通过rvalue引用是一个比值更好的选择 . 使用您库的客户只会编写相同的代码 .

现在,为你的问题 . 如何将unique_ptr参数传递给构造函数或函数?

你知道什么是最好的选择 .

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html