KaiserJohaan
KaiserJohaan

Reputation: 9240

Custom container range-based iteration

I have a custom container which I want to use in a ranged-based for loop. The container is somewhat based on a vector, like this:

template<typename T>
class IDMap
{
private:
    struct Item {
        uint16_t mVersion;
        T mItem;

        template <typename... Arguments>
        Item(uint16_t version, Arguments&&... args) : mVersion(version), mItem(args...)
        {
        }
    };


public:
    typedef uint32_t ItemID;

    template <typename... Arguments>
    ItemID AddItem(Arguments&&... args);

    void MarkAsFree(const ItemID id);

    T& GetItem(const ItemID id);

    T* TryGetItem(const ItemID id);

    void Clear();


private:
    std::vector<Item> mItems;
    std::vector<uint16_t> mFreeIndices;
};

I want to iterate the mItems vector, but only return the mItem member rather than the entire Item struct. Is there any easy/elegant way to do this?

Upvotes: 6

Views: 2233

Answers (2)

leemes
leemes

Reputation: 45665

You have to provide a begin and end function, both returning a corresponding iterator, which itself implements operators ++, != and *. The begin and end functions can be free-standing or as members.

Start with implementing an iterator which has the behavior you want. You can implement it as a wrapper around a std::vector::iterator to save most of the "core" work.

The following is untested code

Basically, inside class IDMap, add:

class ItemIterator {
    // based on vector iterator
    std::vector<Item>::iterator i;
public:
    ItemIterator(std::vector<Item>::iterator i) : i(i) {}

    // incrementing
    ItemIterator & operator ++() { ++i; return *this; }
    ItemIterator operator ++(int) { const_iterator old(*this); ++(*this); return old; }

    // comparison
    bool operator!=(const ItemIterator &o) const { return i != o.i; }

    // dereferencing
    const T & operator*() const { return i->mItem; }
};

using iterator = ItemIterator;
using value_type = T;

ItemIterator begin() const { return ItemIterator(mItems.begin()); }
ItemIterator end()   const { return ItemIterator(mItems.end()  ); }

If you ever want to support multiple kinds of "special iteration" over your IDMap, like for example also over the indices, or over the "whole" Items, you should wrap everything above in another adaptor. This adaptor can then be accessed with a member method, like .items().

Brief example:

class IDMap {
    // (your code)

public:
    struct ItemsAdaptor {
        // (insert above iterator definition + usings)

        ItemsAdaptor(std::vector<Item>::iterator b,
                     std::vector<Item>::iterator e)
            : b{b}, e{e}
        {}

        ItemIterator begin() const { return b; }
        ItemIterator end()   const { return e; }

    private:
        ItemIterator b, e;
    };

    ItemsAdaptor items() const {
        return ItemsAdaptor(mItems.begin(), mItems.end());
    }
};

Then, you can write:

IDMap<int> map = ...;

for (int i : map.items()) {
    ...
}

Upvotes: 6

Armen Tsirunyan
Armen Tsirunyan

Reputation: 132974

If you want the range-based for to work for your container, you have to provide begin and end functions that return forward iterators.

typedef std::vector<Item>::iterator iterator;
typedef std::vector<Item>::const_iterator const_iterator;
iterator begin()
{
   return mItems.begin();
}
const_iterator begin() const;
{
   return mItems.begin();
}
//also add end functions, and viola.

This will return the whole item struct. If you have to only return mItem, you'll have to write your own iterator adaptor and use it instead of vector's.

Upvotes: 0

Related Questions