简体   繁体   中英

Emulate std::map iterators for custom map that does not use tuples

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::pair s 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;
}

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 eg 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.

您可能可以使用 Eric Niebler 的范围库中的 zip_view https://github.com/ericniebler/range-v3

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM