【问题标题】:Why don't almost all the type aliases in std::iterator_traits have defaults?为什么 std::iterator_traits 中的几乎所有类型别名都没有默认值?
【发布时间】:2021-05-28 09:01:13
【问题描述】:

在不借助 boost.iterator 等库的情况下创建 C++20 之前的新迭代器时,需要指定类型别名 difference_typevalue_typepointerreferenceiterator_category . Accordingcppreference,使用 C++20,只需要指定 difference_typevalue_type,我认为这很棒! 但是为什么这 3 个别名有默认值呢?

对此我有两件事不明白(还有一件在我看来像是疏忽):

  1. 为什么value_typedifference_type 没有默认值?使用std::remove_reference_t<reference> 之类的东西作为value_type 的默认值不是很有意义吗? 作为随机访问迭代器的 difference_type 的默认值,使用带有两个迭代器的 - 运算符的结果类型可以说是有意义的。
  2. C++20 添加了contiguous_iterator_tag。就像input_iterator_tagforward_iterator_tag 一样,我看不出编译器应该如何正确区分连续迭代器和随机访问迭代器,我猜这就是它显然从不选择contiguous_iterator_tag 的原因。这是故意的吗?将输入迭代器误分类为前向迭代器似乎也有些危险,那么为什么不要求程序员自己指定这个别名呢?
  3. 在一个有点不相关的注释中,我不确定是否为iterator_category 静默生成一个值是否是个好主意,即使程序员已明确声明另一个类别,并为iterator_category 生成一个与concept 似乎也很奇怪。考虑这个不切实际的例子:
#include <iostream>
#include <iterator>

// With the == operator, this is an input iterator, but nothing else.
struct WeirdIterator {
    // Not an output iterator because you can't assign to a const reference
    const int& operator*() const { return 42; }
    WeirdIterator& operator++() { return *this; } // unimportant
    WeirdIterator operator++(int) { return *this; } // unimportant
    // bool operator==(const WeirdIterator&) const = default;
    using iterator_category = std::random_access_iterator_tag;
    using value_type = int;
    using difference_type = int;
};


void iteratorConcept(std::input_iterator auto) {
    std::cout << "input iterator concept" << std::endl;
}
void iteratorConcept(std::random_access_iterator auto) {
    std::cout << "random access iterator concept" << std::endl;
}

void iteratorTag(std::output_iterator_tag) {
    std::cout << "output iterator tag" << std::endl;
}
void iteratorTag(std::input_iterator_tag) {
    std::cout << "input iterator tag" << std::endl;
}
void iteratorTag(std::random_access_iterator_tag) {
    std::cout << "random access iterator tag" << std::endl;
}

int main() {
    WeirdIterator iter;
    iteratorConcept(iter);
    iteratorTag(std::iterator_traits<WeirdIterator>::iterator_category{});
    return 0;
}

这会打印“输入迭代器概念”和“输出迭代器标记”,因为它缺少比较运算符(概念不需要)。 如果我添加注释行,现在将打印“输入迭代器概念”和“随机访问迭代器标记”,即使它显然不是随机访问迭代器。公平地说,像这样写错iterator_category(即random_access_iterator_tag)是一个非常愚蠢的例子,但我仍然认为检查这个概念是否满足是有意义的,尤其是在“回退”的情况下" output_iterator_tag:忘记编写 == 运算符不应将输入迭代器变成不可用的输出迭代器。检查相应的概念是否得到满足是否可能并且有意义?

