459

我是 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?
4

7 回答 7

937

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

(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)返回 a Base&&,它是对 的 r 值引用nextBase。这就是它所做的一切。

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

(B) 通过非常量左值引用

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

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

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

this 的含义与任何其他使用非常量引用的含义相同:函数可能会或可能不会声明指针的所有权。鉴于此代码:

Base newBase(nextBase);

不能保证nextBase是空的。它可能是空的;它可能不会。这真的取决于Base::Base(std::unique_ptr<Base> &n)想做什么。正因为如此,仅仅从函数签名上看会发生什么并不是很明显;您必须阅读实现(或相关文档)。

因此,我不建议将其作为接口。

(C) 通过 const l-value 引用

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

我没有展示一个实现,因为你不能const&. 通过传递 a 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) 按值:如果您的意思是让函数声明a 的所有权unique_ptr,请按值取它。
  • (C) 通过 const l-value 引用:如果您的意思是让函数在unique_ptr该函数执行期间简单地使用 ,请使用const&. 或者,将 a &or传递给const&指向的实际类型,而不是使用 a unique_ptr
  • (D) 通过 r-value 引用:如果一个函数可能会或可能不会声明所有权(取决于内部代码路径),那么将其通过&&. 但我强烈建议尽可能不要这样做。

如何操作 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-value 引用实际上不是 r-values。这里仅用于演示目的)。

temporary只是对 的 r 值引用oldPtr。它在运动发生的构造函数中。的移动构造函数(一个自身带有 a 的构造函数)是实际的移动。newPtrunique_ptr&&

如果你有一个unique_ptr值并且你想将它存储在某个地方,你必须使用它std::move来进行存储。

于 2011-11-13T21:32:58.480 回答
67

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

这种递归类型让我们有机会讨论一些在指向普通数据的智能指针的情况下不太明显的情况。此外,函数本身偶尔也会(递归地)提供客户端代码的示例。typedef forlist当然偏向于unique_ptr,但是可以将定义更改为 use 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可以调用此函数 as 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初始化next字段时生成node),以及为什么它是异常安全的问题(如果在递归分配过程中内存用完并且调用了newthrows std::bad_alloc,那么当时指向部分构造的列表的指针被匿名保存在临时类型中list为初始化列表创建,其析构函数将清理该部分列表)。顺便说一句,一个人应该抵制诱惑(就像我最初所做的那样)替换第二nullptrp,毕竟在那一点上知道它是空的:即使知道它是空的,也不能从(原始)指针构造一个智能指针到常量

方式一:按值传递智能指针

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

对于被调用函数无条件获取(窃取)指向对象的所有权的情况,这种模式与std::unique_ptrorstd::auto_ptr一起使用是传递指针及其所有权的好方法,这样可以避免任何内存泄漏的风险。尽管如此,我认为只有极少数情况下模式 3 不优于模式 1(非常轻微)。出于这个原因,我将不提供该模式的使用示例。(但请参见reversed下面模式 3 的示例,其中指出模式 1 至少也可以。)如果函数接受的参数比这个指针更多,则可能会出现另外一个技术原因来避免模式1(带std::unique_ptrstd::auto_ptr):因为实际的移动操作发生在传递指针变量时p通过表达式std::move(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::moveY&&to强制转换是不可能的;但是,如果确实需要,可以通过简单的模板函数获得这种转换;请参阅https://stackoverflow.com/a/24868376 /1436796)。对于被调用函数打算无条件地获取对象所有权的情况,从参数中窃取,提供左值参数的义务是给出错误的信号:调用后变量将没有有用的值。因此,模式 3 在我们的函数中提供了相同的可能性,但要求调用者提供一个右值,这种用法应该是首选。Y&Y

但是,模式 2 有一个有效的用例,即可以修改指针的函数,或者以涉及所有权的方式指向的对象。例如,将节点添加到 a 的函数list提供了这样一个使用示例:

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

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

同样有趣的是,如果prepend调用因缺少可用内存而失败,观察会发生什么。然后new调用将抛出std::bad_alloc;此时,由于 nonode可以分配,因此可以肯定传递的右值引用(模式 3)std::move(l)还不能被窃取,因为这样做是为了构造未能分配的next字段。node所以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是未链接的(by ,它返回指针但使原始空值),确保当时只有一个节点被销毁。(在评论中提到的替代形式中,这个时间将留给实例的移动赋值运算符的实现内部;标准说 20.7.1.2.3;2 该运算符应该“像调用“,这里的时间也应该是安全的。) resetpstd::unique_ptrlistreset(u.release())

请注意,prepend为始终非空列表remove_first存储局部变量的客户端不能调用nodeand ,这是正确的,因为给出的实现不适用于这种情况。

模式 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)。该构造函数使用 (in std::tr1) 获取可修改的左值引用(就像auto_ptr<T>&复制构造函数一样),因此可以使用in的auto_ptr<T>左值调用,之后将其重置为 null。由于在参数传递中从模式 2 更改为 3,现在必须重写此旧代码,然后才能继续工作。我知道委员会不喜欢这里的模式 2,但他们可以选择更改为模式 1,通过定义pstd::shared_ptr<T> q(p)pstd::shared_ptr<T> q(std::move(p))std::shared_ptr<T>(auto_ptr<T> p)相反,他们可以确保旧代码无需修改即可工作,因为(与唯一指针不同)自动指针可以被静默地取消引用到一个值(指针对象本身在过程中被重置为 null)。显然,与模式 1 相比,委员会更喜欢提倡模式 3,以至于他们选择主动破坏现有代码,而不是使用模式 1,即使是已经被弃用的用法。

