Я понимаю, что многие могут сказать, что если QMap тебя не устраивает не надо использовать, есть много других вариантов ассоциативных контейнеров в других библиотеках. Но в дань уважения к Qt фреймворку в целом (что они предложили миру) мы считаем, что можно немного подправить QMap для предоставления лучшей функциональности.

Решаем одну старую проблему с QMap.  Если у вас в программе только один QMap (map1), то вы можете спокойно с ним работать: добавлять (insert), изменять(тоже insert), удалять(remove) элементы в контейнере без проблем.

Для конкретности предположим, что мы работаем с QVariantMap контейнером. Если мы теперь создаем второй экземпляр QVariantMap контейнера (map2) и делаем его вложенным в первый QVariantMap контейнер, то теперь мы уже не можем просто взять и изменить элементы второго QVariantMap контейнера (map2). Точнее мы можем только взять второй QVariantMap контейнер по ключу из первого контейнера, потом изменить и далее придется полностью заменить второй контейнер в дереве.

В примере ниже показано, что элемент с ключом key_before и key_after2 появляется в дереве, ключ key_after1 отсутствует:

пример на Qt4
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QpVariantMap map1;
    QpVariantMap map2;

    map2.insert("key_before", "ok");

    map1.insert("map2", map2);

    map2.insert("key_after1", "ok"); // так не правильно

    // так сработает и "key_after2" добавится в map2
    QVariant &vmap2 = map1["map2"];
    QVariantMap map_2 = vmap2.toMap();
    map_2.insert("key_after2", "ok");
    map1.insert("map2" , map_2 );


    qDebug()<< "---------------------------------";
    qDebug()<< "map1 "<< QtJson::serialize1( map1 );
    qDebug()<< "---------------------------------";

   return 0;
}
вывод на экран:
---------------------------------
map1  "{
    "map2": {
        "key_after2": "ok",
        "key_before": "ok"
    }
}"
---------------------------------

Ключ key_after2 появляется потому,что мы берем целую копию всего map2 из map1 (по ключу "map2") далее добавляем key_after2 в это строчке map_2.insert("key_after2", "ok"); и перезаписываем заново весь map2 в дереве.

Теперь если у вас появляется третий контейнер (map3), вложенный во второй, что произойдет? Похоже, чтобы изменить элемент в третьем контейнере придется перезаписывать второй контейнер вместе с третьим и так далее.

Тут вся загвоздка в том, что вы не можете использовать примерно такую конструкцию map1["map2"]["map3"]["prop"]="value133"; (как в том же js или php).

На практике у вас возможно только так (поправьте если я ошибаюсь):

map1.value["map2"].toMap().value("map3").toMap().value("map3").toMap().insert("prop","value133");

И value() и toMap() возвращают только копии элементов контейнера. И это получается большая проблема.

То есть чем больше уровней вложенности, тем больше данных придется перезаписывать, что конечно хорошим программированием не назовешь. А почему такое происходит, потому что данные в контейнерах QVariantMap хранятся как QVariant, что с одной стороны очень удобно, а с другой стороны, чтобы получить доступ к элементам второго контейнера нам приходится использовать только один известный штатный вариант это метод toMap(). И вот тут оказывается, что toMap() может  возвращать только копию QVariantMap. И чего нам с ним теперь делать? Правильно изменить и потом остаётся только  полностью перезаписать целиком всю ветку в дереве.

Как решать эту проблему?

Для истории: сначала надо бы походить отладчиком по исходному коду Qt и тогда многое станет очевидным. Например элементы дерева (ключи и значения) создаются только в куче, никогда на стеке. Это логично, так как далее используется метод подсчёта ссылок на объект, чтобы удалять его только при обнулении счётчика ссылок на него. Так вот на стеке объект удаляется сразу после выхода из области видимости (например из функции).
И все ключи дерева связаны между собой по принципу первый указывает на следующего и т.д.

Все это приводит к простым выводам, что ничего вроде не должно нам мешать добавить новый ключ-значение в дерево.
Все что нам нужно это только найти родителя (контейнер типа QVariantMap) и сделать штатно ему insert(key,value).

И вот тут оказывается, что есть только один метод, который позволит этого добиться, это void *data() класса QVariant (но хотя бы он есть). Значениями QVariantMap как мы понимаем являются QVariant и поэтому мы можем достучаться до содержимого в QVariant только его штатными средствами. А штатное средство для типа QVariant:Map это метод toMap(), который возвращает именно копию QVariantMap. Вопрос зачем он так делает пока оставим на совести разработчиков Qt и подумаем - можем ли мы  из void* data() получить указатель на QVariantMap. Ну так это он и есть, тот самый указатель на объект в памяти (куча).

