为什么我们应该在使用unique指针时使用std::move语义?

4

概念问题

假设我们有以下简单示例:

 void foo(std::unique_ptr<int> ptr)
 {
    std::cout << *ptr.get() << std::endl;
 }
 int main()
 {
    std::unique_ptr<int> uobj = std::make_unique<int>(4);
    foo(uobj );  // line-(1) Problem ,but Alternative -> foo(std::move(uobj ))
    std::unique_ptr<int> uobjAlt = uobj; // line-(2) Problem ,but Alternative -> std::unique_ptr<int> uobjAlt = std::move(uobj);        
    return EXIT_SUCCESS;
}

我们知道,std::unique_ptr 的概念是资源只被单个所有者拥有,并可在多个所有者之间移动;而 shared_ptr 则具有相反的方面。
就像上面展示的例子一样,当您查看 line-(1)line-(2) 时,您会注意到某些标准规则被违反了,因为 std::unique_ptr 没有定义复制构造函数和复制赋值运算符(已删除)。但为了避免编译错误,我们必须使用 std::move 函数。

问题

为什么现代 C++ 编译器不能自动生成指令来在 line-(1)line-(2) 中在 unique pointers 之间移动资源?因为我们知道 unique pointer 的设计本意就是为了这个目的。为什么我们需要显式地使用 std::move 来指示机器移动资源的所有权?

虽然我们知道,std::unique_ptr 只是一个类模板,但是在 line -1 和 line -2 中的情况却存在问题,编译器会抱怨无法复制 unique pointers(已删除的函数)。为什么会出现这种错误?为什么 C++ 标准和编译器供应商不能覆盖这个概念呢?

Unique Pointer 的设计意图是为了在传递其所有权时移动资源,当我们将其作为函数/构造函数参数或分配给另一个 unique pointer 时,它应该只是概念上的所有权移动,而没有其他内容。但是为什么我们需要使用 std::move 来告诉编译器实际进行移动呢?为什么我们不能自由地调用 line-(1) 和 line-(2) 呢?(除非有 const 或非 const 引用 的传递)。

(对于冗长的描述和不太流利的英语表示抱歉)谢谢。


5
因为自动化可能会导致太多危险。不自动化能强迫你思考自己在做什么……毕竟,如果你设计了一个只有唯一所有者的东西,你可能希望它始终是同一个所有者,而不会因为调用函数而错误地更改所有权。 - Jean-Baptiste Yunès
3
早些年,auto_ptr 的工作方式是这样的,而且非常糟糕。 - Dietrich Epp
1
@macxfadz 我完全不同意,编程语言的特性和编译器必须保护程序员免受错误的影响,否则这可能是一个严重的错误...你没有遇到过吗?此外,代码清晰地表达了程序员的意图。 - Jean-Baptiste Yunès
3
简而言之:因为在C++中,编译错误优于猜测的意图。 - Christian Hackl
3
std::string没有隐式转换为const char*,而是需要调用c_str()函数,原因与这里相同:为了避免在内存管理时因疏忽而造成错误。 - Pete Becker
显示剩余8条评论
3个回答

5

unique_ptr可以在uobj超出范围时自动为您释放内存。这就是它的工作。因此,由于它只有一个指针需要释放,所以它必须是唯一的,因此它的名称为unique_ptr

当您执行以下操作时:

std::unique_ptr<int> uobjAlt = uobj;

你正在执行一个“复制”操作,但是不应该复制指针,因为复制意味着必须释放两个对象“uobjAlt”和“uobj”,这将直接导致分段错误和崩溃。因此,通过使用“std::move”,你可以将所有权从一个对象移动到另一个对象中。
如果你想要多个指向同一个对象的指针,应该考虑使用{{link1:std::shared_ptr}}。

