如何将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?
233556 次浏览

是的,如果你在构造函数中按值取unique_ptr,你就必须这样做。明确是件好事。由于unique_ptr是不可复制的(私有复制ctor),你所写的应该会给你一个编译器错误。

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)

同样的身体。

和…evthandle()中是什么??

这个答案是错误的,即使,严格地说,代码工作。我把它留在这里只是因为下面的讨论太有用了。另一个答案是我上次编辑这个时给出的最佳答案:如何将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在没有明显原因的情况下突然失去所有权将是非常混乱的。

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

(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-value引用,所以c++会自动为我们构造一个临时变量。它从我们通过std::move(nextBase)赋予函数的Base&&创建了一个std::unique_ptr<Base>。这个临时函数的构造实际上是移动nextBase的值转换到函数实参n中。

(B)通过非const l-value引用

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

这必须在一个实际的l值(一个命名变量)上调用。它不能像这样调用temporary:

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

这或多或少与“通过非const l-value引用”的情况相同。区别在于两点。

  1. 可以传递一个临时的:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
    
  2. You must use std::move when passing non-temporary arguments.

The latter is really the problem. If you see this line:

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

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

问题是,事实并非如此。它不是要被移动的保证。它五月已经从移动,但你只能通过查看源代码知道。你不能只从函数签名来判断。

建议

  • (A)按价值计算:如果你想让一个函数声明unique_ptr所有权,请按值取它。
  • (C)通过const l-value引用:如果你的意思是让一个函数在该函数执行期间简单地使用unique_ptr,则通过const&获取它。或者,将&const&传递给实际指向的类型,而不是使用unique_ptr
  • (D) 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::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,而不是listnode不需要定义析构函数:因为它的字段的析构函数是自动调用的,一旦初始指针或节点的生命周期结束,整个列表将被智能指针析构函数递归删除

这种递归类型使我们有机会讨论一些在指向普通数据的智能指针中不太可见的情况。此外,函数本身偶尔也会(递归地)提供客户端代码示例。list的类型定义当然偏向于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).

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

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

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

这段代码值得仔细观察,这两个问题都在于它为什么会编译(初始化器列表中对copy的递归调用的结果在初始化生成的nodenext字段时绑定到unique_ptr<node>的move构造函数中的右值引用参数,也就是list),以及为什么它是异常安全的问题(如果在递归分配过程中内存耗尽,而某个new的调用抛出了std::bad_alloc,这时,指向部分构造的列表的指针被匿名保存在为初始化器列表创建的list类型的临时对象中,其析构函数将清理该部分列表)。顺便说一下,应该抵制用p替换第二个nullptr的诱惑(就像我最初做的那样),毕竟在这一点上,p是已知的空值:不能从(原始)指针unique_ptr<node>0构造智能指针,即使已知它是空值。

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

一个以智能指针值作为参数的函数立即占有了所指向的对象:调用者持有的智能指针(无论是在命名变量中还是匿名临时变量中)在函数入口处被复制到参数值中,调用者的指针已变为空(在临时变量中,副本可能已被省略,但在任何情况下,调用者都失去了对所指向对象的访问)。我想将此模式称为现金通话:调用者预先为所调用的服务付费,并且在调用后可以对所有权没有幻想。为了更清楚地说明这一点,如果智能指针保存在变量中(技术上,如果参数是左值),语言规则要求调用者将参数包装在std::move中;在这种情况下(但不是下面的模式3),这个函数做的就是它的名字所暗示的,即将变量的值从变量移动到临时变量,让变量为空。

对于被调用函数无条件地获得(窃取)指向对象的所有权的情况,此模式与std::unique_ptrstd::auto_ptr一起使用是传递指针及其所有权的好方法,从而避免了任何内存泄漏的风险。尽管如此,我认为只有极少数情况下,下面的模式3并不比模式1更受欢迎。由于这个原因,我将不提供这种模式的使用示例。(但请参阅下面模式3的reversed示例,其中指出模式1至少也可以做到这一点。)如果函数接受的参数不仅仅是这个指针,可能会出现额外的std::auto_ptr0(带有std::unique_ptrstd::auto_ptr):由于实际的移动操作是在通过表达式std::move(p)传递指针变量p时发生的,因此不能假设p在计算其他参数时持有有用的值(计算的顺序未指定),这可能会导致微妙的错误;相比之下,使用模式3确保在函数调用之前不会从p中移动,因此其他参数可以安全地通过p访问值。

