c++unique_ptr参数传递

c++ unique_ptr argument passing

本文关键字:参数传递 ptr c++unique      更新时间:2023-10-16

假设我有以下代码:

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

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

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

这种情况是一个调用函数,它构建了一个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几乎是等效的:它们强制调用方编写一个右值作为调用的参数,可能是通过在对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。可以获得输出行的几种排列。

是的,应该这样做。您正在明确地将所有权从main转移到A。这与您以前的代码基本相同,只是它更明确、更可靠。

所以我的简单问题是:这是做这件事的方法吗;移动(b)";唯一的方法是什么?(我在想右价参考,但我不完全理解,所以…)

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

无耻的插头,搜索标题";移入成员";。它准确地描述了你的场景。

main中的代码可以简化一点,因为C++14:

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

在这里,您可以将B的构造函数的参数放在内圆括号内。


您还可以考虑一个拥有原始指针所有权的类成员函数:

void take(B *ptr) { vb.emplace_back(ptr); }

并且CCD_ 40中对应的代码将是:

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的构造函数参数放在括号内。

链接到工作示例