为什么 std::iterator_traits 中的几乎所有类型别名都没有默认值?

Irg*_*why 0 c++ iterator language-lawyer c++20

在不借助 boost.iterator 等库的帮助下创建 C++20 之前的新迭代器时,有必要指定类型别名difference_typevalue_typepointer和。 根据cppreference 对于 C++20,只需要指定和referenceiterator_categorydifference_typevalue_type这很棒!但是为什么这 3 个别名有默认值呢?

有两件事我不明白(其中一件事在我看来像是一个疏忽):

  1. value_type为什么和没有默认值difference_type?使用类似的东西std::remove_reference_t<reference>作为默认值难道没有意义吗value_type?作为随机访问迭代器的默认设置difference_type,使用采用两个迭代器的运算符的结果类型可以说是有意义的-
  2. C++20 添加了contiguous_iterator_tag. 就像与input_iterator_tagvsforward_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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Nic*_*las 5

此时理解一些东西很重要,因为某些不同的东西在这里被混淆了。

在 C++20 中,迭代器有两种分类:旧的 C++17 名为“需求”,以及新的基于C++20concept的迭代器。大多数旧要求都映射到后者,但这些concept要求允许将更多的东西视为迭代器,而不是 C++17 要求所允许的。

std::iterator_traits然而它们都使用了,因为它们确实使用了许多相同的移动部件。要点是应该可以编写一个既满足 C++17 命名要求又满足类似 C++20 概念的迭代器。也就是说,您可以编写一个满足 Cpp17RandomAccessIterator 的类型 std::random_access_iterator而不需要太多麻烦。

我提出这一点是因为正在讨论的许多事情对于一组需求比另一组需求更重要。

value_type为什么和没有默认值difference_type?使用类似的东西std::remove_reference_t<reference>作为默认值难道没有意义吗value_type

显然,这需要您指定reference. 所以你仍然需要指定两件事。value_type无论如何,迭代器的创建者正在考虑这一点。如果他们想到这一点,可能是因为reference需要是 a 以外的东西value_type&,所以无论如何他们都需要指定两者。

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

在 C++17 中,不存在“连续迭代器”这样的东西。与 RandomAccessIterator 不同。标准中有一整节解释了 RandomAccessIterator 的要求,而“连续迭代器”则获得一段语句,没有关于它的附加信息,也很少有实际用途。

当然,“连续迭代器”没有迭代器标签。这是故意这样做的,以避免添加另一个迭代器标签,并可能导致大量无法工作的代码因为连续迭代器反而将自己宣传为随机访问。

C++20 改变了一切。它添加了 a std::contiguous_iterator_tag,但这样做是因为std::contiguous_iteratornow存在语法差异std::random_access_iteratorvalue_type也就是说,连续迭代器必须允许转换为指向其via的指针std::to_pointer。这允许您将迭代器对转换为指针对,而不必取消引用潜在的不可取消引用的迭代器(例如尾后迭代器)。

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

自动赋值仅适用于 C++17 要求的原因是因为 C++20 概念是根据 std::iterator_traits. 因此,如果不创建循环定义,它就无法使用这些概念。

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

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

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

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

主模板iterator_category3 个可能的版本,具体取决于您定义迭代器类型的方式。如果您的迭代器提供了除 之外的所有成员类型 alise pointer,那么它只会使用它们。如果它只提供其中一些,那么它会concept针对 Cpp17InputIterator 的仅说明版本进行检查。如果您的类型符合该类型,那么它会使用您的类型iterator_category(如果您不提供类型,那么它会计算一个)。

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

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

但你说它是一个随机访问迭代器。系统不应该推翻你所说的;这只是如果您的类型与输入迭代器不匹配但仍然碰巧是某种迭代器时会发生的情况。

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

我仍然认为检查这个概念是否满足是有意义的,特别是在“后退”output_iterator_tag 的情况下:忘记编写 == 运算符不应将输入迭代器变成不可用的输出迭代器。

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

那么你的问题是什么?如果您意外地未能将您的类型设置为输入迭代器,您是否希望系统根据其行为将其正确分类,或者您希望它向前转发您的错误类别?