当与std::shared_ptr一起使用时,此模式的有趣之处在于,对于单个函数定义,它允许调用者选择是否为自己保留指针的共享副本,同时创建一个新的共享副本供函数使用(这种情况发生在提供左值参数时;在调用中使用的共享指针的复制构造函数会增加引用计数),或者只给函数一个指针的副本,而不保留一个指针或不更改引用计数(这种情况发生在提供右值参数时,可能是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处理的相反情况不同,使用智能指针类型YY&&转换到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子句而幸存下来,它将仍然保存原始列表。

这是一个有建设性的例子;对于这个问题,也可以给出更具破坏性的例子,即删除包含给定值的第一个节点(如果有的话):

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)->next被解除链接(通过release,它返回指针,但使原始值为空)之前 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参数传递才真正标志着强制向调用者失去所有权。但是,如果客户端对被调用函数的意图有任何怀疑,她应该知道被调用函数的规范,这应该消除任何怀疑。

令人惊讶的是,很难找到涉及list类型使用模式3参数传递的典型示例。将一个列表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>&复制构造函数一样),因此可以像在std::shared_ptr<T> q(p)中一样使用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)来代替,他们可以确保旧代码无需修改就能工作,因为(不像唯一指针)自动指针可以无声地解引用到一个值(指针对象本身在进程中被重置为空)。显然,委员会更喜欢提倡模式3而不是模式1,他们选择std::tr10而不是使用模式1,即使对于一个已经被弃用的用法。

什么时候更喜欢模式3而不是模式1

模式1在许多情况下是完全可用的,并且在假设所有权以将智能指针移动到局部变量的形式(如上面的reversed示例)的情况下可能比模式3更可取。然而,我可以看到在更一般的情况下更喜欢模式3的两个原因:

  • 传递一个引用比创建一个临时指针和删除旧指针(处理现金有点费力)要稍微有效一些;在某些情况下,在实际窃取指针之前,指针可能会被多次传递给另一个函数。这样的传递通常需要写入std::move(除非使用模式2),但请注意,这只是一个强制转换,实际上不做任何事情(特别是没有解引用),因此它的附加代价为零。

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

返回一个智能指针:总是按值

概括地说,返回是一个智能指针,它可能指向一个创建供调用者使用的对象。这与将指针传递给函数并不是一个真正的情况,但为了完整性,我想坚持在这种情况下总是按值返回(和return语句中的不要使用 std::move)。没有人想让参考指向一个可能刚刚被禁用的指针。

投票最多的答案。我更喜欢通过右值引用传递。

我理解传递右值引用可能导致的问题。但是让我们把这个问题一分为二:

  • 调用者:

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

  • 被:

标准库作者应该保证,如果它想拥有所有权,它会实际移动unique_ptr成员来初始化。

这是所有。

如果传递右值引用,它将只调用一个“move”指令,但如果传递值,它将调用两个。

是的,如果库作者不是这方面的专家,他可能不会移动unique_ptr来初始化成员,但这是作者的问题,而不是你。无论它是通过值还是右值引用传递,你的代码都是一样的!

如果您正在编写一个库,现在您知道应该保证它,那么就这样做吧,传递右值引用比传递值是更好的选择。客户端谁使用你的库将只是编写相同的代码。

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

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

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

tl;dr:不要这样使用unique_ptr

我相信您正在制造一个可怕的混乱——对于那些需要阅读您的代码、维护它以及可能需要使用它的人来说。

如果你有公开的unique_ptr成员,只接受unique_ptr构造函数参数。

unique_ptr的所有权包装原始指针&生命周期管理。它们非常适合本地化使用——但对于接口来说并不好,实际上也不是有意的。想接口吗?记录你的新类的所有权,并让它获得原始资源;或者,在指针的情况下,使用核心指导方针中建议的owner<T*>

只有当你的类的目的是保存unique_ptr,并让其他人这样使用这些unique_ptr时,你的构造函数或方法才有理由接受它们。

不要暴露你在内部使用unique_ptrs的事实。

对列表节点使用unique_ptr是一个非常详细的实现细节。实际上,在我看来,让你的类列表机制的用户直接使用裸列表节点(自己构造它并将它提供给你)并不是一个好主意。我不需要形成一个新的list-node-它也是一个list-来添加一些东西到你的列表中-我应该只是通过值传递有效负载,通过const左值ref和/或右值ref。然后你处理它。对于拼接列表,同样是value, const lvalue和/或rvalue。