转发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
并访问它以向同一函数调用提供参数可能是一个常见的错误。
答案 0 :(得分:5)
由于移动后您确实不需要访问p
,因此一种方法是在移动之前获取p->x
然后再使用它。
示例:
unique_ptr<C> transform1(unique_ptr<C> p) {
int x = p->x;
return transform2(move(p), x);
}
答案 1 :(得分:4)
代码不罚款。
static_cast<typename std::remove_reference<T>::type&&>(value)
)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::move
和operator ->
在函数体之前排序--- 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 ++基本魅力的一部分:它需要关注细节。作为增加责任的回报,它会给你更多的控制权 - 或者反过来呢?