Usage
I have two classes, which give me a counter in range based for loops (bit like a simple ranges v3 lib).
// Usage with l-values
std::initializer_list<int> li = {10, 11, 12, 13, 14};
for (auto [value, index] : enumerate(li))
std::cout << index << '\t' << value << std::endl;
Classes
// Iterator
template<typename iterator_type>
class enumerate_iterator {
public:
using iterator = iterator_type;
using reference = typename std::iterator_traits<iterator>::reference;
using index_type = typename std::iterator_traits<iterator>::difference_type;
private:
iterator iter;
index_type index = 0;
public:
enumerate_iterator() = delete;
explicit enumerate_iterator(iterator iter, index_type start)
: iter(iter), index(start) {}
enumerate_iterator &operator++() {
++iter;
++index;
return *this;
}
bool operator==(const enumerate_iterator &other) const { return iter == other.iter; }
bool operator!=(const enumerate_iterator &other) const { return iter != other.iter; }
std::pair<reference, const index_type &> operator*() const { return { *iter, index }; }
};
// Range
template<typename iterator_type>
class enumerate_range {
public:
using iterator = enumerate_iterator<iterator_type>;
using index_type = typename std::iterator_traits<iterator_type>::difference_type;
private:
const iterator_type first, last;
const index_type start;
public:
enumerate_range() = delete;
explicit enumerate_range(iterator_type first, iterator_type last, index_type start = 0)
: first(first), last(last), start(start) {}
iterator begin() const { return iterator(first, start); }
iterator end() const { return iterator(last, start); }
};
//*************************************************
// Usage functions
// For l-values
template<typename type>
decltype(auto) enumerate(const std::initializer_list<type>& content, std::ptrdiff_t start = 0) {
return enumerate_range(content.begin(), content.end(), start);
}
Issue: Expanding for usage with r-value std::initializer_list
Now when I want to use the above example with r-values like this
// Usage with r-values
for (auto [value, index] : enumerate({10, 11, 12, 13, 14}))
std::cout << index << '\t' << value << std::endl;
It obviously doesn't work, since the r-value std::initializer_list
causes a lifetime issue. GCC address sanitizer gives me a stack-use-after-scope error.
As a solution an overloaded enumerate
function with a r-value reference would be the first step to add. However this is where my problem begins. In the enumerates
function I somehow need to call an overloaded enumerate_range
constructor which would move the list into the enumerate_range
object to keep the r-value alive. But I have no idea how that new enumerate_range
constructor should look like or how to correctly move the list into the enumerate_range
object.
template<typename type>
decltype(auto) enumerate(std::initializer_list<type>&& content, std::ptrdiff_t start = 0) {
// Calling a new overloaded constructor, which moves the list into the enumerate_range object
return enumerate_range(content, start);
}
I tried multiple different version of a constructor using std::move
but I cannot get it to work correctly.
Question
How can I add support for r-value lists using the given construct? How should the enumerate_range
constructor for r-values look like?
One constructor version I tried looked like this:
// In class enumerate_range
template<typename type>
enumerate_range(std::initializer_list<type>&& content, index_type start = 0)
: first(std::move(content).begin()), last(std::move(content).end()), start(start)
{}
Your
enumerate
is a non-owning view into the range that it is given.Therefore it is wrong to use it as
for (auto [value, index] : enumerate(/*range prvalue*/))
. That's true for all non-owning views and all prvalue ranges as argument. The way a range-for loop works all temporaries constructed in the range-expression are destroyed before iteration of the range begins.Instead the user should declare the range in a named variable first (or in an init-statement of the range-for loop since C++20):
The only way to avoid this is to make
enumerate
an owning range, so that it contains a copy of (or moved-from) the temporary range constructed in the range-expression. And because of the special behavior ofstd::initializer_list
you can't actually just store a copy of it either. You would need to choose a container as member ofenumerate_range
to store the individual elements into.I would also not provide any
std::initializer_list
overload at all. You shouldn't have views into them. They are meant to be used to initialize containers in their constructors. Instead have only an overload that takes a rangeR&&
whereR
is a template parameter. That wayenumerate({10, 11, 12, 13, 14})
will result in a deduction failure and not silent UB.