Единственно что нам остаётся это сделать приведение типа через interpretet_cast (по-видимому). Это конечно не безопасно, точнее вообще не безопасно, кто вам гарантирует, что там в памяти QVariantMap? Единственно поможет сначала получить копию QVariant из метода toMap() и потом проверить ее через isValid() и canConvert(QVariant::Map).

Но у нас появилось небольшое развитие шаблонного класса QMap (назовем его QpMap) с целью напрямую работать с элементами дерева из QVariantMap и работать с ними прямо в памяти (в куче), выносим на ваш суд и делимся результатами. Полной уверенности,что мы все делаем правильно у нас нет, но в первом тестировании все работает без проблем.

class QpMap
#ifndef QPMAP_H
#define QPMAP_H

#include <QMap>
#include <QVariantMap>
#include <QString>
#include "common/json/my_json.h"

template <class Key, class T>
class QpMap : public QMap<Key,T>
{
public:
    QpMap():QMap<Key,T>(){};
    

    // -----------------------------------------------------------------
    // addToMap добавляет в QVariantMap ключ/значение на любой уровень
    // вложенности дерева QVarinatMap
    // -----------------------------------------------------------------

    bool addtoMap( const QStringList &lst,
                   const QString & key,
                   const QVariant & val,
                   bool createKey_IfNotExist = true)
    {
        QpMap<QString,QVariant> *pMap  = this;

        foreach( QString name , lst)
        {

            QVariantMap::Iterator it1 = pMap->find(name);

            if( it1 != pMap->constEnd()) // именно mmm->constEn...
            {
                // ---------------------------------------------------
                // если ключ СУЩЕСТВУЕТ получим ссылку на него в куче
                // ---------------------------------------------------

                if( it1.key() != name)
                {
                    return false;
                }

                if( ! getPtr ( it1.value() , &pMap ) ) // переставляем указатель на следующий QVariantMap
                    return false;

            }
            else if ( createKey_IfNotExist )
            {
                // -------------------------------------------
                // если ключ ЕЩЕ НЕ СУЩЕСТВУЕТ создадим его
                // -------------------------------------------

                pMap->insert( name, QpVariantMap() );
                //pMap->insert( name, new QpVariantMap() ); // можно и так

                QVariantMap::Iterator it2 = pMap->find(name);

                if( it2 != constEnd() && it2.key() == name)
                {
                    if( ! getPtr ( it2.value() , &pMap ) ) // переставляем указатель на следующий QVariantMap
                        return false;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }
        // на выходе устанавливаем ключ значение в контейнере дерева

        //*mmm->insert(key, val); // ИЗМЕНИЛИ/ДОБАВИЛИ НОВЫЙ ЭЛЕМЕНТ !!!
        pMap->insert(key, val); //  странно но работает и так ????

        return true;
    }

    bool getPtr( QVariant &var, QpMap<QString,QVariant> **pMap)
    {
        // ---------------------------------------------------
        // приведение типа
        // ---------------------------------------------------

        *pMap = reinterpret_cast<QpMap<QString,QVariant>*>( var.data());

         QVariant *vv = reinterpret_cast<QVariant*>( var.data() );

        if(! vv->isValid() || ! vv->canConvert( QVariant::Map))
            return false;


        // проверили это  QVariantMap
        return true;
    }
};

typedef QpMap<QString,QVariant> QpVariantMap ;

#endif // QPMAP_H

теперь используем QpMap addtoMap в примере
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QpVariantMap map1;
    QpVariantMap map2;

    map2.insert("key_before", "ok");

    map1.insert("map2", map2);

    // так сработает и "key_after2" добавится в map2
    QVariant &vmap2 = map1["map2"];
    QVariantMap map_2 = vmap2.toMap();
    map_2.insert("key_after2", "ok");
    map1.insert("map2" , map_2 );

    map1.addtoMap( QString("map2").split(","), "key_after1" , "ok" );  // ТЕПЕРЬ ТАК РАБОТАЕТ!
    map1.addtoMap( QString("m2,m3,m4").split(","), "key_after1" , "ok" );  // И ТАК РАБОТАЕТ!

    qDebug()<< "---------------------------------";
    qDebug()<< "map1 "<< QtJson::serialize1( map1 );
    qDebug()<< "---------------------------------";

   return 0;
}
вывод
---------------------------------
map1  "{
    "m2": {
        "m3": {
            "m4": {
                "key_after1": "ok"
            }
        }
    },
    "map2": {
        "key_after1": "ok",
        "key_after2": "ok",
        "key_before": "ok"
    }
}"
---------------------------------

Выложили на гитхабе A_little_development_of_QMap.

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


  1. Kotofay
    02.07.2024 17:58

    QMap умеет возвращать ключ по значению.
    Скорее всего это и было препятствием в быстрой реализации.

    Key QMap::key(const T &value, const Key &defaultKey = Key()) const


    1. kkmspb Автор
      02.07.2024 17:58

      QMap умеет возвращать ключ по значению.

      Не спорю

      Скорее всего это и было препятствием в быстрой реализации.

      В реализации чего?


  1. voldemar_d
    02.07.2024 17:58
    +1

    #include "common/json/my_json.h"

    Что это? И почему уровень статьи "сложный"?


    1. kkmspb Автор
      02.07.2024 17:58

      #include "common/json/my_json.h

      Это только для вывода в qDebug QVariantMap в более читабельном виде (json строка с переносам и т.д.)

      Что это? И почему уровень статьи "сложный"?

      Не придирайтесь, какой уровень по вашему?


      1. voldemar_d
        02.07.2024 17:58
        +3

        Я не придираюсь. Просто посмотрите другие статьи на Хабре про C++ с уровнем "сложный". Для начала, на их объем, и на содержание.

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

        Но в дань уважения к Qt фреймворку в целом (что они предложили миру мы считаем, что можно немного подправить QMap для предоставления лучшей функциональности.

        Так что мы считаем-то? Где закрывающая скобка? И т.д. по всему тексту.


        1. kkmspb Автор
          02.07.2024 17:58

          Где закрывающая скобка?

          Да спс ,не скомпилируется


          1. voldemar_d
            02.07.2024 17:58

            Я не про код, а про эту фразу: "Но в дань уважения к Qt фреймворку в целом (что они предложили миру" - открывающая скобка есть, закрывающей нет, явно что-то пропущено.


            1. kkmspb Автор
              02.07.2024 17:58

              Я не про код, а про эту фразу

              Я понял, шучу так


  1. Deosis
    02.07.2024 17:58

    В Qt 6.6 добавили методы get/get_if для получения типизированных ссылок на внутренний элемент QVariant

    Это должно решить проблемы работы с копиями.


    1. kkmspb Автор
      02.07.2024 17:58

      добавили методы get/get_if для получения типизированных ссылок на внутренний элемент QVariant

      Спасибо! Хорошая новость, хоть что-то начинает меняться в Qt. Но для меня опять получается не повод переходить на Qt6 (т.к. сам навелосипедил, а свое ближе к ...)


    1. kkmspb Автор
      02.07.2024 17:58

      Это должно решить проблемы работы с копиями.

      Чего-то сомнения взяли должно или решили? Не знаете?


      1. Deosis
        02.07.2024 17:58

        Я не работал с версией выше 6, поэтому не могу сказать наверняка.


  1. binque
    02.07.2024 17:58

    Давно не писал на Qt, но вроде проблема из-за использования именно класса QVariantMap. Вот так должно работать:

    QMap<QString, QMap<QString, QMap<QString, QVariant>>> map;
    map["map1"]["map2"]["prop"] = "value123";
    

    Но это если вы всегда храните значения только по такой структуре дерева.

    Также у вас будет неопределенное поведение, если в значении лежит не QpMap<QString,QVariant>, а другой тип, который может конвертироваться в него. Лучше смотреть, какой конкретно тип лежит в QVariant, используя QVariant::userType() или схожие методы.


    1. kkmspb Автор
      02.07.2024 17:58

      Map<QString, QMap<QString, QMap<QString, QVariant>>> map;
      Но это если вы всегда храните значения только по такой структуре дерева.

      Да, в это и есть проблема, надо заранее дерево планировать.


    1. kkmspb Автор
      02.07.2024 17:58

      Лучше смотреть, какой конкретно тип лежит в QVariant

      Да конечно обязательно и там (в QpMap) проверяется методом canConvert.


      1. binque
        02.07.2024 17:58

        Я про это и говорю. Я могу создать какой-нибудь свой класс MyVariantMap и потом добавить конвертер через QMetaType::registerConverter<MyVariantMap, QVariantMap>(). Тогда вызов QVariant::fromValue(MyVariantMap{}).canConvert(QVariant::Map) будет возвращать истину, хотя внутри лежит не QVariantMap. Поэтому метод canConvert() для этой цели использовать нельзя.


        1. kkmspb Автор
          02.07.2024 17:58
          +1

          Поэтому метод canConvert() для этой цели использовать нельзя.

          Спасибо учту


  1. Playa
    02.07.2024 17:58
    +2

    Qt4

    На дворе 2024 год...


    1. kkmspb Автор
      02.07.2024 17:58

      На дворе 2024 год...

      Не хочется разводить флуд, но мне кажется что в этой маленькой статейке и есть ответ. То есть не смотря на то какой год в Qt приходится вставлять очевидные костыли.


  1. 9241304
    02.07.2024 17:58

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

    Вольдэмар все по делу написал)