编辑 我的问题中有几点似乎不清楚,或者我做了一些不正确但未说明的假设。我将尝试更明确地说明它们并重新表述我目前的理解(在阅读 Nicol Bolas 的回答后):

  1. 关于第 3 点:据我了解,T 类型可能有一些 std::iterator_traits&lt;T&gt;::iterator_category 别名,即使它没有模拟相应的 C++20 概念或 C++17 命名要求。这是有意的。所以,让我们忘记这一点,因为它可能更适合单独的问题。
  2. 我认为,如果我没有明确写下 std::type_traits 别名(例如,当我只写 reference 时,我只写 value_type)定义的别名对于某些迭代器来说可能是不正确的,并且是合理的默认值。它是否正确?如果这是不正确的,我的问题已经得到了很好的回答。
  3. 如果没有为输入迭代器T 定义T::reference,则std::iterator_traits::reference 定义为decltype(*std::declval<T&>())。这是正确的吗?
  4. 如果可以基于operator* 定义reference,那么基于* 也定义value_type 是否有意义?假设 5. 是正确的, 我能想到的唯一输入迭代器是来自std::vector&lt;bool&gt; 的迭代器,由于这种差异,有几个建议弃用它。所以大多数输入迭代器都可以使用这个定义,而那些不能简单地指定value_type。我错过了什么吗?
  5. 关于第 2 点:通常无法确定迭代器属于哪个类别。 使用例如一个输入迭代器就好像它是一个更通用的前向迭代器将是一个错误。程序员未指定iterator_category 的有效迭代器的type_traits::iterator_category 可能不正确。这不会影响概念或命名要求(它们考虑了语义),但实际上,stl 函数可能无法与此迭代器一起正常工作,而不会产生(运行时或编译时)错误。因此,我认为要求程序员明确说明类别是个好主意。这种推理有问题还是遗漏了什么?

我希望我不会显得过于迂腐或坚持我的个人观点,但我真的不知道上述几点是否以及哪里有错误,我猜这不是只是让我感到困惑。

【问题讨论】:

  • 给定一个随机的迭代器T,它的value_type 是如何默认定义的?
  • @Sam Varshavchik 也许我在这里遗漏了一些明显的东西,但std::remove_reference_t&lt;decltype(*std::declval&lt;T&gt;())&gt; 不是一个好的候选人吗?
  • value_typestd::ostreambuf_iteratorvoid。然而,它的operator*() 并没有返回空值。
  • 我应该指定我在谈论输入迭代器(即所有不仅是输出迭代器的迭代器),我的默认值为value_type:我认为reference 的默认值为@ 987654374@ 用于仅作为输出迭代器但基于operator* 用于所有其他迭代器的迭代器,所以对我来说,对value_type 执行相同操作似乎有些合理。

标签: c++ iterator language-lawyer c++20


【解决方案1】:

在这一点上理解某些东西很重要,因为某些不同的东西在这里被混为一谈。

在 C++20 中,有两种迭代器分类:old C++17 named requirementsnew C++20 concept-based iterators。大多数旧的要求都映射到后者,但 concept 要求允许将更多的东西视为迭代器,而不是 C++17 要求所允许的。

std::iterator_traits 用于两者,因为它们确实使用许多相同的移动部件。这样做的重点是应该可以编写一个迭代器来满足 C++17 命名要求和类似的 C++20 概念。也就是你可以写一个满足Cpp17RandomAccessIteratorstd::random_access_iterator的类型,不用太麻烦。

我之所以提出这一点,是因为讨论中的许多事情对于一组需求来说比另一组更重要。

为什么value_typedifference_type 没有默认值?将std::remove_reference_t&lt;reference&gt; 之类的东西用作value_type 的默认值不是很有意义吗?

显然,这需要您指定reference。所以你仍然需要指定两件事。 value_type 是迭代器的创建者无论如何都在考虑的那个。如果他们正在考虑,可能是因为reference 需要不是value_type&amp;,所以无论如何他们都需要指定两者。

C++20 添加了 contiguous_iterator_tag。就像 input_iterator_tag 与 forward_iterator_tag 一样,我看不出编译器应该如何正确区分连续迭代器和随机访问迭代器,我猜这就是为什么它显然从不选择 contiguous_iterator_tag 的原因。这是故意的吗?

在 C++17 中,没有“连续迭代器”之类的东西。与 RandomAccessIterator 不同。标准中有一个完整的部分解释了 RandomAccessIterator 的要求,而“连续迭代器”得到一个段落语句,没有关于它的额外信息,也很少有实际用途。

当然,“连续迭代器”没有迭代器标记。这样做是为了避免添加另一个迭代器标记并可能使许多 可能 无法正常工作的代码,因为一个连续的迭代器反而将自己宣传为随机访问。

