如何正确转发unique_ptr?

时间:2016-01-27 16:17:21

标签: c++ c++11 move unique-ptr

转发std :: unique_ptr的正确方法是什么?

以下代码使用std::move,我认为这是我们考虑过的做法,但它与clang崩溃了。

class C {
   int x;
}

unique_ptr<C> transform1(unique_ptr<C> p) {
    return transform2(move(p), p->x); // <--- Oops! could access p after move construction takes place, compiler-dependant
}

unique_ptr<C> transform2(unique_ptr<C> p, int val) {
    p->x *= val;
    return p;
}

在通过p将所有权转移到下一个功能之前,是否有一个更强大的约定,而不仅仅是确保从std::move获得所需的一切?在我看来,在对象上使用move并访问它以向同一函数调用提供参数可能是一个常见的错误。

6 个答案:

答案 0 :(得分:5)

由于移动后您确实不需要访问p,因此一种方法是在移动之前获取p->x然后再使用它。

示例:

unique_ptr<C> transform1(unique_ptr<C> p) {
    int x = p->x;
    return transform2(move(p), x);
}

答案 1 :(得分:4)

代码罚款。

  • std :: move只不过是一个演员(g ++:类似于 static_cast<typename std::remove_reference<T>::type&&>(value)
  • 每个参数表达式的值计算和副作用是 在执行被调用函数之前排序。
  • 然而,函数参数的初始化发生在 调用函数的上下文。 (感谢T.C

N4296草案的引言:

1.9 / 15程序执行

  

[...]当调用一个函数(函数是否为内联函数)时,每一个   值计算和与任何参数相关的副作用   表达式,或使用指定被调用的后缀表达式   函数,在执行每个表达式之前排序或   被调用函数体中的语句。 [...]

5.2.2 / 4函数调用

  

调用函数时,应初始化每个参数(8.3.5)   (8.5,12.8,12.1)及其相应的论点。 [注意:这样   对每个初始化进行不确定的排序   其他(1.9)尾注] [...]每个的初始化和销毁   参数发生在调用函数的上下文中。 [...]

样本(g ++ 4.8.4):

#include <iostream>
#include <memory>
struct X
{
    int x = 1;
    X() {}
    X(const X&) = delete;
    X(X&&) {}
    X& operator = (const X&) = delete;
    X& operator = (X&&) = delete;
};


void f(std::shared_ptr<X> a, X* ap, X* bp, std::shared_ptr<X> b){
    std::cout << a->x << ", " << ap << ", " << bp << ", " << b->x << '\n';
}

int main()
{
    auto a = std::make_unique<X>();
    auto b = std::make_unique<X>();
    f(std::move(a), a.get(), b.get(), std::move(b));
}

输出可能是1, 0xb0f010, 0, 2,显示一个(零)指针移开。

答案 2 :(得分:3)

停止在一行上做多件事,除非操作是非变异的。

如果代码全部在一行上,则代码不会更快,而且通常不太正确。

std::move是一个变异操作(或者更准确地说,它将一个操作标记为“mutate ok”)。它应该在它自己的行上,或者至少它应该在一条线上而不与其参数进行其他交互。

这就像foo( i++, i )。你修改了一些东西并且也使用了它。

如果你想要一个普遍的无脑习惯,只需绑定std::forward_as_tuple中的所有参数,并调用std::apply来调用该函数。

unique_ptr<C> transform1(unique_ptr<C> p) {
  return std::experimental::apply( transform2, std::forward_as_tuple( std::move(p), p->x ) );
}

避免了这个问题,因为p的变异是在不同于p.get()p->x地址读取的行上完成的。

或者,滚动你自己:

template<class F, class...Args>
auto call( F&& f, Args&&...args )
->std::result_of_t<F(Args...)>
{
  return std::forward<F>(f)(std::forward_as_tuple(std::forward<Args>)...);
}
unique_ptr<C> transform1(unique_ptr<C> p) {
  return call( transform2, std::move(p), p->x );
}

此处的目标是将参数评估表达式与参数初始化评估分开排序。它仍然无法修复一些基于移动的问题(例如SSO std::basic_string移动内容和引用到char的问题。)

接下来,希望编译器在一般情况下为无序的mutate-and-read添加警告。

答案 3 :(得分:3)

Dieter Lücking's answer所述,值计算在函数体之前排序,因此std::moveoperator ->在函数体之前排序--- 1.9 / 15. < / p>

然而,这 not 指定参数初始化在所有这些计算之后完成,它们可以相互出现在任何地方,并且可以出现在非依赖值计算中,只要它们是在函数体之前完成--- 5.2.2 / 4。

这意味着此处的行为未定义,因为一个表达式修改p(移入临时参数)而另一个表达式使用p的值,请参阅https://stackoverflow.com/a/26911480/166389。虽然如上所述,P0145建议将评估顺序从左到右(在本例中)固定。这意味着你的代码被破坏了,但是transform2(p->x, move(p))会做你想要的。 (更正,感谢T.C.

就习惯用法来避免这种情况而言,考虑David Haim's approach通过引用取unique_ptr<C>,尽管这对调用者来说是非常不透明的。你发出的信号是“可以修改这个指针”。 unique_ptr移动状态相当清楚,所以这不太可能像你从传入的对象引用中移动一样严重。

最后,您需要使用p和修改p之间的序列点。

答案 4 :(得分:1)

是的......答案不是很多,而是一个建议 - 为什么首先要通过unique_ptr的所有权?似乎transformXXX使用整数值,为什么内存管理必须在这里发挥作用?

通过引用传递unique_ptr

unique_ptr<C>& transform1(unique_ptr<C>& p) {
    return transform2(p, p->x); // 
}

unique_ptr<C>& transform2(unique_ptr<C> p&, int val) {
    p->x *= val;
    return p;
}

将所有权传递给这些功能。创造特定的功能,这是他们的工作。从记忆管理中分离出代数逻辑。

答案 5 :(得分:1)

一般来说,不,没有照顾小细节就没有这种方法可以做到这一点。在成员初始化列表中,它实际上是偷偷摸摸的。

只是一个超越你的例子,如果p->x本身就是一个生命周期取决于*p的对象,然后是transform2(),那么会发生什么呢?不知道它的参数之间的时间关系,将val向前传递给某个接收函数,而不用考虑让*p保持活跃状态​​。并且,鉴于其自身的范围,它应该如何知道呢?

移动语义只是一组容易被滥用的功能之一,需要小心处理。再说一遍,这是C ++基本魅力的一部分:它需要关注细节。作为增加责任的回报,它会给你更多的控制权 - 或者反过来呢?