return 引用 InputIterator 内部状态是否合法?

Is it legal to return reference to an InputIterator internal state?

让我们有一个符合 Cpp17InputIterator 的输入迭代器 it。做完it++能保证参考值*it不变吗?例如,

const auto &old_ref = *it;
auto old_val = ref;
++it; // old_ref might be affected by this

assert(old_ref == old_val); // Is this guaranteed for Cpp17InputIterator?

table 表示 it 的旧副本不需要可取消引用。但这是否意味着从 it 获得的旧引用也可能无法取消引用? *it return 可以引用迭代器的内部状态吗?

永远不要假设在迭代器失效时引用仍然有效。 可能一些迭代器实现的情况,但这样做违反了迭代器的概念,并且不会对所有迭代器通用。

将迭代器在内部实现为 std::optional<T> 是完全合法的,可以 return 引用 T 并在每次迭代之间重建 T。在 Inputiterator 上尤其如此,它不需要多通道支持(例如发电机范围)。

例如,执行以下操作的迭代器是完全合法的:

template <typename T>
auto some_special_iterator<T>::operator*() -> T&
{
  return *m_value; // returns a reference to the currently stored T
}

template <typename T>
auto some_special_iterator<T>::operator++() -> some_special_iterator&
{
  m_value.clear(); // Destroys the object which someone may be holding a reference to
  m_value.emplace( ... ); // Invalidates any existing references by constructing a new object

  return (*this);
}

使用指向已销毁对象的指针或引用是未定义的行为,即使该指针或引用指向新对象的相同存储。对新建对象的唯一合法指针或引用是由 new 编辑的 return(例如放置 new)或 std::launder.


因为它被标记了 :在直接来自标准的引号行中并没有太多可以将其定义为非法的,因为 InputIterator 没有保证允许保留引用保持有效的概念。

所以要证明这是未定义的行为,我们需要倒推。首先:

来自 defns.undefined

Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data

因此我们需要检查上面的迭代器示例是否正确遵守 InputIterator 的概念定义,其中重要的部分是 operator++ 行为:

来自input.iterators中的table:

Requires: r is dereferenceable.
Postconditions: r is dereferenceable or r is past-the-end; any copies of the previous value of r are no longer required either to be dereferenceable or to be in the domain of ==.

(强调我的)

在上述要求中,迭代器 r 之前不可取消引用的条件在上述示例中得到支持,就像后置条件也得到支持一样。

有趣的是我加粗的部分:“r 的先前值的任何副本不再需要可取消引用或位于 == 的域中“

这意味着迭代器本身的任何现有副本可能不再是可取消引用的,也可能无法在与另一个迭代器相同的范围内正确执行比较。这是正式意味着迭代器的所有副本可能已失效(注意:“可能”,因为迭代器不需要无效——但应该假设有)。

C++ 标准文档没有明确声明任何持有的引用仍然有效,因为这不是进程定义的行为;但是,如果迭代器本身在调用 operator++ 后不再被视为“可取消引用”,则还应假定其引用不再有效。由于措辞并未说明在这一点之后保持引用保证保持有效,因此必须假定由于上述来自 defns.undefined.

的段落而未定义的行为

上面说明的示例是一个符合规范的迭代器实现,其中这种期望会导致实际的未定义行为,这符合这种解释。


换句话说,请注意将 const auto& 与输入迭代器一起使用。

operator*() 在输入迭代器上只需要 return 一个 It::reference 类型, 可以转换为 T;它实际上根本不需要作为参考。请注意,这在前向迭代器中得到了加强,而且“reference 必须是对 T 的引用”,但输入迭代器并非如此。

在这里使用 const auto& 实际上可能会导致您无意中 const-lifetime-extend 一个临时代理对象,而不是持有一个真正的引用。