Я понимаю, что многие могут сказать, что если 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.

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


  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 умеет возвращать ключ по значению.

      Не спорю

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

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