Are end+1 iterators for std::string allowed?

前端 未结 3 1187
名媛妹妹
名媛妹妹 2020-12-15 05:04

Is it valid to create an iterator to end(str)+1 for std::string?
And if it isn\'t, why isn\'t it?

This question is restricted

相关标签:
3条回答
  • 2020-12-15 05:21

    A std::basic_string<???> is a container over its elements. Its elements do not include the trailing null that is implicitly added (it can include embedded nulls).

    This makes lots of sense -- "for each character in this string" probably shouldn't return the trailing '\0', as that is really an implementation detail for compatibility with C style APIs.

    The iterator rules for containers were based off of containers that don't shove an extra element at the end. Modifying them for std::basic_string<???> without motivation is questionable; one should only break a working pattern if there is a payoff.

    There is every reason to think that pointers to .data() and .data() + .size() + 1 are allowed (I could imagine a twisted interpretation of the standard that would make it not allowed). So if you really need read-only iterators into the contents of a std::string, you can use pointer-to-const-elements (which are, after all, a kind of iterator).

    If you want editable ones, then no, there is no way to get a valid iterator to one-past-the-end. Neither can you get a non-const reference to the trailing null legally. In fact, such access is clearly a bad idea; if you change the value of that element, you break the std::basic_string's invariant null-termination.

    For there to be an iterator to one-past-the-end, the const and non-const iterators to the container would have to have a different valid range, or a non-const iterator to the last element that can be dereferenced but not written to must exist.

    I shudder at making such standard wording watertight.

    std::basic_string is already a mess. Making it even stranger would lead to standard bugs and would have a non-trivial cost. The benefit is really low; in the few cases where you want access to said trailing null in an iterator range, you can use .data() and use the resulting pointers as iterators.

    0 讨论(0)
  • 2020-12-15 05:38

    TL;DR: s.end() + 1 is undefined behavior.


    std::string is a strange beast, mainly for historical reasons:

    1. It attempts to bring C compatibility, where it is known that an additional \0 character exists beyond the length reported by strlen.
    2. It was designed with an index-based interface.
    3. As an after thought, when merged in the Standard library with the rest of the STL code, an iterator-based interface was added.

    This led std::string, in C++03, to number 103 member functions, and since then a few were added.

    Therefore, discrepancies between the different methods should be expected.


    Already in the index-based interface discrepancies appear:

    §21.4.5 [string.access]

    const_reference operator[](size_type pos) const;
    reference operator[](size_type pos);

    1/ Requires: pos <= size()

    const_reference at(size_type pos) const; reference at(size_type pos);

    5/ Throws: out_of_range if pos >= size()

    Yes, you read this right, s[s.size()] returns a reference to a NUL character while s.at(s.size()) throws an out_of_range exception. If anyone tells you to replace all uses of operator[] by at because they are safer, beware the string trap...


    So, what about iterators?

    §21.4.3 [string.iterators]

    iterator end() noexcept;
    const_iterator end() const noexcept;
    const_iterator cend() const noexcept;

    2/ Returns: An iterator which is the past-the-end value.

    Wonderfully bland.

    So we have to refer to other paragraphs. A pointer is offered by

    §21.4 [basic.string]

    3/ The iterators supported by basic_string are random access iterators (24.2.7).

    while §17.6 [requirements] seems devoid of anything related. Thus, strings iterators are just plain old iterators (you can probably sense where this is going... but since we came this far let's go all the way).

    This leads us to:

    24.2.1 [iterator.requirements.general]

    5/ Just as a regular pointer to an array guarantees that there is a pointer value pointing past the last element of the array, so for any iterator type there is an iterator value that points past the last element of a corresponding sequence. These values are called past-the-end values. Values of an iterator i for which the expression *i is defined are called dereferenceable. The library never assumes that past-the-end values are dereferenceable. [...]

    So, *s.end() is ill-formed.

    24.2.3 [input.iterators]

    2/ Table 107 -- Input iterator requirements (in addition to Iterator)

    List for pre-condition to ++r and r++ that r be dereferencable.

    Neither the Forward iterators, Bidirectional iterators nor Random iterator lift this restriction (and all indicate they inherit the restrictions of their predecessor).

    Also, for completeness, in 24.2.7 [random.access.iterators], Table 111 -- Random access iterator requirements (in addition to bidirectional iterator) lists the following operational semantics:

    • r += n is equivalent to [inc|dec]rememting r n times
    • a + n and n + a are equivalent to copying a and then applying += n to the copy

    and similarly for -= n and - n.

    Thus s.end() + 1 is undefined behavior.

    0 讨论(0)
  • 2020-12-15 05:47

    Returns: A pointer p such that p + i == &operator[](i) for each i in [0,size()].

    std::string::operator[](size_type i) is specified to return "a reference to an object of type charT with value charT() when i == size(), so we know that that pointer points to an object.

    5.7 states that "For the purposes of [operators + and -], a pointer to a nonarray object behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type."

    So we have a non-array object and the spec guarantees that a pointer one past it will be representable. So we know std::addressof(*end(str)) + 1 has to be representable.

    However that's not a guarantee on std::string::iterator, and there is no such guarantee anywhere in the spec, which makes it undefined behavior.

    (Note that this is not the same as 'ill-formed'. *end(str) + 1 is in fact well-formed.)

    Iterators can and do implement checking logic that produce various errors when you do things like increment the end() iterator. This is in fact what Visual Studios debug iterators do with end(str) + 1.

    #define _ITERATOR_DEBUG_LEVEL 2
    #include <string>
    #include <iterator>
    
    int main() {
      std::string s = "ssssssss";
      auto x = std::end(s) + 1; // produces debug dialog, aborts program if skipped
    }
    

    And if it isn't, why isn't it?

    for consistency and interoperability with zero-terminated strings if nothing else

    C++ specifies some specific things for compatibility with C, but such backwards compatibility is limited to supporting things that can actually be written in C. C++ doesn't necessarily try to take C's semantics and make new constructs behave in some analogous way. Should std::vector decay to an iterator just to be consistent with C's array decay behavior?

    I'd say end(std) + 1 is left as undefined behavior because there's no value in trying to constrain std::string iterators this way. There's no legacy C code that does this that C++ needs to be compatible with and new code should be prevented from doing it.

    New code should be prevented from relying on it... why? [...] What does not allowing it buy you in theory, and how does that look in practice?

    Not allowing it means implementations don't have to support the added complexity, complexity which provides zero demonstrated value.

    In fact it seems to me that supporting end(str) + 1 has negative value since code that tries to use it will essentially be creating the same problem as C code which can't figure out when to account for the null terminator or not. C has enough off by one buffer size errors for both languages.

    0 讨论(0)
提交回复
热议问题