何时更喜欢模式 3 而不是模式 1

模式 1 在许多情况下都非常有用,并且在假设所有权将采用将智能指针移动到局部变量的形式(reversed如上例中)的情况下可能比模式 3 更受欢迎。但是,我可以看到在更一般的情况下更喜欢模式 3 的两个原因:

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

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

返回智能指针:总是按值

总结一下返回一个智能指针,大概指向一个为调用者使用而创建的对象。这与将指针传递给函数并不能相提并论,但为了完整起见,我想坚持在这种情况下始终按值返回(并且不要 std::movereturn语句中使用)。没有人想获得对可能刚刚被取消的指针的引用。

于 2014-06-26T10:47:46.433 回答
5

编辑:这个答案是错误的,尽管严格来说,代码有效。我只是把它留在这里,因为它下面的讨论太有用了。这个其他答案是我上次编辑时给出的最佳答案:如何将 unique_ptr 参数传递给构造函数或函数?

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

这意味着您应该在方法中使用对 a 的右值引用unique_ptr,而不是 aunique_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如果这发生在不可见的情况下,那么使用您的课程的人会因为没有显而易见的原因而突然失去所有权,这将是非常令人困惑的。

于 2011-11-13T20:29:35.067 回答
4

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

于 2011-11-13T20:06:54.450 回答
3

tl; dr:不要unique_ptr像那样使用 's 。

我相信你搞得一团糟——对于那些需要阅读你的代码、维护它的人,可能还有那些需要使用它的人。

unique_ptr如果您有公开的unique_ptr成员,则仅采用构造函数参数。

unique_ptr为所有权和生命周期管理包装原始指针。它们非常适合本地化使用 - 不好,实际上也不打算用于接口。要界面吗?将您的新课程记录为所有权,并让它获得原始资源;或者,在指针的情况下,按照Core Guidelinesowner<T*>中的建议使用。

只有当你的类的目的是持有unique_ptr's,并让其他人使用这些unique_ptr's 时——只有这样,你的构造函数或方法才合理地使用它们。

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

用于unique_ptr列表节点在很大程度上是一个实现细节。实际上,即使您让类似列表机制的用户直接使用裸列表节点 - 自己构建它并将其提供给您 - 恕我直言,这也不是一个好主意。我不需要形成一个新的 list-node-which-is-also-a-list 来添加一些东西到你的列表中——我应该只传递有效负载——按值、通过 const lvalue ref 和/或通过 rvalue ref。然后你处理它。对于拼接列表 - 再次,value、const lvalue 和/或 rvalue。

于 2020-06-30T10:55:17.063 回答
0
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而且...里面有什么handle()

于 2011-11-13T20:08:30.490 回答
0

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

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

  • 对于来电者:

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

  • 对于被调用者:

图书馆作者应该保证如果它想要拥有所有权,它实际上会移动 unique_ptr 来初始化成员。

就这样。

如果你通过右值引用传递,它只会调用一个“移动”指令,但如果通过值传递,它是两个。

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

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

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

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

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

于 2018-05-11T03:35:27.473 回答