I have recently come across a use case where I need to lump many maps into a single container. These maps can have different combinations of key/value types, but they all differ from one another.
For a certain set of key types Key1, Key2, ...
and value types Val1, Val2,...
the obvious implementation is
class LumpedMap {
std::map<Key1, Val1> map1;
std::map<Key1, Val2> map2;
std::map<Key2, Val1> map3;
std::map<Key2, Val2> map4;
std::map<Key3, Val2> map4;
...
}
but this doesn't scale well for many maps, as if we want to maintain encapsulation we tend to create a vast number of accessors
Val1& at(const Key1&) {...}
Val2& at(const Key1&) {...}
Val1& at(const Key2&) {...}
...
void insert(const Key1&, const Val1&) {...}
void insert(const Key1&, const Val2&) {...}
void insert(const Key2&, const Val1&) {...}
...
etc...
My goal is to have a class template that allows me to instantiate a particular type with a list of key/value types
using MyMap = LumpedMap<std::pair<Key1, Val1>,
std::pair<Key1, Val2>,
std::pair<Key2, Val1>,
std::pair<Key2, Val3>>;
and that can generate automatically all standard methods for any type
MyMap fm;
//...lookup
fm.at<Key2, Val3>();
//...insertion
fm.insert_or_assign(k2, v3);
//...etc
I have come up with the following variadic template implementation that uses fold expressions (for which reason, I will call it FoldedMap
;)):
template<class K, class V>
class MapBase {
public:
using MapType = std::map<K, V>;
using MapPtr = std::shared_ptr<MapType>;
protected:
MapPtr _map{std::make_shared<MapType>()};
MapBase() = default;
MapBase(MapPtr map) : _map{std::move(map)} {
}
V& at(K& key) {
return _map->at(key);
}
auto insertOrAssign(const K& key, const V& val) {
return _map->insert_or_assign(key, val);
}
const auto& range() {
return *_map;
}
void merge(MapBase<K, V>& source) {
_map->merge(*source._map);
}
};
template<class ...Pair>
class FoldedMap : public MapBase<typename Pair::first_type, typename Pair::second_type>... {
public:
FoldedMap() = default;
FoldedMap(typename MapBase<typename Pair::first_type, typename Pair::second_type>::MapPtr& ...map)
: MapBase<typename Pair::first_type, typename Pair::second_type>(map)... {
}
template<class K, class V>
V& at(K& key) {
return MapBase<K, V>::at(key);
}
template<class K, class V>
auto insertOrAssign(const K& key, const V& val) {
return MapBase<K, V>::insertOrAssign(key, val);
}
template<class K, class V>
auto range() {
return MapBase<K, V>::range();
}
template<class ...P>
void merge(FoldedMap<P...>& other) {
(MapBase<typename P::first_type, typename P::second_type>::merge(other), ...);
}
template<class ...P>
FoldedMap<P...> extract() {
FoldedMap<P...> fm(MapBase<typename P::first_type, typename P::second_type>::_map...);
return fm;
}
template<class K, class V>
void _merge(std::map<K, V>& other) {
MapBase<K, V>::_merge(other);
}
};
A few remarks, mostly arbitrary choices I made that can be revised or fine tuned:
The method
extract()
takes a slice of the object as a new FoldedMap with a subset of key/value types.The choice of using shared pointers is motivated by the intention of supporting shallow-copy semantics in
extract()
(which maybe could be renamed to emphasize this, and is out of line with the semantics of the same method in std::map).
This is an example test code, showing some usage.
int main(int argc, const char * argv[]) {
Key1 k1 = "k1";
Key2 k2 = std::make_pair(2, "two");
Val1 val1 = 3.14;
Val2 val2 = "val2";
FoldedMap<std::pair<Key1, Val1>,
std::pair<Key1, Val2>,
std::pair<Key2, Val1>> fm;
FoldedMap<std::pair<Key2, Val1>> fm2;
// Insert elements
fm.insertOrAssign(k1, val2);
fm.insertOrAssign(k2, val1);
fm2.insertOrAssign<Key2, Val1>({3, "three"}, 6.28);
std::cout << fm.at<Key1, Val2>(k1) << "\n";
// Merge operation - follows std::map<T,T>::merge() semantics
fm.merge(fm2);
// Extract operation - leaves the original object unchanged
// by sharing data with target object
auto fm3 = fm.extract<std::pair<Key1, Val1>,std::pair<Key2, Val1>>();
for (auto& [k,v]: fm.range<Key1, Val1>())
std::cout << "Key1 - Val1 range: " << k << v << "\n";
for (auto& [k,v]: fm.range<Key1, Val2>())
std::cout << "Key1 - Val2 range: " << k << " " << v << " " << "\n";
for (auto& [k,v]: fm.range<Key2, Val1>())
std::cout << "Key2 - Val1 range: " << k.first << " " << k.second << " " << v << "\n";
for (auto& [k,v]: fm3.range<Key1, Val1>())
std::cout << "from view " << k << v << "\n";
for (auto& [k,v]: fm3.range<Key2, Val1>())
std::cout << "from view " << k.first << " " << k.second << " " << v << "\n";
std::cout <<
std::any_of(fm.range<Key2, Val1>().begin(),
fm.range<Key2, Val1>().end(),
[] (const auto& k2) { return k2.first.first == 3;}
)
<< "\n";
return 0;
}
Any feedback on the above implementation is welcome. Has someone seen something similar in any library?
fm2 = fm1;
to copy the map followed by a change tofm2
will result in unexpected changes tofm1
as well.) \$\endgroup\$