Skip to content

stackoverflow How to make my custom type to work with “range-based for loops”?

A

The standard has been changed since the question (and most answers) were posted in the resolution of this defect report.

The way to make a for(:) loop work on your type X is now one of two ways:

  • Create member X::begin() and X::end() that return something that acts like an iterator
  • Create a free function begin(X&) and end(X&) that return something that acts like an iterator, in the same namespace as your type X

And similar for const variations. This will work both on compilers that implement the defect report changes, and compilers that do not.

C++11 range-for

The objects returned do not have to actually be iterators. The for(:) loop, unlike most parts of the C++ standard, is specified to expand to something equivalent to:

for( range_declaration : range_expression )

becomes:

{
  auto && __range = range_expression ;
  for (auto __begin = begin_expr,
            __end = end_expr;
            __begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

where the variables beginning with __ are for exposition only, and begin_expr and end_expr is the magic that calls begin/end

Requirements on the begin/end return value

The requirements on the begin/end return value are simple: You must overload pre-++, ensure the initialization expressions are valid, binary != that can be used in a boolean context, unary * that returns something you can assign-initialize range_declaration with, and expose a public destructor.

Doing so in a way that isn't compatible with an iterator is probably a bad idea, as future iterations of C++ might be relatively cavalier about breaking your code if you do.

C++17 range-for

As an aside, it is reasonably likely that a future revision of the standard will permit end_expr to return a different type than begin_expr. This is useful in that it permits "lazy-end" evaluation (like detecting null-termination) that is easy to optimize to be as efficient as a hand-written C loop, and other similar advantages.


¹ Note that for(:) loops store any temporary in an auto&& variable, and pass it to you as an lvalue. You cannot detect if you are iterating over a temporary (or other rvalue); such an overload will not be called by a for(:) loop. See [stmt.ranged] 1.2-1.3 from n4527.

² Either call the begin/end method, or ADL-only lookup of free function begin/end, or magic for C-style array support. Note that std::begin is not called unless range_expression returns an object of type in namespace std or dependent on same.


In c++17 the range-for expression has been updated

{
  auto && __range = range_expression ;
  auto __begin = begin_expr;
  auto __end = end_expr;
  for (;__begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

with the types of __begin and __end have been decoupled.

This permits the end iterator to not be the same type as begin. Your end iterator type can be a "sentinel" which only supports != with the begin iterator type.

C++17 range-for Example: C string

A practical example of why this is useful is that your end iterator can read "check your char* to see if it points to '0'" when == with a char*. This allows a C++ range-for expression to generate optimal code when iterating over a null-terminated char* buffer.

struct null_sentinal_t {
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(Rhs const& ptr, null_sentinal_t) {
    return !*ptr;
  }
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(Rhs const& ptr, null_sentinal_t) {
    return !(ptr==null_sentinal_t{});
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(null_sentinal_t, Lhs const& ptr) {
    return !*ptr;
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(null_sentinal_t, Lhs const& ptr) {
    return !(null_sentinal_t{}==ptr);
  }
  friend bool operator==(null_sentinal_t, null_sentinal_t) {
    return true;
  }
  friend bool operator!=(null_sentinal_t, null_sentinal_t) {
    return false;
  }
};

live example of this.

Minimal test code is:

struct cstring {
  const char* ptr = 0;
  const char* begin() const { return ptr?ptr:""; }// return empty string if we are null
  null_sentinal_t end() const { return {}; }
};

cstring str{"abc"};
for (char c : str) {
    std::cout << c;
}
std::cout << "\n";

完整测试程序

#include <utility>
#include <iterator>
#include <iostream>

struct null_sentinal_t
{
    template<class Rhs, std::enable_if_t<!std::is_same<Rhs, null_sentinal_t> { }, int> = 0>
    friend bool operator==(Rhs const &ptr, null_sentinal_t)
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return !*ptr;
    }
    template<class Rhs, std::enable_if_t<!std::is_same<Rhs, null_sentinal_t> { }, int> = 0>
    friend bool operator!=(Rhs const &ptr, null_sentinal_t)
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return !(ptr == null_sentinal_t { });
    }
    template<class Lhs, std::enable_if_t<!std::is_same<Lhs, null_sentinal_t> { }, int> = 0>
    friend bool operator==(null_sentinal_t, Lhs const &ptr)
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return !*ptr;
    }
    template<class Lhs, std::enable_if_t<!std::is_same<Lhs, null_sentinal_t> { }, int> = 0>
    friend bool operator!=(null_sentinal_t, Lhs const &ptr)
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return !(null_sentinal_t { } == ptr);
    }
    friend bool operator==(null_sentinal_t, null_sentinal_t)
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return true;
    }
    friend bool operator!=(null_sentinal_t, null_sentinal_t)
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return false;
    }
};

struct cstring
{
    const char *ptr = 0;
    const char* begin() const
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return ptr;
    }
    null_sentinal_t end() const
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return
        {};
    }
};

int main()
{
    cstring str { "abc" };
    for (char c : str)
    {
        std::cout << c;
    }
    std::cout << "\n";
}
// g++ --std=c++17 test.cpp

输出如下:

[48]begin
[53]end
[16]operator!=
[10]operator==
a[16]operator!=
[10]operator==
b[16]operator!=
[10]operator==
c[16]operator!=
[10]operator==

分析:

1、在range-for中,会调用如下函数:

    template<class Rhs, std::enable_if_t<!std::is_same<Rhs, null_sentinal_t> { }, int> = 0>
    friend bool operator!=(Rhs const &ptr, null_sentinal_t)
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return !(ptr == null_sentinal_t { });
    }

2、这个函数,会调用如下函数:

    template<class Rhs, std::enable_if_t<!std::is_same<Rhs, null_sentinal_t> { }, int> = 0>
    friend bool operator==(Rhs const &ptr, null_sentinal_t)
    {
        std::cout << "[" << __LINE__ << "]" << __FUNCTION__ << std::endl;
        return !*ptr;
    }

C++17 range-for Example

Here is a simple example.

namespace library_ns {
  struct some_struct_you_do_not_control {
    std::vector<int> data;
  };
}

Your code:

namespace library_ns {
  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(); }
  int const* cbegin(some_struct_you_do_not_control const& x){ return x.data.data(); }
  int* cend(some_struct_you_do_not_control const& x){ return x.data.data()+x.data.size(); }
  int const* begin(some_struct_you_do_not_control const& x){ return cbegin(x); }
  int const* end(some_struct_you_do_not_control const& x){ return cend(x); }
}

this is an example how you can augment a type you don't control to be iterable.

C++17 range-for Example

Here I return pointers-as-iterators, hiding the fact I have a vector under the hood.

For a type you do own, you can add methods:

struct egg {};
struct egg_carton {
  auto begin() { return eggs.begin(); }
  auto end() { return eggs.end(); }
  auto cbegin() const { return eggs.begin(); }
  auto cend() const { return eggs.end(); }
  auto begin() const { return eggs.begin(); }
  auto end() const { return eggs.end(); }
private:
  std::vector<egg> eggs;
};

here I reuse the vector's iterators. I use auto for brevity; in c++11 I'd have to be more verbose.

完整测试程序

#include <utility>
#include <iterator>
#include <iostream>
#include <vector>

struct egg
{
    int id { 0 };
    egg(int i) :
                    id { i }
    {
    }
};
struct egg_carton
{
    auto begin()
    {
        return eggs.begin();
    }
    auto end()
    {
        return eggs.end();
    }
    auto cbegin() const
    {
        return eggs.begin();
    }
    auto cend() const
    {
        return eggs.end();
    }
    auto begin() const
    {
        return eggs.begin();
    }
    auto end() const
    {
        return eggs.end();
    }
    void add(int id)
    {
        eggs.emplace_back(id);
    }
private:
    std::vector<egg> eggs;
};
int main()
{
    egg_carton ec;
    for (int i = 0; i < 10; ++i)
    {
        ec.add(i);
    }
    for (auto &&e : ec)
    {
        std::cout << e.id << std::endl;
    }
    std::cout << "\n";
}
// g++ --std=c++14 test.cpp

输出如下:

0
1
2
3
4
5
6
7
8
9

C++17 range-for Example

Here is a quick and dirty iterable range-view:

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  std::size_t size() const { return end()-begin(); }
  bool empty() const { return begin()==end(); }

  range_t without_back( std::size_t n = 1 ) const {
    n = (std::min)(n, size());
    return {begin(), end()-n};
  }
  range_t without_front( std::size_t n = 1 ) const {
    n = (std::min)(n, size());
    return {begin()+n, end()};
  }
  decltype(auto) front() const { return *begin(); }
  decltype(auto) back() const { return *(std::prev(end())); }
};
template<class C>
auto make_range( C&& c ) {
  using std::begin; using std::end;
  return range_t{ begin(c), end(c) };
}

using c++17 template class deduction.

std::vector<int> v{1,2,3,4,5};
for (auto x : make_range(v).without_front(2) ) {
  std::cout << x << "\n";
}

prints 3 4 5, skipping first 2.

完整测试程序

#include <utility>
#include <iterator>
#include <iostream>
#include <vector>
#include <algorithm>

template<class It>
struct range_t
{
    It b, e;
    It begin() const
    {
        return b;
    }
    It end() const
    {
        return e;
    }
    std::size_t size() const
    {
        return end() - begin();
    }
    bool empty() const
    {
        return begin() == end();
    }

    range_t without_back(std::size_t n = 1) const
    {
        n = (std::min)(n, size());
        return
        {   begin(), end()-n};
    }
    range_t without_front(std::size_t n = 1) const
    {
        n = (std::min)(n, size());
        return
        {   begin()+n, end()};
    }
    decltype(auto) front() const
    {
        return *begin();
    }
    decltype(auto) back() const
    {
        return *(std::prev(end()));
    }
};
template<class C>
auto make_range(C &&c)
{
    using std::begin;
    using std::end;
    return range_t<decltype(begin(c))> { begin(c), end(c) };
}
int main()
{
    std::vector<int> v { 1, 2, 3, 4, 5 };
    for (auto x : make_range(v).without_front(2))
    {
        std::cout << x << "\n";
    }
}
// g++ --std=c++14 test.cpp

输出如下:

3
4
5

A

I write my answer because some people might be more happy with simple real life example without STL includes.

I have my own plain only data array implementation for some reason, and I wanted to use the range based for loop. Here is my solution:

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;
    };
private:
    unsigned len;
    DataType *val;
public:
    iterator begin() const
    {
        return iterator(val);
    }
    iterator end() const
    {
        return iterator(val + len);
    }

    // rest of the container definition not related to the question ...
};

Then the usage example:

PodArray<char> array;
// fill up array in some way
for(auto& c : array)
  printf("char: %c\n", c);