为什么在C++11中所有的迭代器/迭代器适配器都应该是非可移动的?

19
这个问题中讨论了C++11中何时将类型设为不可移动,我发现Scott Meyers在comp.std.c++上也提出了类似的问题。在该帖子中,SG列出以下类别的类型在C++11库中都是不可移动的。

  • 所有互斥类型(recursive_mutex、timed_mutex、recursive_timed_mutex)
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • reference_wrapper
  • duration
  • time_point
  • - 所有迭代器/迭代器适配器
  • ios_base
  • basic_istream::sentry
  • basic_ostream::sentry
  • 所有原子类型
  • once_flag

问题是:为什么所有迭代器/迭代器适配器都是不可移动的?


4
可能存在误解,因为迭代器概念要求可复制性,这是移动构造/赋值的一种完善。 - Luc Danton
@Mehrdad,准确地说,我抄袭了你的问题。 - billz
@billz:哈哈,确实,我注意到了 =P 希望你能得到一个好答案! :) - user541686
@Mehrdad 嗯,不幸的是它建立在给定列表正确的假设上。尽管这个问题仍然可以很好地用来证明这个列表是错误的,因此还是+1。 - Christian Rau
4个回答

15

这篇帖子是一年前标准被认可之前发布的,已经过时了。发帖人是Daniel Krügler,一个活跃的委员会成员,这是一种政治游说:

这些类型不可移动,可能还有更多的意外情况,因为在匹兹堡会议上才明确了隐式生成移动操作的规则。已经开放了一个通用库问题

http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#1331

以应对库中缺乏移动支持的问题。请务必联系您的国家机构代表,因为我听说这个问题不能替代国家机构反对FCD的评论。

换句话说,所有这些类型都不能移动将成为标准的致命缺陷,他希望Usenet观众要求在标准正式发布之前解决这个问题。

缺陷已移动到“关闭”列表中。解决方案为(提供链接以方便查看):
回顾规范的库部分并整合新添加的核心功能“Move Special Member Functions (N3044)”。
由于N3044是一份庞大的材料,因此很容易理解为什么这样的基本功能是必不可少的。
迭代器和任何其他具有简单值语义的东西,例如std::durationstd::time_point肯定是可移动的。正如其他人所提到的,可复制性意味着可移动性,如果没有这种情况,该语言将会出现问题。这篇文章在当时并没有错;相反,它在谈论未完成的语言的错误之处。

这是对于问题实际问题的一个好答案,以我个人看来。 - Christian Rau
+1 感谢 @Potatoswatter 我会尝试跟随链接,感谢提供指南。 - billz
你能说明“可复制性意味着可移动性”的确切含义吗?它如何适用于仅定义了复制操作和析构函数的类?它如何适用于定义了复制操作、析构函数和删除移动操作的类?我倾向于认为术语“可移动”不够精确。对我来说,这意味着什么并不明显。“可移动”只是指您可以使用rvalue源(可能解析为复制构造函数)构造T,还是实际上调用了移动构造函数? - sellibitze
@Mehrdad 这是个例外,但在复制/移动方面几乎是无意义的。移动总是可以建模为复制,然后删除原始对象的内容。只有在没有权限清空原始对象的情况下才会发生这种情况,这表明资源(而不是值)语义。在这种情况下,将一个对象从另一个对象初始化称为“复制”着实有些牵强。或者,该类可能正在滥用rvalue引用进行除了复制/移动语义之外的其他操作。 - Potatoswatter
@sellibitze 请看我对Mehrdad的回答。按照标准术语,17.6.3.1表21说标准模板的CopyConstructible类型参数也必须满足MoveConstructible - Potatoswatter
显示剩余3条评论

10

我理解你的列表中包括“以常规复制实现移动的类”这一类型。 迭代器被视为轻量级对象,很容易进行复制。强制规定它们有一个移动运算符是没有意义的。例如,std::vector<T>::iterator 实际上只是封装了一个 T*,复制它们的成本与移动它们一样便宜。


std::vector<T>::iterator 不需要被包装,这使得问题的断言值得怀疑。我没有跟进 Usenet 的链接... - Potatoswatter
1
@Potatoswatter:至少对于T=bool,你需要将其包装起来。;-) 无论如何,list<T>::iterator是类似于list_node<T>*的东西的包装器。 - sellibitze

2

“不可移动”对于迭代器来说,可能意味着类定义中没有用户声明的移动操作。但是迭代器仍然是可复制的。因此,移动请求仍然有效并会退回到复制。由于这个原因,在移动操作与复制操作完全相同时,没有必要在情况中提供移动操作。对于典型的迭代器实现,不存在优化移动的必要。


