«Свернутый» шаблон класса карты с несколькими типами ключей / значений.

Недавно я столкнулся с вариантом использования, когда мне нужно объединить множество карт в один контейнер. Эти карты могут иметь разные комбинации типов ключей и значений, но все они отличаются друг от друга.

Для определенного набора типов ключей 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 ответ
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() имеет смысл, учтите, что кто-то может захотеть извлечь узел из одного из MapBases, и позже 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(); перегрузка функций должна работать нормально.

Проходить Pairs где возможно

Вместо того, чтобы делать 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::maps по стоимости. С семантикой значений всегда гораздо легче работать. Например, если бы я написал:

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 тип? Оба эти решения сделают семантику неглубокого копирования явной.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *