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?
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
}