C++中unique_ptr参数传递

14

假设我有以下代码:

class B { /* */ };

class A {
    vector<B*> vb;
public:
    void add(B* b) { vb.push_back(b); }
};


int main() {

    A a;
    B* b(new B());
    a.add(b);
}

假设在这种情况下,所有的原始指针 B* 都可以通过 unique_ptr<B> 来处理。

令人惊讶的是,我没有找到如何使用 unique_ptr 转换此代码。经过几次尝试,我得出了以下编译代码:

class A {
    vector<unique_ptr<B>> vb;
public:
    void add(unique_ptr<B> b) { vb.push_back(move(b)); }
};


int main() {

    A a;
    unique_ptr<B> b(new B());
    a.add(move(b));
}
所以我的简单问题是:这样做的方式是否正确,特别是move(b)是唯一的方法吗?(我想到了右值引用,但我不完全理解它们。)
如果您有一个链接,其中包含我无法找到的关于移动语义、unique_ptr等的完整解释,请不要犹豫与我分享。
编辑根据http://thbecker.net/articles/rvalue_references/section_01.html,我的代码似乎没问题。
事实上,std::move只是一种语法糖。对于类X的对象x,move(x)就像是:
static_cast <X&&>(x)

需要这两个移动函数是因为转换为右值引用:

  1. 防止函数 "add" 通过值传递
  2. 使 push_back 使用 B 的默认移动构造函数

显然,如果我将我的 "add" 函数更改为通过引用传递(普通左值引用),则不需要在我的 main() 中使用第二个 std::move

虽然如此,我仍需要一些确认...


我认为这个链接对你可能有用:https://dev59.com/SnE85IYBdhLWcg3wOw_s - Kiril Kirov
@KirilKirov 谢谢,这个问题提供了有趣的链接(请参见编辑)。 - Bérenger
如果您更改了add的方式,就可以避免在主函数中使用std::move,但这样做是不好的,因为调用add的人没有查看add源代码就无法确定他们是否仍然拥有该对象。 - Puppy
@DeadMG 哎呀。我知道我会做一些傻事。谢谢。顺便说一下,我正在考虑通过 rvalue 引用传递参数,即 "add(B&& b)",以便在其实现中抑制 "move",但在这种情况下似乎是多余的。但是,这样做(通过 rvalue 引用而不是按值传递)我得到了相同的结果,特别是我无法抑制这个“move”。有什么原因吗? - Bérenger
因为所有的变量都是lvalues,包括那些引用rvalues的变量。 - fredoverflow
还非常有用的是:https://dev59.com/p2sz5IYBdhLWcg3wHUEU - kevinarpe
4个回答

11

我有点惊讶于这里没有很清楚和明确地回答,也没有任何我容易发现的地方。虽然我对这个东西还比较新,但我认为以下内容可以说出来。

情况是一个调用函数构建了一个unique_ptr<T>值(可能通过将结果从调用new的结果强制转换而来),并且想将其传递给一些将拥有该对象指针的函数(例如将其存储在数据结构中,如此处存储在vector中)。为了表明调用者已经获得了所有权,并准备放弃它,传递unique_ptr<T>值即可。就我所见,有三种合理的传递这样的值的模式。

  1. 按值传递,如问题中的add(unique_ptr<B> b)
  2. 通过非const左值引用传递,如add(unique_ptr<B>& b)
  3. 通过右值引用传递,如add(unique_ptr<B>&& b)

通过const左值引用传递是不合理的,因为它不允许被调用的函数获取所有权(而const右值引用会更傻比; 我甚至不确定它是否被允许)。

就有效代码而言,选项1和3几乎是等效的:它们强制调用者将rvalue作为参数传递给调用,可能通过将变量包装在对std :: move的调用中来实现(如果参数已经是rvalue,即无名字,例如从new的结果中转化而来,则不需要这样做)。然而,在选项2中,不能传递rvalue(可能来自于std::move),并且必须使用命名的unique_ptr<T>变量调用该函数(当传递从new转换而来的值时,必须首先赋值给一个变量)。

