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

我新移动C ++ 11中的语义,我不知道如何处理构造函数或函数中的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? 

以下是将独特的指针作为参数的可能方式,以及相关的含义。

(A)按价值

 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保证为 。 你不拥有这个对象,而且你甚至没有指针了。 它消失了。

这是确保,因为我们采取的价值参数。 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的值nextBase函数参数n

(B)由非常量值引用

 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)由常量l值引用

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

我不显示一个实现,因为你不能从一个const& 。 通过传递一个const& ,你就是说这个函数可以通过指针访问Base ,但是它不能把它存储在任何地方。 它不能要求它的所有权。

这可能是有用的。 不一定是针对你的具体情况,但是能够向某人传递一个指针并且知道他们不能 (不违反C ++的规则,就像没有强制转换const )声明拥有它是一件好事。 他们不能存储它。 他们可以把它传递给其他人,但其他人必须遵守相同的规则。

(D)由r值参考

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

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

  1. 可以通过一个临时的:

     Base newBase(std::unique_ptr<Base>(new Base)); //legal now.. 
  2. 传递非临时参数时, 必须使用std::move

后者真的是这个问题。 如果你看到这一行:

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

你有一个合理的期望,在这一行完成之后, nextBase应该是空的。 它应该已经被移出。 毕竟,你有那个std::move坐在那里,告诉你发生了移动。

问题是没有。 不保证被移出。 它可能已经被移除,但只有通过查看源代码才能知道。 你不能只从功能签名。

建议

  • (A)通过值:如果你的意思是声明一个unique_ptr 所有权的函数,请把它作为值。
  • (C)通过const l-value的引用:如果你的意思是一个函数在函数的执行过程中简单的使用unique_ptr ,那就用const&来代替。 或者,将一个&或者const&传递给指向的实际类型,而不是使用unique_ptr
  • (四)由r值引用:如果一个函数可能或不可以声明所有权(取决于内部代码路径),然后把它与&& 。 但我强烈建议不要这样做,只要有可能。

如何操作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来执行存储。

让我尝试说明将指针传递给内存由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的析构函数:由于自动调用其字段的析构函数,一旦初始指针或节点的生命周期结束,整个列表将由智能指针析构函数递归地删除。

这个递归类型给出了一些机会来讨论在智能指针指向普通数据的情况下不太明显的情况。 此外,函数本身也偶尔提供(递归)一个客户端代码的例子。 listdef的typedef当然是偏向unique_ptr ,但是定义可以改为使用auto_ptr或者shared_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)

如果指针保证是非空的(在这里不是这样,因为列表可能是空的),可能更喜欢传递引用而不是指针。 如果函数需要更新节点的内容,而不添加或删除任何节点(后者将涉及所有权),则可能是指向非const的指针/引用。

属于模式0类的一个有趣的案例是制作(深)复制的列表; 而一个这样做的函数当然必须转移它创建的副本的所有权,而不关心它正在复制的列表的所有权。 所以可以定义如下:

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

这个代码值得仔细看看,不管是为什么它编译的问题(在初始化列表中copy的递归调用的结果绑定到unique_ptr<node> ,也就是list的移动构造函数中的右值引用参数,当初始化生成的nodenext字段时),以及为什么它是异常安全的问题(如果在递归分配过程中内存耗尽并且有一些new std::bad_alloc调用被调用,那么当时一个指针到部分构造的列表被匿名保存在为初始化列表创建的类型list中,并且其析构函数将清除该部分列表)。 顺便说一句,我们应该抵制用p代替第二个nullptr (正如我最初所做的那样)的诱惑,毕竟在这一点上已知它是空的:不能从(原始)指针构造一个智能指针到常数 ,甚至当它被认为是空的。

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

一个将智能指针值作为参数的函数占据了马上指向的对象:调用者持有的智能指针(无论是在命名变量中还是在匿名临时中)都被复制到函数入口的参数值中,调用者指针已经变为空(在临时的情况下复制可能已经被消除,但是在任何情况下,调用者已经失去了对被指向的对象的访问)。 我想用现金打电话给这个模式:打电话给被叫的服务先付款,并且在通话结束后不会有关于所有权的幻想。 为了清楚std::move如果智能指针被保存在一个变量中(技术上,如果参数是一个左值),语言规则要求调用者将参数包装在std::move 。 在这种情况下(但不适用于下面的模式3),这个函数完成了它的名字,也就是将变量的值移动到一个临时变量中,并保留变量null。

对于被调用的函数无条件地取得指向对象的所有权的情况, std::unique_ptrstd::auto_ptr使用的这种模式是将指针与其所有权一起传递的一种好方法,这样可以避免内存泄漏。 尽管如此,我认为只有极少数情况下,模式3不会比模式1更受欢迎(因此略微)。为此,我将不提供这种模式的使用示例。 (但是请看下面模式3的reversed例子,其中表示模式1至少可以做到这一点)。如果函数的参数比这个指针多,那么可能会出现另外一个技术原因模式1 (使用std::unique_ptrstd::auto_ptr ):由于实际的移动操作是通过表达式std::move(p)传递指针变量p的,所以不能认为p保留有用的值评估其他论点(评估的顺序未指定),这可能导致微妙的错误; 相比之下,使用模式3确保在函数调用之前不会从p移动,所以其他参数可以通过p安全地访问一个值。