C++20 改变了一切。它添加了一个std::contiguous_iterator_tag,但这样做是因为std::contiguous_iterator now has syntactical differences 来自std::random_access_iterator。也就是说,一个连续的迭代器必须允许通过std::to_pointer 转换成指向它的value_type 的指针。这允许您将迭代器对转换为指针对,而无需取消引用可能无法取消引用的迭代器(例如过去的迭代器)。

另请注意,迭代器类别的自动分配基于满足 C++17 命名要求,不是 C++20 概念。由于没有名为“连续迭代器”的需求(即使有,它在语法上也无法确定),因此不能自动分配它。

自动分配仅适用于 C++17 要求的原因是因为 C++20 概念是根据 std::iterator_traits 定义的。所以它不能在不创建循环定义的情况下使用这些概念。

在一个有点不相关的说明中,我不确定是否为 iterator_category 静默生成一个值是否是个好主意,即使程序员已经明确声明了另一个类别

这不是标准的作用。如果您指定一个(除了下面提到的一个奇怪的怪癖之外),它只会提供一个。

这会打印“输入迭代器概念”和“输出迭代器标签”,因为它缺少比较运算符(这不是概念所必需的)。

这是 iterator_category 的新定义的一个奇怪的怪癖,但这个怪癖最终确实正确地代表了你的类型的不连贯性。

主模板iterator_category 具有3 possible versions, depending on how you defined your iterator type。如果您的迭代器提供除pointer 之外的所有成员类型别名,那么它只使用它们。如果它只提供其中的一些,那么它会针对 Cpp17InputIterator 的仅展示版本进行concept 检查。如果你的类型适合,那么它使用你的类型的iterator_category(如果你不提供,那么它会计算一个)。

但是,如果您的迭代器不是输入迭代器,那么它会检查基本的 Cpp17Iterator。如果合适,那么iterator_traits::iterator_category固定output_iterator_tag。这当然是一个奇怪的选择。

如果我添加注释行,现在会打印“输入迭代器概念”和“随机访问迭代器标记”,即使它显然不是随机访问迭代器。

但你这是一个随机访问迭代器。系统不应该覆盖你所说的;如果您的类型与输入迭代器不匹配但仍碰巧是某种迭代器,这只是一个怪癖。

无论如何,如果你撒谎,你撒谎。垃圾进,垃圾出。

我仍然认为检查概念是否满足是有意义的,尤其是在“后备”输出迭代器标签的情况下:忘记编写 == 运算符不应将输入迭代器变成不可用的输出迭代器.

但是... 就是这样。对于输入迭代器,相等性测试不是可选的。如果您无法测试它是否相等,那么它不是输入迭代器。实际上,如果系统按照您的建议进行操作,那正是您将获得的标签:输出迭代器。

那么你的问题是什么?如果您不小心未能使您的类型成为输入迭代器,您希望系统将其正确分类为符合其行为的类型,还是希望系统将您错误的类别向前转发?

【讨论】:

  • 感谢您的帮助。还有几点我不明白,所以我更新了我的问题。具体来说,我不明白这个答案中提到的两件事:“显然,这需要你指定reference。”我认为reference 的默认值没有根据value_type 实现,因此应该可以(但可能不是一个好主意?)为value_type 添加默认值。 “这不是标准所做的。如果你不指定一个,它只会提供一个。”我认为WeirdIterator 是一个反例,但这与我的主要问题无关。
  • @Irgendwhy:你不能在收到答案后就添加六个新问题。要求澄清是一回事,但其中大部分都是全新的,与现有的关系不大。像这样问多个问题已经不受欢迎了,所以通过问更多来加倍关注它是没有帮助的。所以我将把我的回答限制在最初的问题上。
  • 抱歉,我会尝试将它们改写为单独的问题
  • 我真的不同意“没有理由选择reference 作为更基本的” - reference 绝对是基本的。这是*it 给你的,它是你与迭代器的主要交互。许多算法甚至根本不使用value_type
  • @Barry:用法和定义是不同的东西。大多数算法关心reference。但是 迭代器的创建者 没有。很可能,他们关心的是value_type。这里的默认值是关于 creator 必须指定的内容,而不是用户可以使用的内容。
猜你喜欢
  • 2020-05-08
  • 1970-01-01
  • 2021-05-29
  • 2021-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多