Keywords: C++ range-based for loops | custom type adaptation | iterator design | C++17 sentinel | ADL lookup
Abstract: This article provides an in-depth exploration of the C++11 range-based for loop mechanism, detailing how to adapt custom types to this syntactic feature. By analyzing the evolution of standard specifications, from C++11's begin/end member or free function implementations to C++17's support for heterogeneous iterator types, it systematically explains implementation principles and best practices. The article includes concrete code examples covering basic adaptation, third-party type extension, iterator design, and C++20 concept constraints, offering comprehensive technical reference for developers.
Mechanism of Range-based For Loops
The C++11 range-based for loop syntax for (Type& v : a) is not a new control structure but syntactic sugar built upon existing iterator mechanisms. According to the standard specification, this statement expands to:
{
auto && __range = range_expression;
for (auto __begin = begin_expr, __end = end_expr; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
Here begin_expr and end_expr are determined through specific lookup rules: member functions begin()/end() are called first, followed by argument-dependent lookup (ADL) for free functions in the same namespace, with special handling for C-style arrays. Notably, the standard library function std::begin() is only invoked when the type belongs to the std namespace.
Basic Adaptation Approaches
To adapt a custom type X for range-based for loops, iteration start and end points must be provided through two equivalent methods:
Member Function Approach
Define begin() and end() member functions within the type, returning iterator-like objects:
struct egg_carton {
auto begin() { return eggs.begin(); }
auto end() { return eggs.end(); }
auto begin() const { return eggs.begin(); }
auto end() const { return eggs.end(); }
private:
std::vector<egg> eggs;
};
This approach directly reuses the underlying container's iterators, offering simplicity and efficiency. In C++11, explicit return types are required, while C++14 onward supports auto deduction.
Free Function Approach
Define free functions in the type's namespace, suitable for third-party types that cannot be modified:
namespace library_ns {
struct some_struct_you_do_not_control {
std::vector<int> data;
};
int* begin(some_struct_you_do_not_control& x) {
return x.data.data();
}
int* end(some_struct_you_do_not_control& x) {
return x.data.data() + x.data.size();
}
}
This non-intrusive extension leverages ADL to be recognized by range-based for loops.
Iterator Requirements and Design
The returned iterator-like objects must satisfy minimal interface requirements:
- Support prefix increment operator
++ - Support inequality comparison operator
!=, returning a value convertible to boolean context - Support dereference operator
*, returning a type initializable to the loop variable - Public destructor
A complete iterator implementation example:
template <typename DataType>
class PodArray {
public:
class iterator {
public:
iterator(DataType* ptr) : ptr(ptr) {}
iterator operator++() { ++ptr; return *this; }
bool operator!=(const iterator& other) const {
return ptr != other.ptr;
}
const DataType& operator*() const { return *ptr; }
private:
DataType* ptr;
};
iterator begin() const { return iterator(val); }
iterator end() const { return iterator(val + len); }
private:
unsigned len;
DataType* val;
};
C++17 Heterogeneous Iterator Support
C++17 revised the range-based for loop expansion, decoupling begin and end types:
{
auto && __range = range_expression;
auto __begin = begin_expr;
auto __end = end_expr;
for (; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
This improvement allows end() to return a sentinel of different type than begin(), requiring only != comparison support. A typical application optimizes C-style string iteration:
struct null_sentinel_t {
template<class Rhs,
std::enable_if_t<!std::is_same<Rhs, null_sentinel_t>{}, int> = 0
>
friend bool operator==(Rhs const& ptr, null_sentinel_t) {
return !*ptr;
}
// Symmetric comparison operators omitted
};
struct cstring {
const char* ptr = nullptr;
const char* begin() const { return ptr ? ptr : ""; }
null_sentinel_t end() const { return {}; }
};
This design enables for (char c : cstring{"abc"}) to generate machine code equivalent to hand-written C loops.
Range Views and C++20 Enhancements
Iterator pairs can construct generic range views, with C++20 concept constraints enhancing safety:
template<class It>
struct range_t {
It b, e;
It begin() const { return b; }
It end() const { return e; }
std::size_t size() const
requires std::random_access_iterator<It>
{
return end() - begin();
}
bool empty() const { return begin() == end(); }
range_t without_front(std::size_t n) const
requires std::random_access_iterator<It>
{
n = (std::min)(n, size());
return {b + n, e};
}
};
template<class C>
auto make_range(C&& c) {
using std::begin; using std::end;
return range_t{begin(c), end(c)};
}
Usage example: for (auto x : make_range(v).without_front(2)) skips the first two elements.
Considerations and Best Practices
Implementation considerations: range-based for loops bind temporary objects to rvalue references auto&& then pass them as lvalues, preventing temporary-specific overloads. Return types need not strictly conform to iterator standards but should maintain compatibility to avoid future standard changes. For const iteration support, provide cbegin()/cend() and const overloads.
By systematically implementing range-based for loop adaptation for custom types, developers can significantly improve code readability and expressiveness while leveraging modern C++ syntactic advancements.