как изменить в многоуровневом контейнере QMap глубоко вложенный отдельный элемент

О чем тут речь. В контейнерах QMap (или также QHash) можно создавать многоуровневую структуру со всеми возможными типа данных (из набора QVariant и не только).

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

То есть это приводит к тому, что если вы этот элемент в оригинале потом изменили, то его копия ничего об этом не узнает.

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

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

Что имеем в начале

Вот простой пример:

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    mmA.insert("mmB" , mmB);
    mm.insert("mmA" , mmA);

    mmB.insert("bbb1",111111);
    mmA.insert("aaa1",222222);

    QVariant & vmmA = mm["mmA"]; //это ссылка ок
    QVariantMap &vmmA1 = vmmA.toMap(); // это уже копия
    QVariantMap &vmmA2( mmA );

    vmmA1.insert("test1" , "ok");
    vmmA2.insert("test2" , "ok");

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

    return 0;

    return a.exec();
}

Вывод на экран:

---------------------------------
mm "{
    "mmA": {
        "mmB": {

        }
    }
}"
---------------------------------

Элементов bbb1 и aaa1 нет в контейнере, потому что они добавлялись после добавления самого контейнера (которому они принадлежат). И это очень не удобно. Нужен способ добавлять в дерево mm элементы в любой время.

Что мы примерно хотим

Еще как пример у вас не такого штатного способа установить новое значение произвольному элементу:
mm["mmA"]["mmB"]["bbb2"]="234";
вам приходится делать так:

QVariant &rmmA = mm["mmA"];
QVariantMap &rmmA_ = rmmA.toMap() // а тут уже опять копия

Долгое время такого способа мы не знали, хотя внутренне подозрения были, что как-то можно выкрутится. В php и js это кстати штатно все работает без проблем.

Логика подсказывает, что все элементы в результате insert создаются в куче (через  new или Malloc). Удаляется любой элемент контейнера после уменьшения всех количества всех ссылок на него до нуля (QBasicAtomicInt ref;). Так почему бы не добавить еще один элемент в контейнер.

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

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

data()

Нашли мы только один реально нужный public метод это - data(), который всегда вернет указатель на объект (в куче) , который и является нашим объектом (значением ключа). Но указатель этот void*, а это значит, что кому-то надо проверять на валидность, что там в памяти сидит (там может быть все, что угодно). Примерно так можно его использовать:

QVariantMap::Iterator it = mm.find("mmA");

if( it != mm.constEnd() && it.key() == "mmA")
{
	// нашли

	QVariantMap *mmm = (QVariantMap*)(it.value().data());

	*mmm->insert("test2" , "ok");
}	

И это сработает! Но есть нюанс вам надо точно знать, что в it.value().data() у вас QVariantMap*, в противном случае вас ждет коллапс.

---------------------------------
mm "{
    "int": 123,
    "mmA": {
        "mmB": {

    },
        "test2": "ok"  // вот он появился
    }
}"
---------------------------------

Теперь как изменить глубоко вложенный элемент

Тут надо пройтись по всем, начиная с корня контейнера, ключам и проделать примерно то же самое.

Вот такой наш шаблонный класс QpMap унаследованный от QMap подойдет:

#ifndef QPMAP_H
#define QPMAP_H

#include <QMap>
#include <QVariantMap>
#include <QString>

typedef QVariantMap QpVariantMap ;

template <class Key, class T>
class QpMap : public QMap<Key,T>
{

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

    inline bool addtoMap2( const QStringList &lst, const QString & key, const QVariant & val)
    {
        QVariantMap *mmm = this;

        foreach( QString name , lst)
        {
            QVariantMap::Iterator it = mmm->find(name);

            if( it != constEnd() && it.key() == name)
            {
                // нашли существующий по ключу элемент
                mmm = static_cast<QVariantMap*>(it.value().data());

                QVariant *vv = static_cast<QVariant*>(it.value().data());

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

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

        *mmm->insert(key, val); // ИЗМЕНИЛИ/ДОБАВИЛИ НОВЫЙ ЭЛЕМЕНТ !!!

        return true;
    }
};


#endif // QPMAP_H

Используем его так:

mm.addtoMap2( QString("mmA,mmB").split(","), "test2" , "ok" );

Вывод - все работает похоже ОК:

---------------------------------
mm "{
    "int": 123,
    "mmA": {
        "mmB": {
            "test2": "ok"
        }
    }
}"
---------------------------------

Это первый наш опыт создания шаблонных классов, возможно есть еще нюансы (надо понаблюдать).



Развитие_QMap_доступ_и_изменение_элементов_дерева.mp4

Файлы для скачивания