Boost::Multi-index для вложенных списков

Как реализовать Boost::Multi-index в списке списков

У меня есть иерархическое дерево следующим образом:

typedef std::list<struct obj> objList // the object list
typedef std::list<objList> topLevelList // the list of top-level object lists

struct obj
{
   int Id; // globally unique Id
   std::string objType;
   std::string objAttributes;
   ....
   topLevelList  childObjectlist;
}

На верхнем уровне у меня есть std::list структуры obj. Затем каждый из этих объектов верхнего уровня может иметь любое количество дочерних объектов, которые содержатся в списке topLevelList для этого объекта. Это может продолжаться, когда дочерний элемент во вложенном списке также имеет своих собственных дочерних элементов.

Некоторые объекты могут быть только дочерними, в то время как другие являются контейнерами и могут иметь своих собственных дочерних элементов. Объекты-контейнеры имеют X подконтейнеров, каждый подконтейнер имеет свой собственный список дочерних объектов, и поэтому у меня есть topLevelList в каждой структуре объекта, а не просто objList.

Я хочу проиндексировать этот список списков с помощью boost::Multi-index, чтобы получить произвольный доступ к любому из объектов либо в списке верхнего уровня, либо в списке потомков по его глобально уникальному идентификатору.

Можно ли это осуществить? Я искал примеры без успеха.

Я думаю, что единственный способ получить сглаженный главный поисковый индекс по идентификаторам объектов — это сделать приведенные выше списки списками указателей на объекты, затем пройти по завершенному иерархическому списку и войти в главный поисковый индекс указатель, где находится каждый объект. физически размещается в памяти. Тогда любой объект можно найти с помощью главного поискового индекса.

С Boost::Multi-index мне все равно придется проходить иерархию, хотя, надеюсь, с возможностью использовать случайный, а не последовательный доступ в каждом встречающемся списке, чтобы найти нужный объект.

Использование вложенных векторов вместо списков является проблемой - поскольку в векторах происходят добавления и удаления, это снижает производительность, а также вероятность того, что указатели на объекты станут недействительными при перераспределении векторов.

Я почти уговариваю себя реализовать уплощенный индекс поиска указателей master objId, если только у кого-то нет лучшего решения, которое может использовать Boost::Multi-index.

Редактирование от 31 января 2020 г. У меня возникли проблемы с реализацией вложенных списков ниже. У меня бывают случаи, когда код неправильно помещает родительские объекты верхнего уровня в верхний уровень, и, таким образом, в распечатке «в квадратных скобках» мы не видим иерархию для этого родителя. Однако в распечатке «Дети xxx» дети этого родителя отображаются правильно. Вот раздел main.cpp, демонстрирующий проблему:

auto it=c.insert({170}).first;
it=c.insert({171}).first;
it=c.insert({172}).first;
it=c.insert({173}).first;
auto it141=c.insert({141}).first;
    auto it137=insert_under(c,it141,{137}).first;
        insert_under(c,it137,{8});
        insert_under(c,it137,{138});
        auto it9=insert_under(c,it137,{9}).first;
            auto it5=insert_under(c,it9,{5}).first;
                insert_under(c,it5,{6});
                insert_under(c,it5,{7});
        insert_under(c,it137,{142});
        auto it143=insert_under(c,it137,{143}).first;
        insert_under(c,it143,{144});

Если вы поместите этот код в Main.cpp вместо демо-кода и запустите его, вы увидите проблему. Объект 141 является родительским объектом и размещается на верхнем уровне. Но он не печатается в распечатке иерархии «В квадратных скобках». Почему это?

Изменить от 02.02.2020:

Boost::Serialize часто выдает исключение для oarchive, жалуясь на то, что повторное создание определенного объекта приведет к дублированию объектов. Некоторые архивы успешно сохраняются и перезагружаются, но многие приводят к указанной выше ошибке. Мне пока не удалось определить точные условия, при которых возникает ошибка, но я доказал, что ни один контент, используемый для заполнения вложенного_контейнера и плоского списка объектов, не содержит повторяющихся идентификаторов объектов. Я использую текстовый архив, а не бинарный. Вот как я изменил код для nested_container, а также для другого отдельного списка плоских объектов, чтобы выполнить Boost::Serialize:

