Недавно я столкнулся с вариантом использования, когда мне нужно объединить множество карт в один контейнер. Эти карты могут иметь разные комбинации типов ключей и значений, но все они отличаются друг от друга.
Для определенного набора типов ключей Key1, Key2, ...
и типы значений Val1, Val2,...
очевидная реализация
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;
...
}
но это плохо масштабируется для многих карт, так как если мы хотим сохранить инкапсуляцию, мы склонны создавать огромное количество аксессуаров.
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&) {...}
...
так далее…
Моя цель — иметь шаблон класса, который позволяет мне создать экземпляр определенного типа со списком типов ключей / значений.
using MyMap = LumpedMap<std::pair<Key1, Val1>,
std::pair<Key1, Val2>,
std::pair<Key2, Val1>,
std::pair<Key2, Val3>>;
и который может автоматически генерировать все стандартные методы для любого типа
MyMap fm;
//...lookup
fm.at<Key2, Val3>();
//...insertion
fm.insert_or_assign(k2, v3);
//...etc
Я придумал следующую реализацию вариативного шаблона, в которой используются выражения свертки (по этой причине я буду называть ее 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);
}
};
Несколько замечаний, в основном произвольный выбор, который я сделал, который можно изменить или доработать:
Метод
extract()
принимает фрагмент объекта как новую FoldedMap с подмножеством типов ключ / значение.Выбор использования общих указателей мотивирован намерением поддерживать семантику мелкого копирования в
extract()
(который, возможно, можно было бы переименовать, чтобы подчеркнуть это, и он не соответствует семантике того же метода в std :: map).
Это пример тестового кода, демонстрирующий его использование.
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;
}
Любые отзывы о вышеупомянутой реализации приветствуются. Кто-нибудь видел что-то подобное в какой-нибудь библиотеке?
1 ответ
Это уже выглядит неплохо, и это интересная проблема, которую нужно решить. Однако сначала я попытаюсь привести некоторые аргументы, почему вам, возможно, не следует этого делать, а затем упомяну некоторые вещи, которые можно улучшить.
Инкапсуляция
но это плохо масштабируется для многих карт, так как если мы хотим сохранить инкапсуляцию, мы склонны создавать огромное количество аксессуаров.
Я предполагаю, что это зависит от того, что вы называете «инкапсуляцией», но я думаю, что наличие нескольких переменных-членов, сгруппированных внутри class
также отлично подходит инкапсуляция. Вам не нужно большое количество аксессуаров, так как вместо написания:
fm.at<Key1, Val1>();
Ты можешь написать:
fm.map1.at();
Вы должны предоставить информацию о том, к какой карте вы хотите получить доступ, так что либо через имя члена, либо через параметр шаблона. Единственный раз, когда я считаю, что последнее более желательно, это если у вас есть другой шаблонный код, который знает только о паре тип ему нужен доступ, а не карта название.
Рассмотрите возможность использования std::tuple
Ваш FoldedMap
в основном выглядит как std::tuple
над std::maps
. Вместо:
FoldedMap<std::pair<Key1, Val1>, std::pair<Key2, Val2>> fm;
Вы могли написать:
std::tuple<std::map<Key1, Val1>, std::map<Key2, Val2>> fm;
Единственный недостаток этого заключается в том, что у него нет удобной функции-члена. insertOrAssign()
который может определить желаемый тип элемента из его аргументов. Однако для этого можно написать внеклассную функцию:
template<typename FoldedMap, typename Key, typename Val>
insertOrAssign(FoldedMap &fm, const Key &key, const Val &val) {
fm.get<std::map<Key, Val>>().insert_or_assign(key, val);
}
То же самое и с другими функциями-членами.
Избегайте начинать идентификаторы с подчеркивания
В стандарте есть правила относительно идентификаторов, начинающихся с подчеркивания. Хотя ваше использование безопасно, я бы просто не стал полностью начинать имена с подчеркивания, так как слишком легко ошибиться, так как не все знают эти правила наизусть. Я предлагаю вам либо использовать m_
в качестве префикса для переменных-членов или используйте одинарное подчеркивание в качестве суффикса.
Используйте соглашения об именах стандартной библиотеки
Вы пытаетесь сделать FoldedMap
Контейнер выглядит так же, как контейнеры STL. Однако пока at()
а также merge()
пишутся так же, как соответствующие функции-члены std::map
, ты использовал insertOrAssign()
вместо insert_or_assign()
. Я рекомендую вам использовать точно такие же соглашения об именах, что и STL, чтобы избежать сюрпризов и упростить его использование в качестве замены для типов STL.
Также обратите внимание, что extract()
также существует для std::map
, но делает что-то отличное от вашего extract()
. Хотя я думаю, что твоя extract()
имеет смысл, учтите, что кто-то может захотеть извлечь узел из одного из MapBase
s, и позже insert()
это снова. Вы можете перегрузить FoldedMap::extract()
чтобы справиться с обоими этими вещами, но было бы лучше, если бы у этих вещей были четко определенные имена. Возможно, вы могли бы переименовать свой текущий extract()
к get()
, поскольку он делает что-то похожее на std::get()
.
Точное соответствие сигнатуры функций стандартной библиотеки
Если вы посмотрите документацию std::map<>::insert_or_assign()
, вы заметите, что он принимает значение не как const V&
, но как M&&
. Это позволяет избежать ненужного приведения типов (например, при передаче строкового литерала C, когда V
является std::string
), а также позволяет оперативно перемещаться из временных. Так что ваши MapBase::insert_or_assign()
должно выглядеть так:
template<typename M>
auto insert_or_assign(const K& key, M&& val) {
return m_map->insert_or_assign(key, std::forward<M>(val));
}
Для FoldedMap::insert_or_assign()
вы, конечно, не можете этого сделать, так как это зависит от значения, имеющего тип V
чтобы правильно вывести, какая основная MapBase
использовать. Тем не менее, вы все равно должны принять val
есть ссылка на пересылку.
Ваш at()
принимает key
как нет-const
ссылка, это должно быть const
. Также, std::map<>::at()
имеет const
перегрузка функции, которую вам, вероятно, также следует реализовать.
О _merge()
Ваш FoldedMap
имеет функцию-член _merge()
, что не работает (нет соответствующего _merge()
в MapBase
), но его также нужно просто переименовать в merge()
; перегрузка функций должна работать нормально.
Проходить Pair
s где возможно
Вместо того, чтобы делать MapBase
быть шаблоном двух типов, сделайте его шаблоном всего Pair
:
template<typename Pair>
class MapBase {
using K = typename Pair::first_type;
using V = typename Pair::second_type;
...
void merge(MapBase& source) {...}
};
Это упрощает большую часть FoldedMap
, хотя некоторые функции-члены должны сохранять template<class K, class V>
для работы вычета типа, и в этом случае вам нужно использовать MapBase<std::pair<K, v>>
для доступа к основной карте:
template<class ...Pair>
class FoldedMap : public MapBase<Pair>... {
...
FoldedMap(typename MapBase<Pair>::MapPtr& ...map): MapBase<Pair>(map)... {}
template<class K, class V>
V& at(const K& key) {
return MapBase<std::pair<K, V>>::at(key);
}
...
template<class ...P>
void merge(FoldedMap<P...>& other) {
(MapBase<P>::merge(other), ...);
}
...
};
Использование std::shared_ptr
Я бы избегал использования std::shared_ptr
, и вместо этого сохраните базовый std::map
s по стоимости. С семантикой значений всегда гораздо легче работать. Например, если бы я написал:
FoldedMap<std::pair<std::string, std::string>> fm1;
auto fm2 = fm1; // copy?
fm1.insert_or_assign(std::string{"Hello,"}, std::string{"world!"});
std::cout << fm2.at<std::string, std::string>("Hello,") << "n";
Если fm1
были постоянным std::map<std::string, std::string>
, то я ожидаю, что последняя строка вызовет исключение, потому что копия была сделана, когда fm1
все еще было пусто. Однако ваш класс в основном имеет ссылочную семантику и позволяет этому коду печатать «world!
«, что было бы неожиданно для большинства программистов на C ++.
Если кому-то действительно нужна семантика неглубокого копирования, он может просто создать std::shared_ptr<FoldedMap<...>>
сами себя. Если вы хотите сделать неглубокую копию, ограничивающую количество базовых MapBase
доступны, возможно, вы могли бы создать какой-нибудь FoldedMapView
тип? Оба эти решения сделают семантику неглубокого копирования явной.