【问题标题】:Can/Should I inherit from an STL iterator?我可以/应该从 STL 迭代器继承吗?
【发布时间】:2011-09-22 04:43:25
【问题描述】:

我可以/应该从 STL 迭代器继承来实现我自己的迭代器类吗?如果没有,为什么不呢?

【问题讨论】:

标签: c++ stl iterator


【解决方案1】:

简答

许多人认为std::iterator 类与常规类型别名相比并没有提供太多功能,甚至通过不显式提供名称并依赖模板参数的顺序来稍微混淆它们。它在 C++17 中已被弃用,并且很可能在几年后消失。

这意味着您不应再使用std::iterator。如果您对全文感兴趣,可以阅读下面的整篇文章(由于在弃用提案之前已经开始,因此有些冗余)。


旧版答案

如果您对历史不感兴趣,可以忽略以下所有内容。以下片段甚至多次自相矛盾。

截至今天 (C++11/C++14),该标准似乎暗示从 std::iterator 继承来实现自定义迭代器不再是一个好主意。这里简单解释一下,来自N3931

尽管该标准几乎多次犯了这个错误,但我建议不要将directory_iteratorrecursive_directory_iterator 描述为派生自std::iterator,因为这是对实现的绑定要求。相反,它们应该被描述为具有适当的 typedef,并由实现者决定如何提供它们。 (使用is_base_of 的用户可以观察到差异,而不是他们应该问这个问题。)

[2014-02-08 Daniel cmets 并提供措辞]

这个问题与N3198 所描述的用于消除从unary_function 和朋友派生的要求的解决方案基本相似,我也强烈赞成在这里遵循这种精神。我想补充一点,基本上所有“较新”的迭代器类型(例如 regex 相关的迭代器)也不是从 std::iterator 派生的。

论文引用了N3198,它本身声明它遵循N3145 中讨论的弃用。弃用仅提供typedefs 的类的原因如下:

我们在概念方面的经验让我们相信,如果类型和函数的可用性足够,则很少需要依赖特定的基类派生类关系。即使在没有语言支持的概念的情况下,新的语言工具也允许我们推断类类型中类型名的存在,这将在它们之间引入更弱的耦合。用关联类型替换继承的另一个优点是,这将减少出现歧义的情况:如果一个类型同时继承自 unary_functionbinary_function,这很容易发生(这是有道理的,如果函子既是一元函数对象又是二元函数对象)。

tl;dr:只提供typedefs 的类现在被视为无用。此外,它们会在不需要时增加耦合,更冗长,并且在某些极端情况下可能会产生不必要的副作用(参见前面的引文)。


更新: issue 2438 from N4245 似乎实际上与我之前断言的相矛盾:

为了 LWG 的方便,九个 STL 迭代器被描述为派生自 std::iterator 以获取它们的 iterator_category/etc。类型定义。不幸的是(并且无意中),这也要求继承,这是可观察的(不仅通过is_base_of,而且还通过重载决议)。这是不幸的,因为它使用户感到困惑,他们可能被误导认为他们自己的迭代器必须从std::iterator 派生,或者重载函数以采用std::iterator 是有意义的。这也是无意的,因为 STL 最重要的迭代器容器迭代器不需要派生自 std::iterator。 (有些甚至被允许是原始指针。)最后,这不必要地限制了实现者,他们可能不想从std::iterator 派生。 (例如,简化调试器视图。)

总而言之,我错了,@aschepler 是对的:它可以使用,但它肯定不是必需的 - 也不气馁。整个“让我们删除std::iterator”的东西是为了标准而不是限制标准库实现者。


第 3 轮: P0174R0 建议弃用 std::iterator,以便将来可能删除。该提案已经很好地解释了为什么应该弃用它,所以我们开始吧:

对于读者来说,长序列的 void 参数比简单地在类定义本身中提供预期的类型定义要清楚得多,这是当前工作草案所采用的方法,遵循 C++14 中设置的模式,我们在弃用了从 unary_function 和 binary_function 派生的整个函子库。

除了降低清晰度之外,迭代器模板还为粗心的人设置了一个陷阱,因为在典型用法中,它将是一个依赖基类,这意味着它不会在从类内部或其所在的名称查找过程中查看成员函数。这导致用户惊讶地试图理解为什么以下简单的用法不起作用:

#include <iterator>

template <typename T>
struct MyIterator : std::iterator<std::random_access_iterator_tag, T> {
   value_type data;  // Error: value_type is not found by name lookup 

   // ... implementations details elided ...
};

单是清晰的原因就足以说服 LWG 更新标准库规范,不再要求标准迭代器适配器从 std::iterator 派生,因此在标准本身中不再使用此模板。因此,它看起来很适合弃用。

这变得有点累了,似乎不是每个人都同意,所以我会让你得出自己的结论。如果委员会最终决定不推荐使用std::iterator,那么很清楚你不应该再使用它了。请注意,follow-up paper 突出了对删除 std::iterator 的大力支持:

2016 年杰克逊维尔更新:

投票: 为 C++17 弃用 iterator??
SF  F   N   A   SA
6    10  1    0   0

在上述投票结果中,SFFNASA 代表强烈支持支持中立反对强烈反对.

2016 年奥卢更新:

投票: 仍想弃用 std::iterator?
SF F N A SA
3   6  3  2  0