当与std::shared_ptr ,这种模式是有趣的,因为使用单个函数定义,它允许调用者选择是否为自己保留指针的共享副本,同时创建将由函数使用的新共享副本(this当提供左值参数时会发生;在调用时使用的共享指针的复制构造函数会增加引用计数),或者只给函数一个指针的副本而不保留一个或触及引用计数(当右值参数被提供,可能是一个包裹在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:通过(可修改)左值引用传递智能指针

这里的函数只需要对智能指针有一个可修改的引用,但并不指示它将如何处理。 我想打电话给这个方法来电卡 :来电者通过给一个信用卡号码来确保付款。 引用可以用来获取指向对象的所有权,但不一定。 这种模式需要提供一个可修改的左值参数,相应于该函数的所需效果可能包括在参数变量中留下一个有用的值。 一个具有右值表达式的调用者希望传递给这样的函数将被迫将其存储在命名变量中以便能够进行调用,因为该语言仅提供对常量左值引用的隐式转换(指的是临时)从右值。 (与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)还不能被窃取,就像构建next字段未能分配的node 。 所以原来的智能指针l在抛出错误时仍然保持着原来的列表。 该列表将被智能指针析构函数正确地销毁,或者如果由于足够早的catch子句而存在,它仍将保留原始列表。

这是一个建设性的例子。 对这个问题的一个眨眼,人们也可以给出更具破坏性的例子,删除包含给定值的第一个节点,如果有的话:

 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); } 

这里的正确性也很微妙。 值得注意的是,在最后的语句中,在被删除的节点内部的指针(*p)->nextreleaserelease ,它返回指针,但是使原来的空), reset 之前 (隐式地)销毁该节点p )所持有的旧价值,确保当时只有一个节点被销毁。 (在注释中提到的替代形式中,这个时间将留给std::unique_ptr实例list的移动赋值运算符的内部实现;标准说20.7.1.2.3; 2该运算符应该行动“就好像通过调用reset(u.release()) ,那么这里的时机也应该是安全的。)

请注意, prependremove_first不能由存储一个总是非空列表的本地node变量的客户端调用,正确的是,因为给定的实现不能用于这种情况。

模式3:通过(可修改的)右值引用传递一个智能指针

当简单地获取指针的所有权时,这是首选模式。 我想通过检查来调用这个方法:调用者必须接受放弃所有权,就好像提供现金一样,签名支票,但是实际的提款被推迟到被调用的函数实际上偷取指针为止(就像使用模式2 )。 “检查签名”具体意味着调用者必须在std::move包含一个参数(如模式1),如果它是一个左值(如果它是一个右值,“放弃所有权”部分是显而易见的,并且不需要单独的代码)。

请注意,技术上模式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)); 将列表反转成自身,但反转列表也可以不同地使用。

在这里,参数立即被移动到局部变量以提高效率(人们可以直接使用参数l来代替p ,但是每次访问都会涉及额外的间接级别)。 因此与模式1参数传递的差别是最小的。 实际上使用这种模式,这个论点可以直接作为局部变量,从而避免了最初的移动。 这只是一个普遍原理的一个实例,如果通过引用传递的参数仅用于初始化局部变量,那么可以通过值来传递它,并使用该参数作为局部变量。

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

何时比模式1更喜欢模式3

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

  • 传递引用比创建临时引用稍微有效一些(处理现金有些费力); 在某些情况下,在实际上被窃取之前,指针可以被多次不变地传递给另一个函数。 这样的传递通常需要编写std::move (除非使用模式2),但是请注意,这只是一个实际上没有做任何事情(特别是没有取消引用)的转换,所以它没有附加成本。

  • 应该可以想象的是,在函数调用的开始和它(或某个包含的调用)实际上将指向对象移动到另一个数据结构的位置之间引发异常(并且这个异常还没有被捕获到函数本身内部),那么当使用模式1时,由智能指针引用的对象将在catch子句可以处理异常之前被销毁(因为函数参数在堆栈展开期间被破坏),但在使用模式3时不会如此。后者给出调用者可以选择在这种情况下恢复对象的数据(通过捕获异常)。 请注意,这里的模式1 不会导致内存泄漏 ,但可能会导致程序不可恢复的数据丢失,这可能也是不可取的。

返回一个智能指针:始终按照价值

总结一句关于返回一个智能指针,大概指向为调用者创建的对象。 这并不是一个与将指针传入函数相比的情况,但为了完整性,我想坚持在这种情况下总是按值返回 (并且不要return语句中使用 std::move )。 没有人希望得到一个可能刚被否定的指针的引用

是的,你必须如果你在构造函数中通过值unique_ptr 。 明确性是一件好事。 由于unique_ptr是不可复制的(private copy ctor),所以你写的应该会给你一个编译器错误。

编辑:这个答案是错误的,即使严格来说,代码的作品。 我只是把它留在这里,因为它下面的讨论太有用了。 这另一个答案是我最后编辑这个时候给出的最佳答案: 我如何将unique_ptr参数传递给构造函数或函数?

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

这意味着你应该在你的方法中使用右值引用unique_ptr ,而不是unique_ptr本身。 这将无法正常工作,因为传入一个普通的旧unique_ptr将需要复制,并且在unique_ptr的接口中明确禁止。 有趣的是,使用一个名为右值的引用将它重新变回左值,所以你需要你的方法中使用::std::move

这意味着你的两个方法应该是这样的:

 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表示指针将在最相关且有帮助的地方失去所有权。 如果这种情况发生在无形的情况下,那么使用你的班级的人让objptr突然失去了所有权,这是非常混乱的。

 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) 

与同一个身体。

而且…什么是handle()