对于输入迭代器而言,仅支持移动而不是复制可能更有意义。 - flodin

1

简短回答

因为它们是可复制的。

详细回答

我们首先要澄清“移动”实际上是什么意思。 冯·诺伊曼机不会移动数据:它们只是“复制”。数据从一个内存位置复制到另一个位置。从来没有“移动”过。

但在更高的抽象层次上,数据可以只是指向其他数据的指针。当您复制指针并使已复制的指针失效时,所涉及的数据被称为从一个“所有者”移动到另一个“所有者”。

更普遍地说,复制一个值(例如指针中包含的地址)并销毁原始值使其设置为可识别的“无效”状态的操作被称为“移动”操作。

在C ++方面,我们可以区分不同类型的对象:

  1. 只包含简单值的对象。
  2. 只包含简单指针或引用的对象,它们“拥有”引用的内容。
  3. 只包含简单指针或引用的对象,它们不“拥有”引用的内容。
  4. 包含巨大值的对象。
  5. 表示物理实体或操作系统(更一般的“托管平台”实体)的对象。
对于所有这些类型,"复制"和"移动"的概念可能具有不同的语义意义,对于其中一些对象,其中一个操作可能根本没有意义。
现在考虑第一种类型的对象:
int a=5; c=0;
c = a;
c = std::move(a);

你期望移动后a的值是多少?c = a+b呢?a和b应该被“移动”到operator+中吗?
现在考虑类型2对象:
std::unique_ptr<int> pa(new int(5)), pb;
pb = std::move(pa);

这里有两个智能指针(它们都将在作用域退出时被销毁),以及一个整数。有一种操作(在本例中是delete)只能执行一次,因此只有一个指针必须保留对整数的“所有权”。这种情况下,“复制”是无意义的,移动是唯一支持的操作。

现在考虑第三种类型的对象:

std::list<int> lst = { 1,2,3,4 };
auto i = lst.begin();
auto j = i; 
*j = *i+5;
++i;
*i = *j;

这完全有道理:它只会使列表变为 {6,6,3,4}。 迭代器不拥有其引用的内容:可能有多个迭代器都指向同一值。复制是有意义的,但移动则不然:如果我们将 i 移入 j (而不是复制),那么就不再可能使用 *i 和 ++i 了。
现在考虑类型为4的对象:
class A
{
   int m[15000000]; //15 million integers
public:
   int& operator[](unsigned x) { return m[x]; }
   const int& operator[](unsigned x) const { return m[x]; }
};

这样一个巨大的对象在大多数系统中分配到栈上可能会有问题。它很可能会留在堆上,并由(智能)指针拥有/引用。它的地址将在其指针之间移动,但对象本身不可移动。它仍然可以被复制。

还有另一种微妙的情况:当A本身是指向动态分配的巨大数组的指针时:这与std::vector相同:它是可移动的,因为它本身是拥有动态分配数据的“智能指针”,但也可以被复制,因为可能存在需要新的不同副本的情况。

现在考虑类型5:

class window
{
private:
   HWND handle;
public:
   window() :handle(CreateWindow(....)) 
   { .... }
   ~window() { DestroyWindow(handle); }
};

这里的window实例代表屏幕上存在的一个window。 "复制"或"移动"是什么意思?

这很可能是mutexcondition_variable等情况,其中复制和移动都被禁用。


2
如果你正在撰写一篇较长的答案,你可以写、发布、删除、恢复以防止他人看到你未完成的帖子。 - RedX
@Emilio 这个人就是我,只是因为你发布了一个未完成的答案(这样我就可以问你同样的问题;))而将其投下反对票,这种行为应该受到打击(而且你的“简短回答”并不清晰或解释,它可能适合作为评论)。尽管如此,我没有理由评论我的反对票,因为“答案”的低质量非常明显,反对票很可能在不久的将来消失(它已经消失了)。 - Christian Rau
@ChristianRau:你对自己的观点负有唯一责任,我也对我的观点负有唯一责任。唯一的区别是我在事后写了下来。现在你谈论“低质量和其他废话”,但这些观点在你已经玩过游戏之后就没有价值了。在我看来,你只是说出了你必须说的话来证明你的偏见。个人认为Redex的评论更具建设性,但这只是我的观点。 - Emilio Garavaglia
@EmilioGaravaglia “你对自己的观点负有唯一责任,我也是我的观点的唯一持有人” - 做到了。所以如果我认为回答质量不佳,我决定下票,如果在你准备阅读和回应评论的时候这个评论会很快被删除(即使它没有删除,它的理由也已经改变,会根据现在已完成的答案的实际内容撰写不同的评论)。无论如何,我现在不再看到任何下票的理由了。 - Christian Rau

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