Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is comparing two spans pointing to the same container well defined?

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.

like image 304
Ahmed AEK Avatar asked Dec 17 '25 13:12

Ahmed AEK


2 Answers

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.

like image 65
Jan Schultke Avatar answered Dec 19 '25 02:12

Jan Schultke


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.

like image 34
許恩嘉 Avatar answered Dec 19 '25 01:12

許恩嘉