struct obj
{
    int             id;
    const obj * parent = nullptr;

    obj()
        :id(-1)
    { }

    obj(int object)
        :id(object)
    { }

    int getObjId() const
    {
        return id;
    }

    bool operator==(obj obj2)
    {
        if (this->getObjId() == obj2.getObjId())
            return true;
        else
            return false;
    }
#if 1
private:
    friend class boost::serialization::access;
    friend std::ostream & operator<<(std::ostream &os, const obj &obj);

    template<class Archive>
    void serialize(Archive &ar, const unsigned int file_version)
    {
        ar & id & parent;
    }
#endif
};

struct subtree_obj
{
    const obj & obj_;

    subtree_obj(const obj & ob)
        :obj_(ob)
    { }
#if 1
private:
    friend class boost::serialization::access;
    friend std::ostream & operator<<(std::ostream &os, const subtree_obj &obj);

    template<class Archive>
    void serialize(Archive &ar, const unsigned int file_version)
    {
        ar & obj_;
    }
#endif
};

struct path
{
    int         id;
    const path *next = nullptr;

    path(int ID, const path *nex)
        :id(ID), next(nex)
    { }

    path(int ID)
        :id(ID)
    { }
#if 1
private:
    friend class boost::serialization::access;
    friend std::ostream & operator<<(std::ostream &os, const path &pathe);

    template<class Archive>
    void serialize(Archive &ar, const unsigned int file_version)
    {
        ar & id & next;
    }
#endif
};

struct subtree_path
{
    const path & path_;

    subtree_path(const path & path)
        :path_(path)
    { }
#if 1
private:
    friend class boost::serialization::access;
    friend std::ostream & operator<<(std::ostream &os, const subtree_path &pathe);

    template<class Archive>
    void serialize(Archive &ar, const unsigned int file_version)
    {
        ar & path_;
    }
#endif
};

//
// My flattened object list
//

struct HMIObj
{
    int         objId;
    std::string objType;

    HMIObj()
        :objId(-1), objType("")
    { }

    bool operator==(HMIObj obj2)
    {
        if (this->getObjId() == obj2.getObjId())
            && this->getObjType() == obj2.getObjType())
            return true;
        else
            return false;
    }

    int getObjId() const
    {
        return objId;
    }

    std::string getObjType() const
    {
        return objType;
    }
#if 1
private:
    friend class boost::serialization::access;
    friend std::ostream & operator<<(std::ostream &os, const HMIObj &obj);

    template<class Archive>
    void serialize(Archive &ar, const unsigned int file_version)
    {
        ar & objId & objType;
    }
#endif
};

person NukerDoggie    schedule 04.11.2019    source источник


Ответы (1)


Если это поможет, вы можете использовать Boost.MultiIndex для реализации своего рода иерархического контейнера, используя понятие упорядочивания путей.

Предположим, у нас есть следующая иерархия объектов, идентифицируемых их идентификаторами:

|-------
|      |
0      4
|----  |----
| | |  | | |
1 2 3  5 8 9
       |--
       | |
       6 7

Мы определяем путь каждого объекта как последовательность идентификаторов от корня до объекта:

0 --> 0
1 --> 0, 1
2 --> 0, 2
3 --> 0, 3
4 --> 4
5 --> 4, 5
6 --> 4, 5, 6
7 --> 4, 5, 7
8 --> 4, 8
9 --> 4, 9

Эти пути могут быть упорядочены лексикографически, так что последовательность объектов, отсортированных по пути, на самом деле является представлением базовой иерархии. Если мы добавим указатель parent к объектам для моделирования отношений родитель-потомок:

struct obj
{
   int        id;
   const obj* parent=nullptr;
};

тогда мы можем определить multi_index_container как с доступом O(1) по идентификатору, так и с индексацией на основе иерархии:

using nested_container=multi_index_container<
  obj,
  indexed_by<
    hashed_unique<member<obj,int,&obj::id>>,
    ordered_unique<identity<obj>,obj_less>
  >
>;

где obj_less сравнивает объекты в соответствии с порядком пути. Возможны все типы манипуляций с деревьями и посещений, как показано ниже (код не совсем тривиален, не стесняйтесь спрашивать).

Жить на Coliru

#include <boost/multi_index_container.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/identity.hpp>
#include <boost/multi_index/member.hpp>
#include <iterator>