问题在于,当std::unique_ptr<int> uobjAlt = uobj;这一行被编译器分析时,为什么它不能为我们生成移动操作,而是寻找复制操作?难道它不应该为unique pointer进行特殊化吗?为什么编译器将其视为复制而不是移动unique_ptr,这就是冲突首先出现的原因。 - user7467325
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - The Quantum Physicist
@macxfadz 不应该这样做。正如评论中指出的那样,这会使犯错误变得太容易。想法是,如果您有一个左值,只有通过显式请求将其移动才能从中移动。 - juanchopanza
1
@juanchopanza 所以基本思想是避免错误。感谢传达其背后的基本规则。非常感谢您的所有想法和答案。:D - user7467325

3

这与编译器是否能够实现无关。它当然可以以这种方式工作,事实上,在C++11之前,std::auto_ptr<>就是这样工作的。但那真是太可怕了。

std::auto_ptr<int> x = std::auto_ptr<int>(new int(5));
std::auto_ptr<int> y = x;
// Now, x is NULL

问题在于等号=通常意味着"从x复制到y",但在这种情况下,所发生的是"从x移动到y,同时使x无效"。是的,如果你是一个精明的程序员,你会理解发生了什么,这并不总是让你感到惊讶。然而,在更常见的情况下,这将是非常令人惊讶的:
这是MyClass.h
class MyClass {
private:
    std::auto_ptr<Type> my_field;
    ...
};

下面是 MyClass.cpp 的代码:

void MyClass::Method() {
    SomeFunction(my_field);
    OtherFunction(my_field);
}

这里是Functions.h代码文件:
// Which overload, hmm?
void SomeFunction(Type &x);
void SomeFunction(std::auto_ptr<Type> x);

void OtherFunction(const std::auto_ptr<Type> &x);

现在你必须查看三个不同的文件才能确定my_field被设置为NULL。而使用std::unique_ptr,您只需要查看一个文件:

void MyClass::Method() {
    SomeFunction(std::move(my_field));
    OtherFunction(my_field);
}

仅仅看到这个函数,我就知道它是错的。我不需要弄清楚是哪个重载被用于SomeFunction,也不需要知道my_field的类型。在使事情变得显式和隐式之间,我们需要取得平衡。在这种情况下,无法明确地区分C++中移动和复制一个值的差异是一个问题,因此rvalue引用、std::movestd::unique_ptr等被添加到C++中以澄清事情,这些东西非常棒。

另一个auto_ptr如此糟糕的原因是它与容器交互不良。

// This was a recipe for disaster
std::vector<std::auto_ptr<Type> > my_vector;

一般来说,许多模板与auto_ptr搭配使用效果不佳,不仅仅是容器。


2

如果编译器被允许自动推断移动语义,例如std::unique_ptr类型,那么像这样的代码将会出错:

template<typename T> void simple_swap(T& a, T& b) {
    T tmp = a;
    a = b;
    b = tmp;
}

以上代码假设tmpa的一个拷贝(因为它仍然将a作为赋值运算符的左操作数)。标准算法中有一些代码实际上需要容器值的临时拷贝。推断移动可能会破坏它们,在运行时导致崩溃。这就是为什么不建议在STL容器中使用std::auto_ptr的原因。

1
如果您的函数 simple_swap 接收的参数 ab 都是 unique_ptr 类型,并且以非 const 引用类型传递,那么最终 a 拥有了 b 的资源,而 b 则拥有了之前由 a 拥有的 tmp 资源。这样做有什么问题吗? - user7467325
@macxfadz 复制和移动语义不仅在std::unique_ptr中定义。移动对象除了销毁之外,不能以任何方式使用。在此示例中,移动的对象a出现在赋值运算符的左侧,这将导致有效的C++对象崩溃。 - user4815162342
即使允许进一步使用移动的对象,仍有其他算法实际上依赖于临时副本与原始值并行存在(例如快速排序中“枢轴”元素的副本),如果在期望复制的地方突然执行移动操作,则会导致破坏。 - user4815162342
@user4815162342 是的,使用移动资源进行所有权转移必须是有意识的。我接受算法大多数情况下会产生意外结果或未定义行为,但另一方面,程序员/开发人员应该知道他在使用std::move/std::unique_ptr或至少auto_ptr时在做什么。 - user7467325

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