3

Is behavior of std::distance undefined when called on a pair of std::vector iterators that have been invalidated by moving the vector?

For context: I'm writing copy and move constructors for a class that has a vector of data and a vector of iterators that point to that data. Once I move the data to its destination, I need to translate the vector of iterators to point to the new container. I would like to avoid creating intermediate index representation in memory.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
tsuki
  • 907
  • 4
  • 17
  • According to the standard - no, because one iterator is not longer reachable from the other. In practice it will probably work fine, but I wouldn't use it this way. – Rinat Veliakhmedov Jun 16 '20 at 09:00
  • @RinatVeliakhmedov _"According to the standard"_ Can you link the paragraph? – Thomas Sablik Jun 16 '20 at 09:01
  • @ThomasSablik I assume that cppreference part here is from the standard https://en.cppreference.com/w/cpp/iterator/distance "If InputIt is not LegacyRandomAccessIterator, the behavior is undefined if last is not reachable from first by (possibly repeatedly) incrementing first. If InputIt is LegacyRandomAccessIterator, the behavior is undefined if last is not reachable from first and first is not reachable from last. " – Rinat Veliakhmedov Jun 16 '20 at 09:04
  • @RinatVeliakhmedov cppreference is not the standard. Undefined is not illegal. – Thomas Sablik Jun 16 '20 at 09:06
  • i'd say no, but I am curious for a good answer, because I have a hard time to find any information on what one can actually do with invalidated iterators – 463035818_is_not_an_ai Jun 16 '20 at 09:06
  • @RinatVeliakhmedov your premise is that last cannot be reached from first to arrive at the conclusion that last cannot be reached from first, that is a moot argument – 463035818_is_not_an_ai Jun 16 '20 at 09:07
  • @ThomasSablik 23.4.2 "Preconditions: last is reachable from first, or InputIterator meets the Cpp17RandomAccessIterator requirements and first is reachable from last." – Rinat Veliakhmedov Jun 16 '20 at 09:10
  • Iterators are for iterating, not for long-term storage of indirections. If you use indices to begin with, you don't need translation or intermediates and you don't need to worry about invalidation. – molbdnilo Jun 16 '20 at 09:10
  • @ThomasSablik I clarified what I mean by legal. I'm not sure why would invalidated iterators be unreachable. – tsuki Jun 16 '20 at 09:12
  • @molbdnilo This data structure is an intermediate result of algorithms that operate on iterators and do indeed iterate. I figured that storing indices would be wasteful and I only need to copy these structures for strong exception guarantee. – tsuki Jun 16 '20 at 09:15
  • 3
    So this is an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). I would take a step back and refocus on the original problem instead of the problems caused by a proposed solution to that problem. – molbdnilo Jun 16 '20 at 09:18
  • @tsuki What operations on the data-vector are you doing that invalidates the iterators? – Ted Lyngmo Jun 16 '20 at 12:05
  • 1
    @TedLyngmo Moving the vector. – tsuki Jun 16 '20 at 12:49
  • @tsuki Why do you think that invalidates the iterators? See my answer. – Ted Lyngmo Jun 16 '20 at 12:52
  • @TedLyngmo https://stackoverflow.com/a/11022447/2571986 states that moving invalidates the iterators, but it does indeed seem like `std::swap` is a different case. – tsuki Jun 16 '20 at 13:29
  • @tsuki See my updated answer. I claim both move and swap preserves iterator validity. – Ted Lyngmo Jun 16 '20 at 15:00

1 Answers1

1

Is behavior of std::distance undefined when called on a pair of std::vector iterators that have been invalidated by moving the vector?

If the iterators are valid before the move, they will remain valid after the move - so you don't need to recalculate them using std::distance.

(emphasis mine below)

std::vector::vector

After container move construction, references, pointers, and iterators (other than the end iterator) to other remain valid, but refer to elements that are now in *this.

The current standard makes this guarantee via the blanket statement in [container.requirements.general/12], and a more direct guarantee is under consideration via LWG 2321.

[container.requirements.general/12] states that

Unless otherwise specified (either explicitly or by defining a function in terms of other functions), invoking a container member function or passing a container as an argument to a library function shall not invalidate iterators to, or change the values of, objects within that container.

The same blanket statement goes for the move assignment operator and this means that, in accordance with the standard, the iterators will stay valid after the move.

The current wording in LWG 2321 gives a hint of what a new paragraph in the standard could look like if the library working group finalize this - which seems to be hard. LWG 2321 was opened back in 2013.

no move constructor (or move assignment operator when allocator_traits<allocator_type>::propagate_on_container_move_assignment::value is true) of a container (except for array) invalidates any references, pointers, or iterators referring to the elements of the source container. [Note: The end() iterator does not refer to any element, so it may be invalidated. — end note]

If that's too vague, you can use

[container.requirements.general/11.6]

no swap() function invalidates any references, pointers, or iterators referring to the elements of the containers being swapped. [ Note: The end() iterator does not refer to any element, so it may be invalidated. — end note ]

If the iterators are valid before you swap, they are valid after the swap.

Here's an example class using the guarantee given for swap:

#include <vector>

class Foo {
    std::vector<int> data{};
    std::vector<decltype(data)::iterator> dits{};

public:
    Foo() = default;

    Foo(const Foo&) = delete;    // here, dits would need to be calculated

    // A move constructor guaranteed to preserve iterator validity.
    Foo(Foo&& rhs) noexcept {
        data.swap(rhs.data);
        dits.swap(rhs.dits);
    }

    Foo& operator=(const Foo&) = delete;

    // A move assignment operator guaranteed to preserve iterator validity.
    Foo& operator=(Foo&& rhs) noexcept {
        data.swap(rhs.data);
        dits.swap(rhs.dits);
        return *this;
    }

    ~Foo() = default;
};
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • The question is about calculating the distance between two invalidated iterators. The invalidated state is not questionable then. – Nicolas Dusart Jun 16 '20 at 10:07
  • 1
    @NicolasDusart Yes, but when I read the question I interpreted it like OP thinks the iterators are invalidated just _because_ of the move - so I wanted to make it clear that as long as the iterators are valid, you may swap the target container and the iterators will still be valid. – Ted Lyngmo Jun 16 '20 at 10:43
  • @NicolasDusart ... and now OP has verified my suspicion and I've read about it a bit more and claim that even a move preserves the iterators validity. – Ted Lyngmo Jun 16 '20 at 15:42
  • @NicolasDusart No, that wasn't what I meant :-) I thought you were curious, so I tagged you. – Ted Lyngmo Jun 16 '20 at 19:50