Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In what situations does `ranges::for_each` work but `for (auto&& elt : rg)` fail?

Given the following C++23 code (Godbolt):

template<class R, class T>
concept container_compatible_range =
  std::ranges::input_range<R> && std::convertible_to<std::ranges::range_reference_t<R>, T>;

template<class K, class V>
struct Map {
  using value_type = std::pair<K, V>;

  template<class R>
    requires container_compatible_range<R, value_type>
  void insert_range_bad(R&& rg) {
    for (value_type e : rg) {
      // ~~~~
    }
  }

  template<class R>
    requires container_compatible_range<R, value_type>
  void insert_range_good(R&& rg) {
    std::ranges::for_each(rg, [&](value_type e) {
      // ~~~~
    });
  }
};

int main() {
  Map<int, int> m;
  std::tuple<int, int> a[2] = {};  // for example
  m.insert_range_bad(a);
  m.insert_range_good(a);
}

I've been told that there are "contrived corner cases" where insert_range_good will successfully compile, but insert_range_bad will fail (in a SFINAE-unfriendly, hard-error kind of way) and/or Do The Wrong Thing.

What are these corner cases?

like image 226
Quuxplusone Avatar asked Oct 21 '25 23:10

Quuxplusone


1 Answers

Range-based for loops extract the range's iterator and sentinel in a consistent way, which means both must be obtained through member function or free functions.

This is not case for ranges::for_each, as both can be retrieved in different ways via ranges::begin and ranges::end respectively:

struct EvilRange {
  std::tuple<int, int>* begin() const;
};

std::tuple<int, int>* end(EvilRange&);

int main() {
  Map<int, int> m;
  EvilRange evil;
  m.insert_range_bad(evil);  // failed
  m.insert_range_good(evil); // ok
}

Another contrived case is to delete the begin/end members but enable free begin/end functions; the class still satisfies the range concept as ranges::begin/ranges::end checks the validity of the expression, whereas range-based for loops does not:

struct EvilRange2 {
  void begin() = delete;
  void end() = delete;
};

std::tuple<int, int>* begin(EvilRange2&);
std::tuple<int, int>* end(EvilRange2&);

int main() {
  Map<int, int> m;
  EvilRange2 evil;
  m.insert_range_bad(evil);  // failed
  m.insert_range_good(evil); // ok
}
like image 126
康桓瑋 Avatar answered Oct 23 '25 11:10

康桓瑋



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!