当确实使用std::move时,调用方中持有unique_ptr<T>值的变量被概念化地取消引用(转换为rvalue,分别转换为rvalue引用),并在此时放弃所有权。在选项1中,取消引用是真实的,并且该值被移动到一个临时变量中,该变量传递给被调用的函数(如果调用函数检查调用方中的变量,则会发现它已经保持了空指针)。所有权已经转移,调用方没有办法不接受它(什么都不做会导致指向的值在函数退出时被销毁;在参数上调用release方法将防止这种情况,但会导致内存泄漏)。令人惊讶的是,在函数调用期间,选项2和3在语义上是等效的,尽管它们对于调用方需要不同的语法。如果被调用的函数将参数传递给另一个接受rvalue的函数(例如push_back方法),则在两种情况下都必须插入std::move,这将在那一点转移所有权。如果被调用的函数忘记对参数进行任何操作,那么如果调用方持有其名称,则在选项2中仍将拥有该对象;尽管在情况3中,由于函数原型要求调用方同意释放所有权(通过调用std::move或提供临时对象),但实际转移所有权是延迟到函数调用之后的。总之,这些方法做:

  1. 强制调用方放弃所有权,并确保实际认领所有权。
  2. 强制调用方持有所有权,并准备好(通过提供非const引用)放弃所有权;然而,这不是显式的(不需要也不允许调用std::move),并且不能确保夺取所有权。我认为这种方法意图相当不清楚,除非明确打算夺取所有权或不夺取所有权是被调用函数的自由裁量权(可以想象一些用途,但调用方需要注意)
  3. 强制调用方明确指示放弃所有权,如方法1(但实际上只在函数调用之后才延迟转移所有权)。

选项3在其意图上相当清晰;只要实际上获得了所有权,它对我来说是最好的解决方案。它比选项1稍微更有效率,因为没有将指针值移动到临时变量中(对std::move的调用实际上只是类型转换,成本为零);如果指针在其内容实际移动之前通过多个中间函数传递,则这可能尤其重要。

下面是一些可供实验的代码。

class B
{
  unsigned long val;
public:
  B(const unsigned long& x) : val(x)
  { std::cout << "storing " << x << std::endl;}
  ~B() { std::cout << "dropping " << val << std::endl;}
};

typedef std::unique_ptr<B> B_ptr;

class A {
  std::vector<B_ptr> vb;
public:
  void add(B_ptr&& b)
  { vb.push_back(std::move(b)); } // or even better use emplace_back
};


void f() {
    A a;
    B_ptr b(new B(123)),c;
    a.add(std::move(b));
    std::cout << "---" <<std::endl;
    a.add(B_ptr(new B(4567))); // unnamed argument does not need std::move
}

输出结果如下:

storing 123
---
storing 4567
dropping 123
dropping 4567

注意,vector中存储的值按顺序被销毁。尝试更改方法add的原型(根据需要调整其他代码使其编译),以及它是否实际传递其参数b。可以获得多个输出行的排列。


9

是的,这就是应该做的。您明确地将所有权从 main 转移给 A。这基本上与您以前的代码相同,只是更加明确和可靠。


一些关于编程的扩展讨论:Herb Sutter 的 GOTW #91,以及 Scott Meyers 在传递仅移动类型时的建议,具体包括“按值”和“按 r-value 引用”传递的区别。 - Chris Beck

0

你的代码在 main 中可以简化一下,因为C++14提供了新特性:

a.add( make_unique<B>() );

在内部括号中,您可以为B的构造函数放置参数。


您也可以考虑一个类成员函数,它接管了一个原始指针:
void take(B *ptr) { vb.emplace_back(ptr); }

相应的代码在main中将会是:

a.take( new B() );

另一种选择是使用完美转发来添加向量成员:

template<typename... Args>
void emplace(Args&&... args)
{ 
    vb.emplace_back( std::make_unique<B>(std::forward<Args>(args)...) );
}

而主要的代码如下:

a.emplace();

就像以前一样,你可以将构造函数参数放在 B 的括号内。

链接到工作示例


0
所以我的简单问题是:这是做到的方式吗?特别是,这个“move(b)”是唯一的方法吗?(我在考虑rvalue引用,但我不完全理解,所以...)
如果您有一个链接,其中包含我找不到的移动语义、unique_ptr等的完整解释,请不要犹豫。 无耻的广告, 搜索标题“Moving into members”。它恰好描述了您的情况。

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