45

我对 unique_ptr 和右值移动哲学感到困惑。

假设我们有两个集合:

std::vector<std::auto_ptr<int>> autoCollection;
std::vector<std::unique_ptr<int>> uniqueCollection;

现在我预计以下操作会失败,因为不知道算法在内部做什么,并且可能会制作内部数据透视副本等,从而从 auto_ptr 中剥夺所有权:

std::sort(autoCollection.begin(), autoCollection.end());

我明白了。并且编译器正确地不允许这种情况发生。

但后来我这样做:

std::sort(uniqueCollection.begin(), uniqueCollection.end());

这可以编译。我不明白为什么。我不认为 unique_ptrs 可以被复制。这是否意味着不能采用枢轴值,因此排序效率较低?或者这个支点实际上是一个举动,实际上与 auto_ptrs 的集合一样危险,应该被编译器禁止?

我想我错过了一些重要的信息,所以我急切地等待有人向我提供啊哈!片刻。

4

3 回答 3

55

我认为这更像是一个哲学问题而不是技术问题:)

根本问题是移动和复制之间有什么区别。我不会跳入技术/标准语言,让我们简单地做:

  • 复制:创建另一个相同的对象(或者至少,一个应该比较相等的对象)
  • 移动:取出一个物体并将其放在另一个位置

如您所说,可以根据复制来实现移动:将副本创建到新位置并丢弃原始位置。然而,那里有两个问题。一是性能,二是关于用于 RAII 的对象:两者中的哪一个应该拥有所有权?

一个适当的 Move 构造函数解决了两个问题:

  • 清楚的是哪个对象拥有所有权:新的,因为原来的将被丢弃
  • 因此无需复制指向的资源,从而提高效率

和很好地说明了这一点auto_ptrunique_ptr

使用 anauto_ptr你有一个搞砸的 Copy 语义:原始和副本不比较相等。您可以将其用于移动语义,但存在丢失指向某处的对象的风险。

另一方面,unique_ptr就是这样:它保证了资源的唯一所有者,从而避免了复制和随之而来的不可避免的删除问题。并且在编译时也保证了无副本。因此,只要您不尝试进行复制初始化,它就适用于容器中。

typedef std::unique_ptr<int> unique_t;
typedef std::vector< unique_t > vector_t;

vector_t vec1;                           // fine
vector_t vec2(5, unique_t(new Foo));     // Error (Copy)
vector_t vec3(vec1.begin(), vec1.end()); // Error (Copy)
vector_t vec3(make_move_iterator(vec1.begin()), make_move_iterator(vec1.end()));
    // Courtesy of sehe

std::sort(vec1.begin(), vec1.end()); // fine, because using Move Assignment Operator

std::copy(vec1.begin(), vec1.end(), std::back_inserter(vec2)); // Error (copy)

因此,您可以unique_ptr在容器中使用(与 不同auto_ptr),但许多操作将是不可能的,因为它们涉及类型不支持的复制。

不幸的是,Visual Studio 在执行标准方面可能相当松懈,并且还有许多扩展需要禁用以确保代码的可移植性......不要用它来检查标准:)

于 2010-05-21T06:44:08.430 回答
14

s 正在使用它们的unique_ptr移动构造函数移动。unique_ptr是可移动的,但不是可复制构造的。

这里有一篇关于右值引用的好文章。如果您还没有阅读它们,或者感到困惑,请看一看!

于 2010-05-20T18:29:56.977 回答
7

std::sort只要在任何给定时间每个对象只有一个活动副本,就只能使用移动操作而不能复制。这是一个比就地工作更弱的要求,因为原则上您可以临时分配另一个数组并在重新排序时将所有对象移动到它们。

例如,std::vector<std::unique_ptr<T>>如果超出其容量,它会为更大的向量分配存储空间,然后将所有对象从旧存储移动到新存储。这不是就地操作,但它完全有效。

事实证明,像快速排序和堆排序这样的排序算法实际上可以毫无困难地就地工作。快速排序的分区例程在内部使用 std::swap ,这对所涉及的两个对象都算作移动操作。选择枢轴时,一个技巧是将其与范围中的第一个元素交换,这样在分区完成之前它永远不会移动。

于 2013-02-20T09:52:46.647 回答