17

Is the comparison of begin() and end() iterators of two std::spans that point to the same memory well defined?

#include <span>
#include <cassert>

int main() {
    int arr[5]{1,2,3,4,5};
    std::span s1{arr};
    std::span s2{arr};
    assert(s1.begin() == s2.begin());
    assert(s1.end() == s2.end());
    assert(s1.begin() + s1.size() == s2.end());
}

All asserts pass on all implementations of std::span so far, but is there anything I may be missing that makes this a bug, e.g., UB?

For context, this can arise if you have a class that tries to hide its internals with a span.

class some_class
{
public:
  std::span<int> vec_view() { return vec; }
private:
  std::vector<int> vec;
};

int main() {
    some_class c;
    std::for_each(c.vec_view().begin(), c.vec_view().end(), [](auto&){});
}

This is related to Does C++ allow comparison between std::span::iterators when one span is a subspan of the other?, but this question is not about a std::subspan, and moreover the std::span version also passes MSVC asserts, unlike the version with the std::subspan.

6
  • Maybe related of stackoverflow.com/questions/76773345/… Commented Apr 25 at 7:33
  • @康桓瑋 maybe, althought the answer there says they must point to the same sequence, and those are pointing to the same sequence, i think it is not defined according to this cplusplus.github.io/LWG/issue3989 Commented Apr 25 at 7:38
  • Generally, the iterators are specific to the container object. But is std::span really a container in the same sense as e.g. std::vector? Commented Apr 25 at 7:40
  • 1
    when in doubt you can still do &(*s1.begin()) == &(*s2.begin()) Commented Apr 25 at 16:14
  • @463035818_is_not_an_ai that's valid if container is not empty, but isn't that UB for end() or begin() of empty container? Commented Apr 27 at 7:17

2 Answers 2

19

Is comparison of begin() and end() iterators of two std::spans that point to the same memory well-defined?

Who knows. std::span::iterator models contiguous_iterator ([span.iterators]), and therefore, forward_iterator. [iterator.concept.forward] paragraph 2 applies:

The domain of == for forward iterators is that of iterators over the same underlying sequence. However, value-initialized iterators [...].

The C++ standard never defines what an "underlying sequence" is.

  • If the "underlying sequence" is the object that we call .begin() and .end() on, then std::span(...).begin() == std::span(...).begin() is undefined. no matter what.
  • If the "underlying sequence" is the sequence of elements obtained by applications of operator* on the iterators, then s1.begin() == s2.begin() has to be true.

The first option is needlessly restrictive for non-owning ranges like std::string_view and std::span. The second option may have unintended consequences, like being able to compare iterators from entirely different std::ranges::filter_views created with a function pointer as a predicate (same iterator type, same elements, but comparison is nonsensical). Some language design decision will need to be made here, possibly case-by-case.

As you've already pointed out, there is an active LWG Issue 3989, and until that is resolved, there won't be a definitive answer to this question, even for two std::spans that are equivalent, not just subspans.

Sign up to request clarification or add additional context in comments.

Comments

3

All asserts pass on all implementations of std::span so far, but is there anything I may be missing that makes this a bug, e.g., UB?

I don't think this would be undefined behavior. After all, std::span is just a class, not part of the core language, so I think this should only be unspecified behavior.

span<ElementType, Extent> is a trivially copyable type.
For a span s, any operation that invalidates a pointer in the range [s.data(), s.data() + s.size()) invalidates pointers, iterators, and references to elements of s.

A span::iterator is not allowed to hold a reference to the source span, so in addition to a pointer to the referenced element, it can only hold two additional pieces of information: span.data() and span.size().

Therefore, as long as span1.data() == span2.data() && span1.size() == span2.size(), it is guaranteed that iterators from different spans can be compared.

10 Comments

As long as it's not undefined behavior, I believe other potential issues are just minor issues.
I don't see what being a class has to do with UB. std::vector v {13}; v[1]; is UB even though std::vector is also "just a class".
Whether or not something "passes the inspection" or gives us the expected results does not mean that it works or is valid at all. UB basically means that anything can happen, including getting the expected results.
@Fareanor unspecified behavior - The behavior of the program varies between implementations, and the conforming implementation is not required to document the effects of each behavior. Each unspecified behavior results in one of a set of valid results. undefined behavior - There are no restrictions on the behavior of the program. I don't think std::span::iterator::operator==() will trigger undefined behavior.
@Yksisarvinen Comparing two pointers that point to the same memory is not UB in itself, and I don't think any reasonable encapsulation would make it UB, at most it would be unspecified behavior. On the other hand, accessing invalid memory is UB itself.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.