2

I want to create a custom map that that actually uses a fixed set of keys, but should behave like a std::map. Basically I use an array internally and map the keys to indexes, allowing very efficient lookup. I am however struggling to implement iterators that behave like std::map iterators, because I do not have internal std::pairs that I can hand out references to.

Is it possible to implement this as a zero-overhead abstraction while retaining the std::map interface, in particular the iterators?

The best i could come up with as operator* is to return a rvalue std::pair<key_type, mapped_type*>, which basically allows for the same operations, but unfortunately with different usage patterns.

I have also tried std::pair<key_type, boost::referene_wrapper<mapped_type>>, but that still doesn't allow for(auto& elem : map) and often requires elem.second.get() for reasons I do not understand.

I am happy to use boost or lightweight header libraries, if there is anything that helps for the use case.

To illustrate the case, here is a minimal example with a map that contains all letters 'a'-'z'.

using letter = char;
static const letter letter_begin = 'a';
static const letter letter_end = 'z' + 1;

template <typename T>
class letter_map
{
private:
    using self = letter_map<T>;

    template <typename IT, typename M>
    class iterator_base : public std::iterator<std::input_iterator_tag, T>
    {
    public:
        iterator_base(letter index, M& map) : index_(index), data_(map)
        {
        }

        using self_iter = iterator_base<IT, M>;
        IT operator*()
        {
            return IT(index_, &data_[index_]);
        }

        self_iter& operator++()
        {
            index_++;
            return *this;
        }

        self_iter operator++(int)
        {
            self_iter tmp(*this);
            operator++();
            return tmp;
        }

        bool operator==(self_iter other) const
        {
            assert(&data_ == &other.data_);
            return index_ == other.index_;
        }

        bool operator!=(self_iter other) const
        {
            return !(*this == other);
        }

    private:
        letter index_;
        M& data_;
    };

public:
    using key_type = letter;
    using mapped_type = T;
    using value_type = std::pair<const key_type, mapped_type*>;
    using const_value_type = std::pair<const key_type, const mapped_type*>;

private:
    static const size_t data_size = letter_end - letter_begin;
    using container_type = std::array<mapped_type, data_size>;

public:
    using iterator = iterator_base<value_type, self>;
    using const_iterator = iterator_base<const_value_type, const self>;

public:
    mapped_type& operator[](letter l)
    {
        return data_[l - letter_begin];
    }

    const mapped_type& operator[](letter l) const
    {
        return data_[l - letter_begin];
    }

    auto begin()
    {
        return iterator(letter_begin, *this);
    }

    auto end()
    {
        return iterator(letter_end, *this);
    }

    auto begin() const
    {
        return const_iterator(letter_begin, *this);
    }

    auto end() const
    {
        return const_iterator(letter_end, *this);
    }

private:
    container_type data_;
};

void print_string_letter_map(const letter_map<std::string>& lm)
{
    for (auto elem : lm)
    {
        std::cout << elem.first << "->" << *(elem.second) << std::endl;
    }
}

template<typename T>
class std_letter_map : public std::map<letter, T>
{
public:
    std_letter_map()
    {
        for (letter l = letter_begin; l != letter_end; ++l) {
            this->emplace(l, T());
        }
    }
};

void print_string_std_letter_map(const std_letter_map<std::string>& lm)
{
    for (const auto& elem : lm)
    {
        std::cout << elem.first << "->" << elem.second << std::endl;
    }
}

int main()
{
    letter_map<std::string> lm;
    // usually I would use auto& elem here
    for (auto elem : lm) {
        auto let = elem.first;
        // usually this would be without the *
        auto& str = *(elem.second);
        str = std::string("foo ") + let;
    }
    print_string_letter_map(lm);
    return 0;
}
Zulan
  • 21,896
  • 6
  • 49
  • 109
  • Related: http://stackoverflow.com/a/770218/85371 and http://stackoverflow.com/questions/7313160/union-iterator-for-maps/7519009#7519009 – sehe Sep 28 '15 at 16:52
  • If you're using C++11 (or later) then why use `boost::reference_wrapper` instead of `std::reference_wrapper`? I assume you need to use `get()` with it because otherwise `auto` and template argument deduction will deduce the reference_wrapper type itself, not what it refers to. – Jonathan Wakely Sep 28 '15 at 17:14
  • @JonathanWakely I simply wasn't aware of `std::reference_wrapper`, now I am :). And yes, auto defers the wrapper-type - I was hoping that operator T& kicks in on any usage, but it doesn't. – Zulan Sep 28 '15 at 17:31

2 Answers2

2

Implementing operator * is easy - just return std::pair<const Key, Value&> or ..., Value const&> for const iterator, like in this simplified example:

template <typename T>
class iterator_array_as_map
{
public:

    iterator_array_as_map(T* array, int index) 
       : array(array), index(index) 
    {}

    bool operator == (const iterator_array_as_map& other) const
    {
        return index == other.index;
    }
    bool operator != (const iterator_array_as_map& other) const
    {
        return index != other.index;
    }
    iterator_array_as_map& operator ++ ()
    {
        ++index;
        return *this;
    }

    auto operator * ()
    {
        return std::pair<const int, T&>(index, array[index]);
    }


private:
    T* array;
    int index;
};

And usage:

int main() {
    int array[2] = {2, 4};
    auto begin = iterator_array_as_map<int>(array, 0);
    auto end = iterator_array_as_map<int>(array, 2);

    for (auto it = begin; it != end; ++it)
    {
        std::cout << (*it).first << " " << (*it).second << std::endl;
     }

    (*begin).second = 7;

    for (auto it = begin; it != end; ++it)
    {
        std::cout << (*it).first << " " << (*it).second << std::endl;
    }
}

operator -> is a little harder - since you must return something which needs to emulate pointer to std::pair - but if do not mind about dynamic memory fragmentation - you can just return std::shared_ptr<std::pair<....>>...

    auto operator -> ()
    {
        return std::make_shared<std::pair<const int, T&>>(index, array[index]);
    }

If you do not want to use dynamic memory - then you might try with pointer to itself solution:

template<typename data>    
class pointer_to_data
{
public:
    template<typename ...Arg>
    pointer_to_data(Arg&&... arg)
      : data{std::forward<Arg>(arg)...}
    {}
    Data* operator -> ()
    {
        return &data;
     }
  private:
      Data data;
  };

Just return the above instead shared_ptr...


See this what-is-the-correct-way-of-using-c11s-range-based-for, section "The special case of proxy iterators". It is not possible to define for(auto&e:b) if b is e.g. std::vector<bool>- because in general it is not possible to have reference to temporary, and this very container is similar to yours in this sense that it has "Special" reference type.

You can try to have special member in your iterator keeping the "return value"- but that would be troublesome - so probably no better that this presented by me solution exist.

Community
  • 1
  • 1
PiotrNycz
  • 23,099
  • 7
  • 66
  • 112
  • Great, this actually works quite well, I wonder why I didn't try that. There would still be a difference in my use example `for (auto elem : map)` instead of `auto& elem`. Also the `pointer_to_data` is an interesting approach for the `operator->`. I forgot about that since I am mainly using range loops. – Zulan Sep 28 '15 at 17:50
  • I see another issue that would make this kind of map "surprising" to use. If one modifies elem within `for (auto elem : map)`, one usually doesn't expect the map to be modified because elem is a copy. So in a sense while this is probably necessary to fulfill my use case(?), I feel it may not be good design. – Zulan Sep 28 '15 at 17:55
  • @Zulan - you have the same problem as `std::vector` has. See my update. Since folks from C++ standard are not smart enough to solve it - I doubt you'll find something better than my non-perfect proposal... – PiotrNycz Sep 28 '15 at 20:26
  • Your update is very helpful indeed. The `for (auto&& elem : map)` idom helps towards a unified usage. Also `for (auto elem : bool_vector) { elem = ... }` does indeed modify the vector, so it is consistent in that case. – Zulan Sep 29 '15 at 11:12
0

You could probably use zip_view from Eric Niebler's range library https://github.com/ericniebler/range-v3