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

494

我对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?

相关链接:https://dev59.com/QXA75IYBdhLWcg3wy8Qi 和 https://dev59.com/TG035IYBdhLWcg3wVuh1 - R. Martinho Fernandes
2
你在一个空指针上调用b1->setNext,这不是一个分段错误吗? - balki
7个回答

996

以下是作为参数传递独占指针的可能方式以及它们的意义。

(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`的右值引用。仅此而已。
因为`Base::Base(std::unique_ptr n)`按值而非右值引用接收其参数,C ++将自动为我们构建一个临时对象。它从通过`std::move(nextBase)`提供给函数的`Base&&`创建了一个`std::unique_ptr`。正是这个临时对象的构造实际上将值从`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.

这个的含义与使用非const引用的任何其他含义相同:该函数可能会声称指针的所有权,也可能不会。给定以下代码:

Base newBase(nextBase);

不能保证nextBase为空。它可能为空,也可能不为空。这实际上取决于Base::Base(std::unique_ptr<Base> &n)想要做什么。因此,仅凭函数签名很难知道会发生什么,您必须阅读实现(或相关文档)。

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

(C) 通过const左值引用

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

我没有展示具体的实现,因为你无法从const&中移动。通过传递一个const&,你是在表明函数可以通过指针访问Base,但它无法将其存储在任何地方。它不能声明对其的所有权。

这可能很有用。不一定针对你的特定情况,但能够将指针交给他人,并知道他们无法(除非违反C++的规则,如不强制类型转换const)拥有它,这总是不错的。他们不能存储它。他们可以将其传递给其他人,但那些人必须遵守相同的规则。

(D)通过右值引用

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

这与“通过非 const 左值引用”情况基本相同。不同之处在于两点:

  1. 你可以传递临时变量:

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

  • 后者才是真正的问题。如果您看到这行代码:

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

    在这行代码结束后,您有一个合理的期望,即 nextBase 应该为空。毕竟,您使用了 std::move,告诉您已经发生了移动。

    问题是它可能没有被移动。不能仅从函数签名判断,它可能已经被移动,但只有通过查看源代码才能确定。

    建议

    • (A) 按值传递: 如果您希望函数声明对 unique_ptr 的所有权,请按值传递。
    • (C) 按 const 左值引用传递: 如果您希望函数仅在执行期间使用 unique_ptr,请按 const& 传递。或者,传递指向实际类型的 &const&,而不是使用 unique_ptr
    • (D) 按右值引用传递: 如果函数可能会或可能不会声明所有权(取决于内部代码路径),则请按 && 传递。但我强烈建议尽可能避免这样做。

    如何操作 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);
    

    (注:上述代码从技术上讲不会被编译通过,因为非临时的右值引用实际上并不是右值。此处仅用于演示目的。)

    temporary 只是对 oldPtr 的一个右值引用。移动发生在 newPtr 的构造函数中。unique_ptr 的移动构造函数(即接受自身类型的 && 参数的构造函数)才是真正进行移动的操作。

    如果您有一个 unique_ptr 值并希望将其存储在某个地方,您必须使用 std::move 进行存储。


    6
    @Nicol:但是 std::move 并没有给它的返回值命名。请记住,被命名的右值引用是左值。http://www.ideone.com/VlEM3 - R. Martinho Fernandes
    37
    我基本上同意这个答案,不过有一些注释。(1)我认为传递引用给const lvalue没有有效的用例:调用方可以使用指向const(裸)指针的引用来完成所有操作,甚至更好地使用指针本身[而且它不需要知道所有权是通过unique_ptr持有的;也许其他调用方需要相同的功能,但是它们持有一个shared_ptr]。(2)如果被调用的函数修改指针,例如从链表中添加或删除(列表拥有的)节点,则按lvalue引用调用可能很有用。 - Marc van Leeuwen
    10
    虽然你主张通过值传递而不是通过右值引用传递的论点有道理,但我认为标准本身总是通过右值引用传递unique_ptr值(例如在将它们转换为shared_ptr时)。这样做的理由可能是它稍微更有效率(不需要移动到临时指针),同时给调用者提供完全相同的使用权利(可以传递右值或通过std::move包装的左值,但不能传递裸露的左值)。 - Marc van Leeuwen
    22
    重申Marc所说,并引用Sutter(http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/)的话:“不要将const unique_ptr&用作参数;改用widget*代替。” - Jon
    22
    我们发现按值传递存在问题——移动操作发生在参数初始化期间,而该过程与其他参数评估的顺序无序(当然,在initializer_list中除外)。而接受右值引用则强制移动在函数调用后发生,因此在评估其他参数后再进行。因此,每当需要获取所有权时,应优先接受右值引用。 - Ben Voigt
    显示剩余18条评论

    73
    让我尝试说明传递指向由std::unique_ptr类模板管理内存的对象的不同可行模式;它也适用于旧的std::auto_ptr类模板(我相信它允许unique pointer所做的所有使用,但此外还将接受可修改的左值,而无需调用std::move,并且在某种程度上也适用于std::shared_ptr)。作为讨论的具体示例,我将考虑以下简单的列表类型。
    struct node;
    typedef std::unique_ptr<node> list;
    struct node { int entry; list next; }
    

    这种列表的实例(不能与其他实例共享部分或成为循环)完全由持有初始list指针的人拥有。如果客户端代码知道它存储的列表永远不会为空,它也可以选择直接存储第一个节点而不是列表。不需要定义node的析构函数:由于其字段的析构函数会自动调用,一旦初始指针或节点的生命周期结束,智能指针析构函数将递归地删除整个列表。
    这种递归类型提供了讨论在智能指针到纯数据的情况下不太明显的一些情况的机会。函数本身偶尔(递归地)也提供客户端代码的示例。当然,list的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(也就是list)的移动构造函数中的右值引用参数,用于初始化生成的node的next字段),也要考虑为什么它是异常安全的(如果在递归分配过程中内存不足并且某个new的调用抛出std::bad_alloc,则此时一个对部分构造列表的指针匿名地保存在为初始化列表创建的类型为list的临时变量中,并且它的析构函数将清理该部分列表)。顺便说一句,应该抵制将第二个nullptr替换为p(就像我最初所做的那样)的诱惑,因为毕竟在那一点上已知它为空:即使已知它为空,也不能从指向常量的(原始)指针构造智能指针。

    模式1:通过值传递智能指针

    一个以智能指针值为参数的函数会立即接管所指向的对象:在函数进入时,调用者持有的智能指针(无论是在命名变量中还是匿名临时变量中)会被复制到参数值中,而调用者的指针已经变成了空指针(在临时变量的情况下,复制可能已经被省略,但无论如何,调用者已经失去了对所指向对象的访问权限)。我想称这种模式为预付费调用:调用者事先支付服务费用,并且在调用后不能对所有权产生幻想。为了明确这一点,语言规则要求调用者在智能指针存储在变量中(技术上,如果参数是左值)时将其包装在std::move中;在这种情况下(但不适用于下面的第3种模式),该函数会按照其名称所示执行操作,即将值从变量移动到临时变量中,使变量为空。
    对于那些无条件地获取指向对象的所有权(窃取)的调用函数的情况,使用std::unique_ptrstd::auto_ptr与此模式一起使用是传递指针及其所有权的好方法,可以避免任何内存泄漏的风险。尽管如此,我认为只有极少数情况下,模式3不如模式1(稍微)优越。因此,我将不提供此模式的使用示例。(但请参见下文模式3的反转示例,其中指出模式1至少可以做得一样好。)如果函数除了这个指针之外还需要更多参数,则可能会发生technical reason to avoid mode 1(使用std::unique_ptrstd::auto_ptr)的情况:由于在通过表达式std::move(p)传递指针变量p时实际进行移动操作,不能假定在评估其他参数时p持有有用值(求值顺序未指定),这可能导致微妙的错误;相比之下,使用模式3确保在函数调用之前不会从p移动,因此其他参数可以安全地通过p访问值。
    使用std::shared_ptr时,此模式非常有趣,因为它允许调用方通过单个函数定义来选择是否保留指针的共享副本供自己使用,同时创建一个新的共享副本供函数使用(当提供lvalue参数时会发生这种情况;在调用时使用的共享指针的复制构造函数会增加引用计数),或者只是给函数一个指针的副本而不保留任何一个或触摸引用计数(当提供rvalue参数时会发生这种情况,可能是一个包装在std::move调用中的lvalue)。例如。
    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:通过(可修改的)左值引用传递智能指针

    这个函数只需要一个可修改的智能指针引用,但没有说明它将如何使用它。我想称呼这种方法为“刷卡调用”:调用者通过提供信用卡号来确保付款。这个引用可以用来拥有指向的对象,但不一定要这样做。此模式需要提供一个可修改的左值参数,对应于函数所需的期望效果可能包括在参数变量中留下有用的值。希望将rvalue表达式传递给这样一个函数的调用者将被迫将其存储在命名变量中才能进行调用,因为语言仅从rvalue提供隐式转换为常量左值引用(引用临时对象)。 (与std :: move处理的相反情况不同,从 Y && Y&的转换对于智能指针类型的 Y 是不可能的; 尽管如此,如果真的需要,可以通过一个简单的模板函数获得这种转换;请参见https://dev59.com/I4Hba4cB1Zd3GeqPU7mk#24868376)。对于那些打算无条件地获取对象并从参数中窃取的被调用函数,提供左值参数的义务会发出错误的信号:调用后该变量将没有有用的值。因此,应该优先考虑模式3,它在我们的函数内提供了相同的可能性,但要求调用者提供一个rvalue。
    然而,模式2有一个有效的用例,即可能以涉及所有权的方式修改指针或所指对象的函数。例如,将节点前缀到列表中的函数提供了这种用法的示例:
    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仍然持有原始列表;该列表将通过智能指针析构函数正确销毁,或者在catch子句足够早的情况下l应该幸存下来,它仍将持有原始列表。

    这是一个有建设性的例子;通过参考这个问题,我们也可以给出更具破坏性的例子,即删除包含给定值的第一个节点(如果存在):

    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())”一样操作,因此此时的时间也应是安全的。)

    请注意,对于始终非空的列表,存储本地node变量的客户端不能调用prependremove_first。这是正确的,因为所给出的实现对于这种情况无法工作。

    第三种模式:通过(可修改的)右值引用传递智能指针

    这是简单地接管指针的首选模式。我想称之为“检查调用”方法:调用者必须接受放弃所有权,就像提供现金一样,签署支票,但实际提款将延迟到被调用的函数实际窃取指针时(完全与使用模式2相同)。具体来说,“签署支票”意味着如果参数是左值,则调用者必须使用std::move将其包装起来(如模式1中所示),如果它是右值,则“放弃所有权”部分是显而易见的,不需要单独的代码。
    请注意,从技术上讲,模式3的行为与模式2完全相同,因此被调用的函数不必承担所有权;然而,如果存在任何关于所有权转移(在正常使用中)的不确定性,我会坚持认为应该优先选择使用模式2而不是模式3,这样使用模式3隐含地向调用者发出信号,告诉他们他们正在放弃所有权。有人可能会反驳说,只有模式1的参数传递真正向调用者发出了强制失去所有权的信号。但是,如果客户对被调用的函数的意图有任何疑问,她应该知道被调用的函数的规格,这应该消除任何疑虑。

    寻找一个使用模式3参数传递的典型示例,涉及我们的list类型,实际上是非常困难的。将列表b移动到另一个列表a的末尾是一个典型的例子; 但是,最好使用模式2来传递a(它将幸存并保存操作的结果):

    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
    }
    

    一种纯粹的第三模式参数传递的例子是,接受一个列表(及其所有权),并返回一个包含相同节点但顺序相反的列表。
    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>&复制构造函数一样)作为参数,并且因此可以使用auto_ptr<T>左值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)。显然,委员会更喜欢倡导模式3而不是模式1,他们选择积极破坏现有的代码,而不是即使对于已经过时的用法也使用模式1。

    何时优先使用模式3而不是模式1

    在许多情况下,模式1是完全可用的,并且在假设所有权采用将智能指针移动到本地变量的形式(如上面的reversed示例)的情况下,可能比模式3更受青睐。然而,在更一般的情况下,我可以看到优先选择模式3的两个原因:

    • 传递引用比创建临时变量并取消旧指针的效率稍高(处理缓存有些费力);在某些情况下,指针可能会在被实际窃取之前多次不变地传递给另一个函数。这样的传递通常需要编写 std::move(除非使用模式2),但请注意,这只是一种不执行任何操作的转换(特别是没有解引用),因此它没有额外的成本。

    • 如果在函数调用的开头和实际将所指对象移入其他数据结构(或某个包含调用)的点之间抛出异常,并且(该异常)未在函数内部捕获,则使用模式1时,在 catch 子句能够处理异常之前(因为在堆栈展开期间函数参数已被析构),智能指针引用的对象将被销毁,但使用模式3则不会发生这种情况,后者使调用方有可能在这种情况下恢复对象的数据(通过捕获异常)。请注意,此处的模式1不会导致内存泄漏,但可能会导致程序的数据不可恢复的损失,这也可能是不希望发生的。

    返回智能指针:始终按值返回

    关于返回一个智能指针,它可能指向为调用者创建的对象,这并不是将指针传递到函数中的可比情况。但是为了完整起见,我想坚持在这种情况下始终按值返回(并且不要return语句中使用std::move)。没有人想得到一个可能已经被取消的指针的引用


    1
    对于 Mode 0,将底层指针传递而不是 unique_ptr 可以得到 +1。虽然这与传递 unique_ptr 的问题略有偏差,但它很简单且避免了问题。 - Machta
    mode 1 here does not cause a memory leak” - 这意味着模式3会导致内存泄漏,这是不正确的。无论unique_ptr是否被移动或未移动,只要它在销毁或重新使用时仍然持有值,它就会很好地删除该值。 - rustyx
    1
    @RustyX:我看不出你是怎么推断出那个暗示的,而且我也从未想过要表达你所理解的意思。我的意思只是像其他地方一样,使用unique_ptr可以防止内存泄漏(因此在某种程度上实现了其契约),但是在这里(即使用模式1)可能会导致(在特定情况下)更加有害的事情,即数据丢失(指向值的破坏),这可以通过使用模式3来避免。 - Marc van Leeuwen
    @MarcvanLeeuwen,你写代码的方式不太易读,也许你可以进行编辑? - kingsjester
    @kingsjester 我已经忘记了我编写代码的确切方式,但我发现结果非常易读。除了一些单行语句外,我将独立的语句放在不同的行上,并保持了一致的缩进。 - Marc van Leeuwen
    @MarcvanLeeuwen 是的,{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; } 或者 { return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); } 都是可以理解的,但为什么要把所有内容都放在一行上呢? - kingsjester

    5

    编辑:此答案是错误的,即使代码严格执行也是如此。 我之所以将其保留在这里,只是因为其下面的讨论非常有用。我上次编辑时,这个回答是最好的答案。

    ::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 突然丧失所有权而没有明显的原因会非常困惑。


    3
    命名的右值引用是左值。 - R. Martinho Fernandes
    1
    补充我的上一个评论:这段代码无法编译。你仍然需要在构造函数和方法的实现中使用 std::move。即使通过值传递,调用者仍然必须使用 std::move 传递左值。主要区别在于,通过值传递的接口清楚地表明所有权将会丢失。请参见Nicol Bolas对另一个答案的评论。 - R. Martinho Fernandes
    @R.MartinhoFernandes:哦,有趣。我想那是有道理的。我本来以为你错了,但实际测试证明你是正确的。现在已经修复了。 - Omnifarious
    我收到错误信息:在构造函数‘Base::Base(Base::UPtr&&)’中: /usr/include/c++/4.5/bits/unique_ptr.h:207:7: 错误:删除函数‘std::unique_ptr<_Tp, _Tp_Deleter>::unique_ptr(const std::unique_ptr<_Tp, _Tp_Deleter>&) [with _Tp = Base, _Tp_Deleter = std::default_delete<Base>, std::unique_ptr<_Tp, _Tp_Deleter> = std::unique_ptr<Base>]’ - codablank1
    @codablank1:是的,给我点踩的人是正确的。我已经更新并修复了我的代码。另外,回答的那个人:https://dev59.com/p2sz5IYBdhLWcg3wHUEU#8114339 比我更正确。我应该删除我的答案,尽管它可以工作。 - Omnifarious
    显示剩余6条评论

    5

    如果你在构造函数中按值接收unique_ptr,那么你必须这样做。显式是一件好事情。由于unique_ptr是不可复制的(私有复制构造函数),你写的代码应该会产生编译错误。


    5

    总之,不要这样使用unique_ptr

    我认为你正在制造一团糟 - 对于那些需要阅读、维护和可能需要使用你的代码的人来说。

    只有当你有公开的unique_ptr成员时才使用unique_ptr构造函数参数。

    unique_ptr用于封装原始指针的所有权和生命周期管理。它们非常适合本地化使用 - 并不好,事实上也不打算用于接口。想要接口吗?将你的新类标记为获取所有权,并让它获取原始资源;或者,在指针的情况下,如核心指南中建议的那样使用owner<T*>

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

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

    对于列表节点使用unique_ptr实际上是一种实现细节。事实上,即使你允许使用者自己构建裸的列表节点并将其提供给你,这也不是一个好主意。我不应该需要形成一个新的既是列表节点又是列表的东西来添加到你的列表中 - 我只需传递有效负载 - 通过值、通过const lvalue引用和/或通过rvalue引用。然后你来处理它。对于拼接列表 - 同样如此,使用值、const lvalue和/或rvalue。


    2
    尽管关于语言的问题和能力有非常详细的答案,但我认为这个答案的重点非常重要。谢谢。 - Ricardo Cristian Ramirez

    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)
    

    具有相同的主体。

    那么,在 handle() 中,evt 是什么?


    4
    在这里使用std::forward没有任何好处:Base::UPtr&&始终是一个右值引用类型,而std::move将其作为右值传递。它已经被正确地转发了。 - R. Martinho Fernandes
    7
    我强烈反对。如果一个函数通过值传递unique_ptr,那么可以保证已经在这个新值上调用了移动构造函数(或者仅仅是传递了一个临时变量)。这_确保_用户拥有的unique_ptr变量现在为空。如果使用&&获取它,只有在你的代码调用移动操作时才会被清空。这样做可能导致用户拥有的变量没有被移动。这使得用户使用std::move时很容易出现疑问和困惑。使用std::move应该始终确保某些东西被_移动_了。 - Nicol Bolas
    @NicolBolas:你说得对。我会删除我的回答,因为虽然它可以工作,但你的观察是绝对正确的。 - Omnifarious

    0

    回答中得票最高的建议,我更喜欢通过右值引用传递。

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

    • 对于调用者:

    我必须编写代码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


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接