struct obj
{
   int        id;
   const obj* parent=nullptr;
};

struct subtree_obj
{
  const obj& obj_;
};

struct path
{
  int         id;
  const path* next=nullptr;
};

struct subtree_path
{
  const path& path_;
};

inline bool operator<(const path& x,const path& y)
{
       if(x.id<y.id)return true;
  else if(y.id<x.id)return false;
  else if(!x.next)  return y.next;
  else if(!y.next)  return false;
  else              return *(x.next)<*(y.next);
}

inline bool operator<(const subtree_path& sx,const path& y)
{
  const path& x=sx.path_;

       if(x.id<y.id)return true;
  else if(y.id<x.id)return false;
  else if(!x.next)  return false;
  else if(!y.next)  return false;
  else              return subtree_path{*(x.next)}<*(y.next);
}

inline bool operator<(const path& x,const subtree_path& sy)
{
  return x<sy.path_;
}

struct obj_less
{
private:
  template<typename F>
  static auto apply_to_path(const obj& x,F f)
  {
    return apply_to_path(x.parent,path{x.id},f); 
  }

  template<typename F>
  static auto apply_to_path(const obj* px,const path& x,F f)
    ->decltype(f(x))
  { 
    return !px?f(x):apply_to_path(px->parent,{px->id,&x},f);
  }

public:
  bool operator()(const obj& x,const obj& y)const
  {
    return apply_to_path(x,[&](const path& x){
      return apply_to_path(y,[&](const path& y){
        return x<y;
      });
    });
  }

  bool operator()(const subtree_obj& x,const obj& y)const
  {
    return apply_to_path(x.obj_,[&](const path& x){
      return apply_to_path(y,[&](const path& y){
        return subtree_path{x}<y;
      });
    });
  }

  bool operator()(const obj& x,const subtree_obj& y)const
  {
    return apply_to_path(x,[&](const path& x){
      return apply_to_path(y.obj_,[&](const path& y){
        return x<subtree_path{y};
      });
    });
  }
};

using namespace boost::multi_index;
using nested_container=multi_index_container<
  obj,
  indexed_by<
    hashed_unique<member<obj,int,&obj::id>>,
    ordered_unique<identity<obj>,obj_less>
  >
>;

template<typename Iterator>
inline auto insert_under(nested_container& c,Iterator it,obj x)
{
  x.parent=&*it;
  return c.insert(std::move(x));
}

template<typename Iterator,typename F>
void for_each_in_level(
  nested_container& c,Iterator first,Iterator last, F f)
{
  if(first==last)return;

  const obj* parent=first->parent;
  auto       first_=c.project<1>(first),
             last_=c.project<1>(last);

  do{
    f(*first_);
    auto next=std::next(first_);
    if(next->parent!=parent){
      next=c.get<1>().upper_bound(subtree_obj{*first_});
    }
    first_=next;
  }while(first_!=last_);
}

template<typename ObjPointer,typename F>
void for_each_child(nested_container& c,ObjPointer p,F f)
{
  auto [first,last]=c.get<1>().equal_range(subtree_obj{*p});
  for_each_in_level(c,std::next(first),last,f);
}

#include <iostream>

auto print=[](const obj& x){std::cout<<x.id<<" ";};

void print_subtree(nested_container& c,const obj& x)
{
  std::cout<<x.id<<" ";
  bool visited=false;
  for_each_child(c,&x,[&](const obj& x){
    if(!visited){
      std::cout<<"[ ";
      visited=true;
    }
    print_subtree(c,x);
  });
  if(visited)std::cout<<"] ";
}

