13

假设我有以下代码:

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)

需要这 2 个移动函数,因为强制转换为右值引用:

  1. 防止函数“add”按值传递
  2. 使用push_backB 的默认移动构造函数

显然,如果我将“添加”函数更改为通过引用(普通左值引用)传递,std::move我不需要第二个。main()

不过,我想确认一下这一切……

4

4 回答 4

9

是的,应该这样做。您明确地将所有权从 转移mainA。这与您之前的代码基本相同,除了它更明确且更可靠。

于 2012-09-23T12:23:24.733 回答
9

我有点惊讶,这里没有非常清楚和明确地回答这个问题,也没有在我容易偶然发现的任何地方回答。虽然我对这些东西很陌生,但我认为可以说以下内容。

这种情况是一个调用函数,它构建一个unique_ptr<T>值(可能通过将调用的结果强制转换new为就像这里发生的那样进入a 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 几乎是等价的:它们强制调用者写入一个右值作为调用的参数,可能通过在调用中包装一个变量std::move(如果参数已经是一个右值,即未命名)就像从 的结果中进行的强制转换一样new,这不是必需的)。然而,在选项 2 中,不允许传递右值(可能来自std::move),并且必须使用命名变量调用函数(当传递来自 的强制转换时,必须先分配给变量)。unique_ptr<T>new

std::move确实使用时,unique_ptr<T>在调用者中保存该值的变量在概念上被取消引用(转换为右值,分别转换为右值引用),并且此时放弃所有权。在选项 1 中,取消引用是真实的,并且值被移动到传递给被调用函数的临时值(如果调用函数将检查调用者中的变量,它会发现它已经持有一个空指针)。所有权已经转移,调用者不可能决定不接受它(对参数不做任何事情会导致指向的值在函数退出时被销毁;release在参数上调用方法会阻止这种情况,但只会导致内存泄漏)。令人惊讶的是,选项 2. 和 3. 在语义上在函数调用期间等效,尽管它们对调用者需要不同的语法。如果被调用函数将参数传递给另一个采用右值的函数(例如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

请注意,值按存储在向量中的顺序销毁。尝试更改方法的原型add(必要时调整其他代码以使其编译),以及它是否实际上传递了它的参数b。可以获得输出线的几种排列。

于 2014-06-22T14:16:36.407 回答
0

所以我的简单问题是:这是这样做的方法吗?特别是,这是“move(b)”的唯一方法吗?(我在考虑右值引用,但我并不完全理解它......)

如果你有一个完整解释移动语义的链接,unique_ptr...我找不到,不要犹豫。

无耻插件,搜索标题“移入会员”。它准确地描述了您的场景。

于 2014-06-22T14:22:21.140 回答
0

从 C++14 开始,您的代码main可以简化一点:

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

和main中的代码:

a.emplace();

和以前一样,您可以将构造函数参数B放在括号内。

链接到工作示例

于 2016-01-19T05:23:35.330 回答