P0619R1提议删除std::iterator,可能最早C++20,还提议增强std::iterator_traits,使其可以自动推导出difference_typepointerreference的类型std::iterator 未明确提供时的方式。

【讨论】:

  • 我没有读到任何建议用户定义的类不应该继承std::iterator。 N3931 的要点是标准库规范不应该要求 library classes 来继承它。在unary_functionbinary_function 的情况下,整个模型被确定为不如SFINAE 方法、std::bind 等、类型、功能等等。当您打算编写一个迭代器类时,直到您定义了所有正确的 typedef 并支持正确的表达式,您才真正这样做。 std::iterator 只是让部分工作更轻松的一种方式。
  • @Morwenn 你能不能更新一个新的 TLDR?我知道我应该在内部建立我的 typedef,不再依赖于迭代器标记或从 iterator 继承,因为它将在 C++17 中被弃用。对吗?
  • @Morwenn 谢谢!我提出了一个后续问题,并认为将其链接到此帖子很重要:stackoverflow.com/q/37031805/2642059
【解决方案2】:

没有人不应该因为可能会遇到的潜在问题。也许你最好使用 Composition 而不是 Inheritance 和 STL 迭代器。

由于缺少虚拟析构函数而导致的未定义行为:
STL 容器和迭代器并不打算充当基类,因为它们没有虚拟析构函数。

对于没有用作基类的虚拟析构函数的类,当通过指向基类的指针(delete、delete[] 等)解除分配时会出现问题。由于这些类没有虚拟析构函数,因此无法正确清理它们并导致未定义的行为。

有人可能会争辩说没有必要以多态方式删除迭代器,因此继续从 STL 迭代器派生没有错,可能还有其他一些问题,例如:

可能根本不可能继承:
标准容器中的所有迭代器类型都是实现定义
例如:std::vector&lt;T&gt;::iterator 可能只是 T*。在这种情况下,您根本无法继承它。

C++ 标准没有规定要求说 std::vector&lt;T&gt;::iterator 不 使用继承抑制技术来防止派生。因此,如果您从 STL 迭代器派生,则您依赖于您的 STL 恰好允许派生的特性。这使得这样的实现不可移植

如果实施不当会出现错误行为:
考虑到您是从向量迭代器类派生的,例如:

class yourIterator : std::vector<T>::iterator { ... };

可能有一个函数对向量迭代器进行操作,
例如:

void doSomething(std::vector<T>::iterator to, std::vector<T>::iterator from);

由于yourIteratorstd::vector&lt;T&gt;::iterator,您可以在容器类上调用doSomething(),但您将面临Object Slicing 的丑陋问题。 doSomething() 必须以适当的模板方式实现,以避免 问题。

使用标准库算法时遇到的问题:
假设您正在使用向量迭代器的推导,然后您使用标准库算法,如 std::transform()

例如:

yourIterator a;
yourIterator b;
...
std::transform( a++, b--, ... );

后缀 operator ++ 返回 std::vector&lt;T&gt;::iterator 而不是 yourIterator 导致选择了错误的模板。

因此,从 STL 迭代器继承确实是可能的,但如果您准备挖掘所有这些和许多其他潜在问题并解决它们,我个人不会给它时间和精力这样做。

【讨论】:

  • 你的意思是,带有虚析构函数的类只能派生自?
  • @Nawaz:你的意思是“你的意思是,只有带有虚拟析构函数的类才能派生?”? :D
  • 希望能回答您的疑问或讽刺,无论它是什么。
【解决方案3】:

如果你在谈论std::iterator 模板,那么是的,你应该这样做,但我希望你明白它没有任何功能,只是一堆 typedef。这个决定的优点是您的迭代器可以提供给iterator_traits 模板。

另一方面,如果您在谈论一些特定的 STL 迭代器,例如 vector&lt;T&gt;::iterator 或其他,那么答案是响亮的。更不用说其他所有内容了,您不确定它实际上是一个类(例如,相同的vector&lt;T&gt;::iterator 可以只定义为T*

【讨论】:

    【解决方案4】:

    如果你的意思是std::iterator:是的,这就是它的用途。

    如果你还有别的意思:不,因为没有一个 STL 迭代器有 virtual 析构函数。它们不适合继承,从它们继承的类可能无法正确清理。

    【讨论】:

    • 迭代器不在上下文中使用,这很重要(即你永远不会有一个指向派生迭代器的基本 cass 指针)。迭代器始终是对象。
    • 这个建议过于宽泛。如果派生类不需要任何清理(自动生成或其他方式),或者不会以多态方式使用,则可以不使用虚拟析构函数。指向迭代器的指针非常少见。
    • @Martin:仍然可能获得指向迭代器的指针,在这种情况下,你就完蛋了。我在项目中使用了指向(重型,而不是 STL)迭代器的指针。
    • 您知道std::iterator 也没有虚拟析构函数吗? @MarkRansom:通过指向基类的指针删除具有非虚拟析构函数的类始终是 UB。
    • @MikeMB 你可能是对的,但我更重要的观点是:迭代器极不可能被多态地删除。甚至与它们一起使用的函数也可能是基于模板的,以便与完全派生的类一起使用。
    猜你喜欢
    • 1970-01-01
    • 2013-04-06
    • 1970-01-01
    • 2010-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多