int main()
{
  nested_container c;
  auto it=c.insert({0}).first;
    insert_under(c,it,{1});
    insert_under(c,it,{2});
    insert_under(c,it,{3});
  it=c.insert({4}).first;
    auto it2=insert_under(c,it,{5}).first;
      insert_under(c,it2,{6});
      insert_under(c,it2,{7});
    insert_under(c,it,{8});
    insert_under(c,it,{9});

  std::cout<<"preorder:\t";
  std::for_each(c.get<1>().begin(),c.get<1>().end(),print);  
  std::cout<<"\n"; 

  std::cout<<"top level:\t";
  for_each_in_level(c,c.get<1>().begin(),c.get<1>().end(),print);
  std::cout<<"\n"; 

  std::cout<<"children of 0:\t";
  for_each_child(c,c.find(0),print);
  std::cout<<"\n";

  std::cout<<"children of 4:\t";
  for_each_child(c,c.find(4),print);
  std::cout<<"\n";

  std::cout<<"children of 5:\t";
  for_each_child(c,c.find(5),print);
  std::cout<<"\n"; 

  std::cout<<"bracketed:\t";
  for_each_in_level(c,c.get<1>().begin(),c.get<1>().end(),[&](const obj& x){
    print_subtree(c,x);
  });
  std::cout<<"\n"; 
}

Вывод

preorder:       0 1 2 3 4 5 6 7 8 9 
top level:      0 4 
children of 0:  1 2 3 
children of 4:  5 8 9 
children of 5:  6 7 
bracketed:      0 [ 1 2 3 ] 4 [ 5 [ 6 7 ] 8 9 ] 

Обновление от 02.02.2020:

При доступе к элементам верхнего уровня я изменил код:

std::for_each(c.begin(),c.end(),...;  
for_each_in_level(c,c.begin(),c.end(),...);

to

std::for_each(c.get<1>().begin(),c.get<1>().end(),...;  
for_each_in_level(c,c.get<1>().begin(),c.get<1>().end(),...);

Это связано с тем, что индекс #0 хэшируется и не обязательно показывает элементы, отсортированные по идентификатору.

Например, если элементы с идентификаторами (170 171 173 173 141) вставлены в этом порядке, индекс #0 перечисляет их как

170 171 173 173 141 (по совпадению, в том же порядке, что и вставленный),

в то время как индекс № 1 перечисляет их как

141 170 171 173 173 (отсортировано по идентификатору).

В соответствии с тем, как реализован код, for_each_in_level(c,c.begin(),c.end(),...); внутренне сопоставляется с диапазоном index #1 [170,...,173], исключая 141. Затем можно убедиться, что включены все верхние элементы: напиши for_each_in_level(c,c.get<1>().begin(),c.get<1>().end(),...);.

person Joaquín M López Muñoz    schedule 08.11.2019
comment
Это замечательно! Больше, чем я надеялся. Мне нужно пару дней изучить ваш ответ, а потом хочу получить более подробную информацию о реализации. Но это потенциально открывает некоторые мощные функции в моем приложении, которые мне пришлось бы пытаться разработать и реализовать вручную. Огромное спасибо!!! - person NukerDoggie; 08.11.2019
comment
Ваше решение открывает дверь для еще одной важной возможности — осмысленного шага к обобщению шаблонов, присущих иерархической структуре. Ибо если я буду следовать вашей методике, используя Object Type в дополнение к Object ID, то я добьюсь сопоставления паттернов в иерархии — типов объектов, которые служат родителями, и типов объектов, которые служат их дочерними элементами, и где такие паттерны существуют в иерархии. Это основная цель моего приложения, и вы указали мне четкий путь к ее достижению — сопоставление и распознавание образов. Спасибо еще раз!!! - person NukerDoggie; 11.11.2019
comment
Не уверен, что слежу за вашей дискуссией по отображению шаблонов, но я в любом случае рад, что ответ был полезен ???? Не стесняйтесь делиться результатами, если вам нужна дополнительная обратная связь. - person Joaquín M López Muñoz; 11.11.2019
comment
Я пытаюсь создать ваш пример кода, чтобы работать с ним, чтобы ознакомиться с реализацией. Я использую VS2015. Я получаю сообщение об ошибке C2059 - синтаксическая ошибка - пустое объявление в следующей строке - auto[first, last] = c.get‹1›().equal_range(subtree_obj{ *p}); Я не смог понять, почему компилятору не нравится эта строка кода. Позже, когда вы вызываете функцию for_each_child(), вы предоставляете правильные параметры вместе с лямбдой для выполнения. Компилятор жалуется, что инициализация - subtree_obj{ *p} неприемлема? - person NukerDoggie; 14.11.2019
comment
VS2015 не поддерживает структурированные привязки C++17 и, по-видимому, имеет некоторые проблемы с инициализаторами элементов по умолчанию и инициализацией списка копий. Я перенес код здесь. - person Joaquín M López Muñoz; 15.11.2019
comment
Спасибо за быстрый ответ - у меня работает код. Я пошел дальше и установил VS2017, и у меня также есть исходный код, который вы опубликовали, работающий. Я буду публиковать вопросы здесь, как только у меня будет возможность изучить код и настроить его под свои нужды. Спасибо еще раз!! - person NukerDoggie; 17.11.2019
comment
Мне очень нравится эта мультииндексная реализация для иерархических контейнеров. Но у меня есть несколько разных функций, членов отдельных классов, которые должны выполнять вставку() и вставку_под() в контейнер, и тип используемых итераторов довольно многословен - отсюда и использование auto. Но что вы предлагаете для передачи этих итераторов функции, которая должна знать итератор для родителя, чтобы вставить_под() этого родителя, только для одного примера? Получить typedef для it здесь немного сложно. Обычная лямбда как средство передвижения? - person NukerDoggie; 20.11.2019
comment
insert_under – это шаблон функции, принимающий все, что указывает на obj. Если вам абсолютно необходимо иметь конкретный тип, вам нужно решить, использовать ли итераторы для индекса № 0 (хеширование по идентификатору) или индекса № 1 (упорядоченный по пути). Вероятно, индекс #0 имеет больше смысла, и в этом случае это просто nested_container::iterator. - person Joaquín M López Muñoz; 20.11.2019
comment
У меня возникли проблемы с вложенным контейнером — см. мое редактирование от 31 января 2020 г. выше. - person NukerDoggie; 02.02.2020
comment
Была проблема с исходным кодом, только что обновил свой ответ. - person Joaquín M López Muñoz; 02.02.2020
comment
Большое спасибо за ваш быстрый ответ!!! У меня есть целый XML-парсер с рекурсивным спуском, использующий ваш код Boost::MultiIndex для сохранения анализируемой иерархии, и он прекрасно работает для моих нужд! Эта единственная проблема, которую вы сейчас решили, полностью разблокирует меня на пути к завершению приложения для отображения шаблонов, которое я пишу. Я хотел бы дать вам 1000 голосов! - person NukerDoggie; 02.02.2020
comment
Ваше обновление кода заставляет все работать идеально! У меня есть другая, не связанная с этим проблема: теперь я использую Boost::Serialize для сохранения архива вложенного_контейнера, а также списка сглаженных объектов, который отделен от вложенного_контейнера. Часто, но не всегда, Boost::Serialize выдает ошибки в oarchive с жалобой на то, что повторное создание объекта приведет к дублированию объектов. Выше я публикую свой код для функции сериализации. Существуют ли какие-либо особые соображения для сериализации вложенного_контейнера? - person NukerDoggie; 03.02.2020
comment
Хотите отправить отдельный ответ? Хотя я не могу гарантировать, что смогу посмотреть его завтра. - person Joaquín M López Muñoz; 03.02.2020
comment
Не уверен, что понимаю - вы хотите, чтобы я опубликовал совершенно новый вопрос о проблеме Boost::Serialize? - person NukerDoggie; 03.02.2020
comment
Выполнено: stackoverflow.com/questions /60041846/ - person NukerDoggie; 03.02.2020
comment
Это мультииндексное решение для иерархических объектов отлично работает для моего приложения. У меня есть один связанный вопрос об отдельном многоиндексном контейнере, который я хочу использовать, который будет связан с иерархическим контейнером - для многоиндексных контейнеров с составными ключами должны ли члены, составляющие составной ключ, быть смежными членами в структуре данных , или составной ключ может состоять из несмежных членов? Все примеры, которые я могу найти, показывают составной ключ, построенный из соседних элементов структуры данных. - person NukerDoggie; 30.09.2020
comment
Такого ограничения нет: компоненты составного ключа могут ссылаться на несмежные элементы данных в любом нужном вам порядке. На самом деле компоненты не обязательно должны быть элементами данных: например, вы можете указать составной ключ из const_mem_fun экстракторов. - person Joaquín M López Muñoz; 30.09.2020
comment
Большое спасибо! Какой невероятно ценный вклад вы внесли в проектирование и разработку программного обеспечения на C++ с помощью функции Boost::multi-index! - person NukerDoggie; 01.10.2020