Мотивация

Предположим, что ты - С++ программист и тебе нужно создать справочник. Ну а точнее, рассмотрим один из этапов, когда тебе нужно отобразить одно множество на другое. В процессе поиска решения ты узнаешь про хэш-таблицы, деревья, и делаешь самую наивную реализацию. После чего, при усложнении стандартного примера, начинаешь задаваться вопросами:

  1. Как сделать ключом поле класса?

  2. Что, если правил для индексации(способов отображения одного мн-ва на другое) станет больше 1?

Для начала рассмотрим класс с информацией о человеке, на котором будем рассматривать следующие примеры:

struct Person
{
    Person(std::string name, std::string surname, uint8_t years)
        : name(std::move(name))
        , surname(std::move(surname))
        , years(years)
    {
    }

    std::string name;
    std::string surname;
    uint8_t years;
};

Решение в рамках стандартной библиотеки

Давайте начнём с наивной реализации:

std::vector<Person> persons
{
		Person("Jena", "Kisly", 60),
		Person("Vadya", "Molodec", 40), 
		Person("Onnia", "Solnce", 40)
};

auto jenaIter = std::find_if(persons.begin(), persons.end(),
						[](Person const& person) {return person.name == "Jena";});

Очевидна проблема такого решения - слишком долгий поиск по контейнеру. Можно добиться, в среднем, константного времени поиска, вместо линейного(пока что опустим нюанс, что людей с одним именем может быть больше 1). Для этого пусть поле name отныне будет ключом в хэш-таблице:

std::unordered_map<std::string/*name*/, Person> persons
{
		{ "Jena", Person{"Jena", "Kisly", 60} },
		{ "Vadya", Person{"Vadya", "Molodec", 40} },
		{ "Onnia", Person{"Onnia", "Solnce", 40} }
};

auto jenaIter = persons.find("Jena");

Но и в таком решении есть ряд недостатков:

  1. Дублирование одинакового значения(имени) в памяти.
    Плохо, как минимум потому что мы дублируем строки, которые могут занимать много места, и, в рамках предметной области, таких строк может быть много. Следовательно, масштабы дублирования, в перспективе, станут колоссальными.

  2. Проблемы синхронизации, вытекает из п.1.
    Если программисту скажут добавить фичу смены имени, то ему нужно будет помнить о том, что после изменения поля Person::name надо ещё актуализировать значение соответствующего ключа. Можно, конечно, написать для этого специальной обвес, но нужно ли?

  3. Сложность изменения ключа, вытекает из п.2.
    Чтобы добиться синхронизации, нужно будет изменить ключ. Вообще, сделать это до C++17 с его unordered_map<T>::extract красиво не получится, потому что ключ константен. Сделать это можно только через unordered_map<T>::erase+ unordered_map<T>::insert, комбинация которых вроде бы не должна приводить к оверхэду вида ресайза и/или рехэширования.

  4. Решение не выдержит, если мы захотим индексироваться по ещё какому-то полю(а мы обязательно захотим). К примеру, по фамилии.

Проанализировав список проблем, можно сказать, что их часть можно решить довольно просто, если не нарушать DRY. Можем заDRYить, убрав поле Person::name, пусть значение имени будет храниться только в ключе контейнера:

std::unordered_map<std::string/*name*/, Person> persons
{
		{ "Jena", Person{"Kisly", 60} },
		{ "Vadya", Person{"Molodec", 41} },
		{ "Onnia", Person{"Solnce", 40} }
};

auto jenaIter = persons.find("Jena");

Минусы решения налицо:

  1. Нарушение принципа наименьшего удивления.
    Такое решение выглядит как минимум странновато, что значит - новоприбывшему программисту(две недели в отпуске делают нас всех такими) потребуется больше времени на осознание кода.

  2. Высокая связанность со спецификой конкретного контейнера. Это затруднит переход к другому контейнеру, при необходимости.

  3. Никуда не пропала сложность изменения ключа.

  4. Никуда не пропала проблема индексации по другим полям.

Тогда в голову приходит решение через std::unordered_set. Для него нужно будет реализовать хэш-функцию и компаратор по имени:

template<typename ClassWithNameField>
class NameHash
{
public:
    size_t operator()(ClassWithNameField const& person) const
    {
        return std::hash<decltype(person.name)>{}(person.name);
    }
};

template<typename ClassWithNameField>
class NamesEqual
{
public:
    bool operator()(ClassWithNameField const& lhs, ClassWithNameField const& rhs) const
    {
        return lhs.name == rhs.name;
    }
};

void main()
{
    std::unordered_set<Person, NameHash<Person>, NamesEqual<Person>> persons
    {
        Person{"Jena", "Kisly", 60},
        Person{"Vadya", "Molodec", 41},
        Person{"Onnia", "Solnce", 40}
    };

    auto jenaIter = persons.find(Person{"Jena", "blah", 0});
}

Но и такое решение не идеально. Нам приходится делать кучу всего:

  1. Создавать фиктивный объект Person для поиска по имени.

    1. В частности, приходится знать какое из полей Person при поиске - фиктивное, хотя это дело хэш-функции и компаратора.

  2. Писать отдельные от объявления контейнера хэш-функцию и компаратор, что размазывает логику по коду, тем самым затрудняя его понимание.

  3. Людей с одним и тем-же именем может быть больше 1, поэтому множество(set) не подходит по определению.

  4. Никуда не пропала проблема индексации по другим полям.

Решение при помощи transparent компаратора

Забегая немного вперёд, мало кто об этом знает, но начиная с C++14 проблему предыдущего решения 1, связанную с созданием фиктивного объекта, можно устранить, если:

  • У вас ordered контейнер(прим. std::set) и вы используете C++14 или выше .

  • У вас unordered контейнер(прим. std::unordered_set) и вы используете C++20 или выше.

Для этого нужно определить в компараторе алиас is_transparent(какой тип алиасить - не важно). Это позволит использовать перегрузки find( count, upper_bound etc.), которые позволяют сравнивать с элементом в контейнере значения любого другого типа.

На нашем примере это выглядит так:

class PersonsComparator
{
public:
    using is_transparent = void;
		
  	// сравнение по имени, если для поиска передали Person
    bool operator()(Person const& lhs, Person const& rhs) const
    {
        return lhs.name < rhs.name;
    }

  	// сравнение по фамилии, если для поиска передали std::string
    bool operator()(std::string const& name, Person const& person) const
    {
        return name < person.name;
    }
    bool operator()(Person const& person, std::string const& name) const
    {
        return person.name < name;
    }
};

void main()
{
    std::set<Person, PersonsComparator> persons
    {
        Person{"Jena", "Kisly", 60},
        Person{"Vadya", "Molodec", 41},
        Person{"Onnia", "Solnce", 40}
    };

    auto jenaIter = persons.find(Person{"Jena", "Kisly", 60});
    auto onniaIter = persons.find("Onnia");
}

Но данное решение не до конца устраняет проблему [1.1], а только немного видоизменяет: теперь нам надо знать семантику передаваемого в компаратор значения(в данном случае знать, что это - имя), о которой должен знать только компаратор. Но видимо это as design ¯\_(ツ)_/¯

Изначально я даже неверно предположил, что таким решением можно решить проблему [4](индексации по другим полям). Но как верно меня поправили пользователи Хабра, transparent компаратор позволяет сравнивать только по тому полю структуры, которое используется для определения отношения порядка в контейнере. В нашем случае, в методе operator()(Person const &, Person const &), это поле name. То есть transparent  компаратор не решает проблему поиска по нескольким полям, а только позволяет выполнять поиск по значению, не создавая промежуточного пустого объекта Person (решает проблему [1]).

Если же говорить о том, что произойдёт если попробовать сделать индексацию по нескольким полям в transparent компараторе, то @0o00oo00o0 выяснил эмпирически, что поиск по контейнеру с таким компаратором ведёт себя нестабильно и может приводить к падению.

Итог: решение на основе STL

Я не описал все возможные варианты решения, но так или иначе, все они будут иметь схожие существенные проблемы(не обязательно каждую):

  • Слишком много знать о семантике ключа.

  • Размазывать логику сравнения по разным классам, отделяя её от определения контейнера.

  • Смириться с тем, что индексироваться сразу по нескольким полям - несколько проблематично.

Решение на основе стандартной библиотеки получается недостаточно гибким. В такой ситуации приходит время либо пилить самопал, либо же обратиться к готовым решениям. Одно из таких решений - Boost.MultiIndex.

Решение на основе Boost.MultiIndex

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

Возвращаясь к примеру с Person, решение на основе multi_index_container будет выглядеть так:

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

class Person
{
		// name, surname, years
};

using Persons = boost::multi_index::multi_index_container<
        Person,
        boost::multi_index::indexed_by<
            boost::multi_index::hashed_unique<
                boost::multi_index::tag<struct PersonsByName>,
                boost::multi_index::member<Person, decltype(Person::name), &Person::name>
            >
        >
    >;  

void main()
{
    Persons persons
    {
        Person{"Jena", "Kisly", 60},
        Person{"Vadya", "Molodec", 41},
        Person{"Onnia", "Solnce", 40}
    };

    auto const& personsByName = persons.get<PersonsByName>();
    auto jenaIter = personsByName.find("Jena");
}

Да, выглядит посложнее чем с unordered_map. Но зато теперь наши возможности почти безграничны. К примеру, теперь мы можем легко добавить возможность индексироваться и по фамилии:

using Persons = boost::multi_index::multi_index_container<
        Person,
        boost::multi_index::indexed_by<
            boost::multi_index::hashed_unique<
                boost::multi_index::tag<struct PersonsByName>,
                boost::multi_index::member<Person, decltype(Person::name), &Person::name>
            >,
            boost::multi_index::hashed_unique<
                boost::multi_index::tag<struct PersonsBySurname>,
                boost::multi_index::member<Person, decltype(Person::surname), &Person::surname>
            >
        >
    >;  

void main()
{
    Persons persons
    {
        Person{"Jena", "Kisly", 60},
        Person{"Vadya", "Molodec", 41},
        Person{"Onnia", "Solnce", 40}
    };

    auto const& personsByName = persons.get<PersonsByName>();
    auto jenaIter = personsByName.find("Jena");

    auto const& personsBySurname = persons.get<PersonsBySurname>();
    auto vadyaIter = personsBySurname.find("Molodec");
}

Фактически, мы добавили всего ничего кода, но решили почти все существенные проблемы, которые были у предыдущих решений.

Среди альтернативных решений в голову приходит только пара unordered_map, где в одном ключом будет name, а в другом surname. Но такая система из двух карт очень неудобная и рано или поздно приведёт к ошибкам, связанным с синхронизацией элементов контейнеров.
Ну или ещё можно было бы использовать пару unordered_set вместе с is_transparent, как я описывал до этого, но у этого варианта тоже есть существенные недостатки, о которых я писал.

Ещё один из плюсов boost::multi_index::multi_index_container это то, что он позволяет нам довольно просто создавать и использовать составные ключи:

#include <boost/multi_index/composite_key.hpp>

class Person
{
		// name, surname, years
};

using Persons = boost::multi_index::multi_index_container<
        Person,
        boost::multi_index::indexed_by<
            boost::multi_index::hashed_non_unique<
                boost::multi_index::tag<struct PersonsByNameAndSurname>,
                boost::multi_index::composite_key<
                    Person,
                    boost::multi_index::member<Person, decltype(Person::name), &Person::name>,
                    boost::multi_index::member<Person, decltype(Person::surname), &Person::surname>
                >
            >
        >
    >;  

void main()
{
    Persons persons
    {
        Person{"Jena", "Kisly", 60},
        Person{"Jena", "Kisly", 10},
        Person{"Vadya", "Molodec", 41},
        Person{"Onnia", "Solnce", 40}
    };

    auto const& personsByName = persons.get<PersonsByNameAndSurname>();
    auto jenaIter = personsByName.find(boost::make_tuple("Jena","Kisly"));
}

Так же в этом примере мы учли, что существуют люди с одинаковыми именем и фамилией при помощи _non_unique индекса. Теперь, чтобы найти всех людей с одними и теми же именем и фамилией достаточно вызвать метод equal_range:

Persons persons
{
		Person{"Jena", "Kisly", 60},
		Person{"Jena", "Kisly", 62},
		Person{"Vadya", "Molodec", 41},
		Person{"Onnia", "Solnce", 40}
};

auto const& personsByName = persons.get<PersonsByNameAndSurname>();
auto jenasItersPair = personsByName.equal_range(boost::make_tuple("Jena","Kisly"));

// Вывод в зависимости от хэш-функции:
// Jena Kisly 62
// Jena Kisly 60
std::for_each(jenasItersPair.first, jenasItersPair.second,
              [](Person const& person)
              {
                std::cout << person.name 
                  << " " << person.surname
                  << " " << (size_t)person.years << std::endl; 
              });

Проблема с тем, что смена значения ключа довольно некрасивая(до C++17), тоже теперь разрешима. Нужно использовать метод modify_key:

auto& personsByName = persons.get<PersonsByName>();
auto jenaIter = personsByName.find("Jena");
personsByName.modify_key(jenaIter, [](std::string& name){ name="Jonny"; });

Это ещё не вся сила данного контейнера. Существуют и другие виды индексов, которые делают решение на основе данного контейнера более гибким. Если кратко, то инструкция по их выбору примерно следующая:

Способ индексации

Ключ уникален

Вид индекса

Аналогичный STL контейнер

На основе отношения порядка(через сравнение по оператору <>==)

Да

ordered_unique

std::set

Нет

ordered_non_unique

std::multiset

Через хэш

Да

hashed_unique

std::unordered_set

Нет

hashed_non_unique

std::unordered_multiset

Произвольный доступ по целочисленному индексу

random_access

std::vector
std::array

Последовательный доступ

sequenced

std::list

Сравнение производительности Boost.MultiIndex vs STL

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

Согласно проверкам производительности, предоставленным в документации Boost.MultiIndexmulti_index_container превосходит как по пространственной, так и по временной эффективности эквивалентные структуры данных, полученные через ручное комбинирование STL контейнеров. Это становится более заметно, когда количество индексов увеличивается.

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

Заключение

Boost.MultiIndex - мощный инструмент, который за цену довольно допустимых компромиссов даёт большие возможности для индексации по набору данных. Благодарю за внимание!

Комментарии (21)


  1. Sild
    24.05.2022 01:55

    Есть парочка вопросов (предположу что найти на них ответ можно в доке, но раз уж мы тут собрались):
    Что с производительностью?
    Что с накладными расходами на память?
    Какие плюсы/минусы относительно хранения нескольких индексов в нескольких контейнерах, а в значениях - указатели?


    1. agmt
      24.05.2022 07:27
      +2

      Если индексы всё равно нужны, то накладных расходов нет.

      Из особенностей: т.к. для контейнера вся структура ключ, то при необходимости модификации "значения" приходится либо помечать неключевые поля как mutable, либо делать const_cast (вопрос: как это делать канонично?). Использовать modify - явно не вариант, т.к. тогда итерирование замедляется в несколько раз.


      1. reficul0 Автор
        24.05.2022 09:46

        Если вы:
        1. Ищете иных способов изменения значения, кроме modify.
        2. Уверены, что изменение значения поля не повлияет на индексацию(не относится ни к одному индексу).

        То можно получить ссылку на значение через итератор: iter->get_node()->value(). После получения ссылки можно будет модифицировать интересующее вас поле.

        Правда я не уверен насколько этот метод каноничен, да и на безопасность он тоже не претендует. Вполне допускаю, что есть более подходящие способы.


    1. reficul0 Автор
      25.05.2022 05:54

      В сравнении производительности, предоставленном в документации Boost.MultiIndex, авторы утверждают, что multi_index_container превосходит как по пространственной, так и по временной эффективности эквивалентные структуры данных, полученные через ручное комбинирование STL контейнеров. Это становится более заметно, когда количество индексов увеличивается.

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


  1. fougasse
    24.05.2022 08:04

    Так вы производительность меряли или нет? Пляски с логической и физической константностью для mutable никаких проблем не создают? Или вы не обращали внимания?


    1. reficul0 Автор
      25.05.2022 05:58

      Не до конца понял что именно вы имеете ввиду. Предположу, что вы говорите про специфику внутреннего представления ключей в multi_index_container. Если так то, судя по всему, эта специфика либо не влияет на производительность, либо же влияет недостаточно чтобы решение стало хуже эквивалента на STL-ном контейнере.

      В сравнении производительности, предоставленном в документации Boost.MultiIndex, авторы утверждают, что multi_index_container превосходит как по пространственной, так и по временной эффективности эквивалентные структуры данных, полученные через ручное комбинирование STL контейнеров. Это становится более заметно, когда количество индексов увеличивается.

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


  1. svr_91
    24.05.2022 08:47
    +2

    Это позволит использовать перегрузки findcountupper_bound etc.), которые позволяют сравнивать с элементом в контейнере всё что-угодно.

    Не очень понимаю, как это работает (и работает ли это вообще)? Тоесть мы отсортировали set по одному полю, а искать можем по любому другому полю? Это как?


    1. fougasse
      24.05.2022 10:42

      В худшем случае — неправильно. Плюсы ничего особо не запрещают.


    1. reficul0 Автор
      25.05.2022 05:44

      Как оказалось, я действительно неверно понял механизм работы std::set<T>::is_transparent. Прошу прощения. Таким решением нельзя решить проблему [4](индексации по другим полям).

      transparent компаратор позволяет сравнивать только по тому полю структуры, которое используется для определения отношения порядка в контейнере. В нашем случае, в методе operator()(Person const &, Person const &), это поле name. То есть is_transparent не решает проблему поиска по нескольким полям, а только позволяет выполнять поиск по значению, не создавая промежуточного пустого объекта Person (решает проблему [1]).

      Если же говорить о том, что произойдёт если попробовать сделать индексацию по нескольким полям в transparent компараторе, то @0o00oo00o0 выяснил эмпирически, что поиск по контейнеру с таким компаратором ведёт себя нестабильно и может привести к падению:

      mscv2019 программа падает, если заменить в find(41) из примера значение 41 на 40 или 60.


      Поэтому решение на основе transparent компаратора становится не таким привлекательным.


    1. iloskutov
      25.05.2022 05:48

      Собственно для включения «прозрачного сравнения» требуются активные действия именно потому, что это накладывает новый инвариант: компараторы должны быть согласованы. В статье действительно что-то странное предлагается.


  1. ReadOnlySadUser
    24.05.2022 09:22
    +3

    Мне понравилось решение с std:set. Оно проще и не надо городить миллион шаблонов. Если есть вероятность запутаться, на каждое поле в структуре можно завести тип-тэг (PersonByName, PersonBySurnane, PersonByAgeAndName, etc), точно также как это делает решение с boost.

    Вроде это решит все обозначенные проблемы?


    1. reficul0 Автор
      24.05.2022 10:53

      Это действительно решает почти все обозначенные проблемы. Говорю почти все потому что, чтобы применить такое решение нужно:

      1. Использовать минимум C++14, если нужен гетерогенный поиск по упорядоченным контейнерам.

      2. Использовать минимум С++20, если нужен гетерогенный поиск по неупорядоченным контейнерам.

      3. Писать отдельные от объявления контейнера хэш-функции(для неупорядоченного контейнера) и компараторы(для упорядоченного), что размазывает логику по коду, тем самым затрудняя его понимание.

      Ещё, если я вас верно понял, то стоит учитывать, что такое решение потребует написать намного больше кода и потребует больше ручной работы. Поскольку:

      1. Нужно будет определять по структуре тэга с конструктором, принимающим один параметр(если только это не POD), потому что нам надо будет передавать через объект этой тэга значение ключа.

      2. Нужно будет создавать реализацию operator() в компараторе для каждого из тегов вручную. При этом стоит учитывать, что всегда нужно по 2 реализации: когда тег - первый параметр, и когда второй.

      В итоге придётся написать так:

      struct PersonBySurname
      {    
      		PersonBySurname(std::string surname)        
      			: surname(std::move(surname))    
      		{
      		}
      		std::string surname;
      };
      
      class PersonsComparator
      {
      public:
          using is_transparent = void;
      		
          bool operator()(Person const& lhs, Person const& rhs) const
          {
              return lhs.name < rhs.name;
          }
      	
          bool operator()(PersonBySurname const& surnameTag, Person const& person) const
          {
              return surnameTag.surname < person.surname;
          }
          bool operator()(Person const& person, PersonBySurname const& surnameTag) const
          {
              return person.surname < surnameTag.surname;
          }
      };

      Вместо in-place объявления в типе контейнера boost::multi_index::tag<struct PersonsByName> :

      boost::multi_index::hashed_unique<
      		boost::multi_index::tag<struct PersonsBySurname>,
      		boost::multi_index::member<Person, decltype(Person::surname), &Person::surname>
      >

      Масштаб этой проблемы незначителен на маленьком количестве тэгов. Но на маленьком наборе и не получится проверить насколько такое решение гибкое. Если добавить много разных тегов, в т.ч. и композитных, то даже на небольшом множестве из 3 полей может получится уже до 6 структур тегов и 12 реализаций operator() для каждой структуры, и весь этот код придётся писать вручную. Соответственно, в более сложных системах всё будет более печально.

      Поэтому я не считаю такое решение достаточно гибким.


      1. ReadOnlySadUser
        24.05.2022 11:02

        Да, вы меня верно поняли. Я ничего против boost решения не имею, просто иногда он недоступен. Обозначенные проблемы STL решения частично решаются написанием простеньких оберток и - о, ужас! - парочкой макросов. К сожалению, пишу с телефона и потому не могу привести пример.

        Да, надо будет использовать С++14, но ему уже 8 лет, пора бы перейти уже :) А вот с неупорядоченными контейнерами и правда засада, но меня больше беспокоит скорость поиска в по std::set, который сортирует по одному полю, а искать будет по другому. С учётом устройства std::set время поиска будет линейным.


        1. reficul0 Автор
          24.05.2022 11:13

          Я, возможно, понимаю о чём вы говорите, про компенсацию недостатков макросами. В моём понимании, их ещё можно компенсировать метапрограммированием. Но, с моей точки зрения, оба этих решения спорные, потому что в неумелых руках могут принести больше вреда, чем пользы. Среди рисков, использование таких механизмов языка с достаточной вероятностью может увеличить сложность разработки, отладки и может резко увеличить порог входа в решения на её основе.

          Да и даже если обозначенные проблемы решаются, то всё-равно частично.


          1. ReadOnlySadUser
            24.05.2022 11:31
            +2

            С boost все прекрасно, пока он работает. Но как только ты что-то неправильное с точки зрения буста пихаешь в шаблонные параметры - можно заплакать :)

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

            В остальном, да, я согласен с тем, что проблемы не решаются полностью и костылестроение не выход.


            1. reficul0 Автор
              25.05.2022 05:29

              Как оказалось, я неверно понял механизм работы std::set<T>::is_transparent. Прошу прощения. Таким решением нельзя решить проблему [4](индексации по другим полям).

              transparent компаратор позволяет сравнивать только по тому полю структуры, которое используется для определения отношения порядка в контейнере. В нашем случае, в методе operator()(Person const &, Person const &), это поле name. То есть is_transparent не решает проблему поиска по нескольким полям, а только позволяет выполнять поиск по значению, не создавая промежуточного пустого объекта Person (решает проблему [1]).

              Если же говорить о том, что произойдёт если попробовать сделать индексацию по нескольким полям в transparent компараторе, то @0o00oo00o0 выяснил эмпирически, что поиск по контейнеру с таким компаратором ведёт себя нестабильно и может привести к падению:

              mscv2019 программа падает, если заменить в find(41) из примера значение 41 на 40 или 60.


              Поэтому решение на основе transparent компаратора становится не таким привлекательным.


  1. 0o00oo00o0
    24.05.2022 20:58

    В целом, интересный подход с boost. Похоже, multiindex инкапсулирует реализации нескольких ассоциативных структур для разных критериев поиска. В принципе, для себя можно было бы обойтись подобием в виде нескольких unordered_map, ведь именно map - не "карта", а сокращение от "mapping", - отображение.


    1. reficul0 Автор
      25.05.2022 06:14

      В принципе, для себя можно было бы обойтись подобием в виде нескольких unordered_map

      Вероятнее всего, решение на основе нескольких STL-ных контейнеров будет проигрывать multi_index_container как по производительности, так и по сложности поддержки и расширения функциональности. Но если мы говорим про что-то не требовательное к этим параметрам, то почему-бы и нет :)


  1. Izaron
    25.05.2022 12:03

    К сожалению сам Boost.MultiIndex остался "черным ящиком" - неизвестно внутреннее строение контейнера.

    Я придумал схему, по которой сами объекты хранятся в "стандартном" контейнере, а "индекс" это объект `std::multiset<const Person*, comp>`, то есть в индексе лежит указатель на них.

    Тут реализация: https://godbolt.org/z/ne895x73f, но не придумал как быстро искать объект по ключу, потому что std::find_if линейный


    1. reficul0 Автор
      25.05.2022 14:24

      Возможно я неверно понял вашу проблему про линейный поиск, но предположу, что речь про то, что не получается придумать как искать элементы в вашем самопальном индексе быстрее. Конкретно насчёт вашей реализации, в ней каждый 'индекс', это аналог ordered_unique индекса из Boost.MultiIndex. Хоть и используется std::multiset, но специфика его элементов(указателей на значения из другого контейнера) такова, что два указателя на один и тот же объект точно не попадут в этот контейнер. Поэтому, если использовать std::multiset<T>::find то можно добиться логарифмического времени поиска.

      Если же хочется реализовать аналоги для всех видов индексов, то для каждого из индексов в статье я привёл в аналогию STL-ный контейнер, и именно при помощи указанного контейнера можно реализовать аналог того или иного индекса.

      Но в любом случае, ограничения по скорости поиска элемента по индексу всегда будут ограничены внутренним устройством аналогичного STL-ного контейнера. Поэтому, если вы хотите более быстрого поиска, то возможно вам нужен аналог hashed_unique индекса на основе std::unordered_set, с его средним константным временем поиска (чем меньше коллизий, тем вероятнее оно будет таким).


  1. fareloz
    25.05.2022 21:14

    Предположим, что ты - С++ программист и тебе нужно создать справочник

    Можно попросить какой-то реальный пример? Потому что после прочтения статьи у меня осталось впечатление, что решение задачи - БД, а вот такая структура данных - какой-то странный велосипед.

    Меня также смущает, что предложенная структура данных требует обозначить индексацию при создании контейнера, хотя из условия задачи у меня сложилось впечатление, что этот критерий динамический