简体   繁体   中英

r-value lifetime issue (stack-use-after-scope): How to move std::initializer_list

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):

auto range = {10, 11, 12, 13, 14};

for (auto [value, index] : enumerate(range))
//...

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 of std::initializer_list you can't actually just store a copy of it either. You would need to choose a container as member of enumerate_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 range R&& where R is a template parameter. That way enumerate({10, 11, 12, 13, 14}) will result in a deduction failure and not silent UB.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM