1

For some time I’ve used Boost’s flat_map as my go-to associative collection, for the reasons explained cited on their documentation intro, and (originally) the fact that it gave newer features before the compiler’s std implementation, and it was the same across platforms.

Now, I wanted to start using string_view to prevent copying strings, when these are taken from substrings of a larger input. string_view points to a range of characters within the larger string, without having to copy them into a new std::string instance.

In reaching for a map to use, I recalled that another progressive feature of Boost.Container that I’ve enjoyed in the past is conformal keys, where you could use anything that compared correctly against the stored key, rather than converting to the actual type of key.

But now I can’t find any mention of that in the documentation. I know the std::map can do that now (since C++14) but I’d rather use the flat_map for tiny collections.

What could I have seen that allowed this flexibility, years ago, if it’s not apparent in boost::flat_map::insert etc.? What are good flat collections to use now with up-to-date compilers?

JDługosz
  • 5,592
  • 3
  • 24
  • 45
  • 1
    Heterogenous lookup and "compatible key" are indeed features that exist in parts of boost (Eg. Boost multi index). Perhaps these keywords help you find what you seem to remember. Otherwise Andrey's answer is excellent – sehe May 10 '18 at 15:19
  • Multi-index: that might be where I saw it, since that's something I've also used. Thanks. – JDługosz May 10 '18 at 22:38

2 Answers2

4

Support for polymorphic lookup functions has been added only recently to Boost.Container. If everything is good, it should be released with Boost 1.68.

In the meantime you can emulate flat associative containers with an ordered std::vector and std::lower_bound.

typedef std::pair< std::string, int > element_type;
std::vector< element_type > map;

struct element_order
{
    bool operator()(element_type const& left, element_type const& right) const
    {
        return left.first < right.first;
    }

    bool operator()(std::string_view const& left, element_type const& right) const
    {
        return left < right.first;
    }

    bool operator()(element_type const& left, std::string_view const& right) const
    {
        return left.first < right;
    }
};

auto find_element(std::string_view const& key)
{
    auto it = std::lower_bound(map.begin(), map.end(), key, element_order());
    if (it != map.end() && it->first == key)
        return it;
    return map.end();
}
Andrey Semashev
  • 10,046
  • 1
  • 17
  • 27
  • Why do you need `element_order` instead of just using `less<>`? – JDługosz May 10 '18 at 22:39
  • Because `std::less` is not a polymorphic function object. It requires its arguments to be the same as its template argument. – Andrey Semashev May 11 '18 at 19:00
  • Actually, it appears that since C++14 `std::less` (and equivalently, `std::less<>`) is polymorphic. But you still can't use it because you need to access a member of `element_type` and `std::less` would attempt to compare the whole `element_type` against the key. – Andrey Semashev May 11 '18 at 19:10
0

Perhaps this is not what you are referring to, but if you use std::string_view as the key type, all operations already work via the implicit conversion to std::string_view:

Live On Coliru

#include <boost/container/flat_map.hpp>
#include <string_view>

int main() {
    boost::container::flat_map<std::string_view, int> m {
        { "one", 1 },
        { "two", 2 },
        { "three", 3 },
        { "four", 4 },
    };

    std::string key = "one";
    auto one = m.at(key);
    auto range = m.equal_range(key);
    auto it = m.find(key);

    m[key] = 1;
}

The Inverse

Here you'd actually need to use a container that supports compatible-key lookup indeed. It doesn't need to be overly complicated to roll one:

Here's one:

Live On Coliru

#include <initializer_list>
#include <algorithm>
#include <utility>
#include <stdexcept>

#include <boost/container/small_vector.hpp>

template <typename K, typename V, typename Cmp = std::less<K>, typename Storage = boost::container::small_vector<std::pair<K, V>, 10> >
struct flat_map {
    using key_type       = K;
    using mapped_type    = V;
    using key_compare    = Cmp;

    using storage        = Storage;
    using value_type     = typename storage::value_type;
    using iterator       = typename Storage::iterator;
    using const_iterator = typename Storage::const_iterator;

    struct value_compare {
        key_compare _cmp;
        template <typename A, typename B>
        bool operator()(A const& a, B const& b) const { return _cmp(access(a), access(b)); }

      private:
        static auto& access(value_type const& v) { return v.first; }
        template <typename Other>
        static auto& access(Other const& v)      { return v; }
    } _cmp;

    storage _data;

    flat_map(std::initializer_list<value_type> i) : _data(i) {}

    iterator begin()             { return _data.begin(); }
    iterator end()               { return _data.end();   }
    const_iterator begin() const { return _data.begin(); }
    const_iterator end()   const { return _data.end();   }

    template <typename Key>
    mapped_type& operator[](Key&& key) { return find(std::forward<Key>(key))->second; }
    template <typename Key>
    mapped_type const& operator[](Key&& key) const { return find(std::forward<Key>(key))->second; }

    template <typename Key>
    iterator find(Key&& key) {
        auto r = equal_range(std::forward<Key>(key));
        return (r.first == r.second)? end() : r.first;
    }
    template <typename Key>
    const_iterator find(Key&& key) const {
        auto r = equal_range(std::forward<Key>(key));
        return (r.first == r.second)? end() : r.first;
    }

    template <typename Key>
    mapped_type& at(Key&& key) {
        auto r = equal_range(std::forward<Key>(key));
        if (r.first == r.second) throw std::out_of_range("key");
        return r.first->second;
    }
    template <typename Key>
    mapped_type const& at(Key&& key) const {
        auto r = equal_range(std::forward<Key>(key));
        if (r.first == r.second) throw std::out_of_range("key");
        return r.first->second;
    }

    template <typename Key>
    auto equal_range(Key&& key) { return std::equal_range(begin(), end(), std::forward<Key>(key), _cmp); }
    template <typename Key>
    auto equal_range(Key&& key) const { return std::equal_range(begin(), end(), std::forward<Key>(key), _cmp); }
};

It supports precisely the inverse of the first scenario (given the comparator of std::less<>):

#include <string_view>
#include <string>

int main() {
    flat_map<std::string, int, std::less<> > m {
        { "one", 1 },
        { "two", 2 },
        { "three", 3 },
        { "four", 4 },
    };

    std::string_view key = "one";
    auto one = m.at(key);
    auto range = m.equal_range(key);
    auto it = m.find(key);

    m[key] = 1;
}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • I think using a `string_view` as the key type is a singularly bad idea, since it does not own the storage. (OK for limited exposure, though, if you control how things are added; maybe a cross-index for another collection) – JDługosz May 10 '18 at 22:42
  • Something is not a bad idea just because there are situations in which it doesn't apply. It simply doesn't apply to all situations. (By the way, I just fixed a bug that I realized after going to bed. Cheers :)) – sehe May 10 '18 at 22:47
  • I don’t understand how passing a different collection as the template argument to `flat_map` would help. – JDługosz May 10 '18 at 22:48
  • Nobody said that. I think you might not have read the answer fully yet, so I encourage you to do that, and I'll see your comments in the morning. Good night! – sehe May 10 '18 at 22:50
  • You said: Storage container argument to `flat_map` is a better idea than writing a dictionary based on a sorted vector that has a templated `find` member. What did I not read fully? – JDługosz May 10 '18 at 23:04
  • Oh, just confusion. You had responded to two isolated parts of my answer (stringview-as-key and the footnote) - without acknowledging the largest part ("The Inverse", which has the second live demo). Never mind, it seems my use of the footnote was confusing (it relates _only_ to my choice of `storage` type, which is where the footnote is anchored¹). I amended the answer to remove that (and showing what I meant with the footnote instead). – sehe May 11 '18 at 19:51