C++/WinRT iterators resemble iterators in many ways, most notable they resemble them enough to let you use them in ranged for loops or in standard algorithms that use input iterators.
But they aren’t full iterators, so watch out.
Most notably, the iterators for indexed collections (known internally as fast_iterator
) support random access, such as it += 2
to step forward two items, or it[-1]
to read the previous item. But they are not full-fledged random access iterators.
They aren’t proper random access iterators because the return type of the dereferencing operator*
is not a reference. It’s a value.
That means that you can’t do this:
auto it = collection.First(); *it = replacement_value; // replace the first element
If the underlying type of the collection is not a class type, then the compiler will complain:
IVector<int> collection; auto start = collection.First(); *start = 42; // C2106: '=' left operand must be an l-value
But if the underlying type is of class type, then the class’s assignment operator will be used. You’re assigning to a temporary object, which is legal, though not particularly useful.
IVector<Point> collection; auto start = collection.First(); *start = Point{ 1.0f, 2.0f }; // doesn't do what you think start[0].X = 1.0f; // doesn't do what you think IVector<Class> collection; auto start = collection.First(); *start = Class(); // doesn't do what you think
In both cases, what you’re doing is assigning a new object to (or in the case of .X
, mutating) the temporary object returned by operator*
, and then throwing the temporary away. The original collection remains unchanged.
For reference types like the imaginary Windows Runtime class Class
, this is largely not a problem, because the C++/WinRT projection of Windows Runtime classes is as a reference-counted object (similar to shared_ptr
), so you can invoke methods, and those methods will affect the underlying shared object.
But for value types like Point
, this is a hidden gotcha.
Bonus chatter: The dereferencing operator*
could have returned a proxy object which supported conversion to T
(which performs a GetAt
) or assignment from T
(which performs a SetAt
), but that would result in a different kind of confusion, because
auto v = *it;
wouldn’t actually produce the value. It would merely produce a proxy. And that would be really weird:
void Something(T value); IIterable<T> it = ...; auto v = *it; *it = new_value; Something(v); // passes new_value!
The problem is that auto v = *it
deduces that v
is a proxy object. It is only when the proxy is converted to a T
that the GetAt
is performed. In the above code fragment, that happens at the call to Something
, and that means that when GetAt
is called, it will fetch the new_value
that was assigned in the meantime.
To avoid surprises like this, the C++/WinRT iterators are strictly input iterators.
As another commentator on this blog has pointed out before, assigning to a temporary can be disallowed if you restrict operator= to only lvalues, i.e.:
Why does WinRT allow assignment to temporaries?
Another way is to return `T const` instead of `T` (e.g., for the dereferencing operator).
But that means that you won’t be able to invoke non-const methods on the returned object.
Since you can’t actually mutate the original object in the collection, what’s invoking a non-const method on the returned temporary going to achieve?
It depends on what kind of object the returned temporary is. If it's of a class or interface type then there are basically no non-const memfuns on it (since you don't mutate the pointer however you may be mutating the pointee), so it behaves normally as a smart pointer except that you cannot move from it (which requires the pointer itself being non-const). If it's a fundamental type or a struct then there...
IIRC not even standard library types forbid assigning to rvalues