Не удается прочитать QList‹Class*› из файла

У меня проблема с оператором потока>>. Я пытаюсь сохранить и загрузить в файл QList пользовательских объектов. Процедура сохранения работает нормально, но чтение файла приводит к сбою. Я подготовил очень минимальный пример. Прежде всего пользовательский класс:

class CustomObject : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName)
public:
    explicit CustomObject(QObject *parent = 0);
    CustomObject(const CustomObject & copy, QObject *parent = 0);

    QString name() const;
    void setName( const QString & name);

private:
    QString m_name;
};

Q_DECLARE_METATYPE( CustomObject )

QDataStream& operator<<( QDataStream& dataStream, const CustomObject * item );
QDataStream& operator>>( QDataStream& dataStream, CustomObject * item );

Я реализовал потоковые операторы следующим образом:

QDataStream &operator<<(QDataStream &dataStream, const CustomObject *item)
{
    for(int i = 0; i < item->metaObject()->propertyCount(); ++i) {
        if(item->metaObject()->property(i).isStored(item)) {
            dataStream << item->metaObject()->property(i).read(item);
        }
    }
    return dataStream;
}


QDataStream &operator>>(QDataStream &dataStream, CustomObject *item)
{
    QVariant var;
    for(int i = 0; i < item->metaObject()->propertyCount(); ++i) {
        if(item->metaObject()->property(i).isStored(item)) {
            dataStream >> var;
            item->metaObject()->property(i).write(item, var);
        }
    }
    return dataStream;
}

Это функция save() (m_objectsList это QList<CustomObject*>)

QFile saveFile("/tmp/SaveFile");
if(!saveFile.open(QIODevice::WriteOnly)) {
    qWarning() << saveFile.errorString();
    return;
}

QDataStream outStream(&saveFile);
outStream.setVersion(QDataStream::Qt_4_8);
outStream << m_objectsList;
saveFile.close();

и это процедура read():

QFile saveFile("/tmp/SaveFile");
if(!saveFile.open(QIODevice::ReadOnly)) {
    qWarning() << saveFile.errorString();
    return;
}

QDataStream inStream(&saveFile);
inStream >> m_objectsList;
saveFile.close();

Приложение segfaults в операторе условия цикла for в операторе>>:

i < item->metaObject()->propertyCount()

item недоступен.

Можете ли вы объяснить мне, где ошибка?

Большое спасибо.


person bullet    schedule 12.06.2014    source источник
comment
В Qt 5 вы должны использовать QSaveFile вместо QFile при сохранении файла.   -  person Kuba hasn't forgotten Monica    schedule 12.06.2014
comment
спасибо за этот совет, я потихоньку перехожу на qt5   -  person bullet    schedule 13.06.2014


Ответы (2)


Ваш код segfaults, потому что item является висячим указателем. Никто его не инициализировал. Вы должны создать экземпляр item до того, как прочтете его.

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

В приведенном ниже коде представлены две шаблонные функции, которые сериализуют список объектов (в соответствии с его свойствами). Во-первых, давайте повторим тип объекта:

// https://github.com/KubaO/stackoverflown/tree/master/questions/prop-storage-24185694
#include <QtCore>

class CustomObject : public QObject {
   Q_OBJECT
   Q_PROPERTY(QString name READ name WRITE setName STORED true)
   QString m_name;
public:
#ifdef Q_MOC_RUN
   Q_INVOKABLE CustomObject(QObject *parent = {})
#endif
   using QObject::QObject;

   QString name() const { return m_name; }
   void setName(const QString &name) { m_name = name; }
};

Некоторые помощники:

/// Returns a zero-copy byte array wrapping a C string constant
static QByteArray baFromCStr(const char *str) {
   return QByteArray::fromRawData(str, qstrlen(str));
}

/// Returns a list of stored properties for a given type
QList<QMetaProperty> storedProperties(const QMetaObject *mo) {
   QList<QMetaProperty> stored;
   for (int i = 0; i < mo->propertyCount(); ++i) {
      auto prop = mo->property(i);
      if (prop.isStored())
         stored << prop;
   }
   return stored;
}

/// Caches strings for saving to a data stream
struct SaveCache {
   QMap<QByteArray, qint32> strings;
   QDataStream &save(QDataStream &str, const QByteArray &string) {
      auto it = strings.find(string);
      if (it != strings.end())
         return str << (qint32)it.value();
      auto key = strings.count();
      strings.insert(string, key);
      return str << (qint32)key << string;
   }
   QDataStream &save(QDataStream &str, const char *string) {
      return save(str, baFromCStr(string));
   }
};

/// Caches strings while loading from a data stream
struct LoadCache {
   QList<QByteArray> strings;
   QDataStream &load(QDataStream &str, QByteArray &string) {
      qint32 index;
      str >> index;
      if (index >= strings.count()) {
         str >> string;
         while (strings.size() < index)
            strings << QByteArray{};
         strings << string;
      } else
         string = strings.at(index);
      return str;
   }
};

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

template <typename T>
QDataStream &writeObjectList(QDataStream &str, const QList<T*> &items) {
   str << (quint32)items.count();
   if (! items.count()) return str;
   str << (quint8)1; // version

   SaveCache strings;
   for (QObject *item : items) {
      auto *mo = item->metaObject();
      // Type
      strings.save(str, mo->className());
      // Properties
      auto const stored = storedProperties(mo);
      auto const dynamic = item->dynamicPropertyNames();
      str << (quint32)(stored.count() + dynamic.count());
      for (auto &prop : qAsConst(stored))
         strings.save(str, prop.name()) << prop.read(item);
      for (auto &name : dynamic)
         strings.save(str, name) << item->property(name);
   }
   return str;
}

Метод чтения должен попробовать два способа создания экземпляра объекта:

template <typename T> QDataStream &readObjectList(QDataStream &str,
                                                  QList<T*> &items)
{
   quint32 itemCount;
   str >> itemCount;
   if (!itemCount) return str;
   quint8 version;
   str >> version;
   if (version != 1) {
      str.setStatus(QDataStream::ReadCorruptData);
      return str;
   }

   LoadCache strings;
   for (; itemCount; itemCount--) {
      QByteArray string;
      // Type
      T *obj = {};
      strings.load(str, string);
      if (T::staticMetaObject.className() == string)
         obj = new T();
      else {
         string.append('*');
         auto id = QMetaType::type(string);
         const auto *mo = QMetaType::metaObjectForType(id);
         if (mo)
            obj = qobject_cast<T*>(mo->newInstance());
      }
      if (obj)
         items << obj;
      // Properties
      quint32 propertyCount;
      str >> propertyCount;
      for (uint i = 0; i < propertyCount; ++i) {
         QVariant value;
         strings.load(str, string) >> value;
         if (obj) obj->setProperty(string, value);
      }
   }
   return str;
}

И очень простой тестовый жгут:

QDataStream &operator<<(QDataStream &str, const QList<CustomObject*> &items) {
   return writeObjectList(str, items);
}

QDataStream& operator>>(QDataStream &str, QList<CustomObject*> &items) {
   return readObjectList(str, items);
}

int main() {
   qRegisterMetaType<CustomObject*>(); // necessary for any classes derived from CustomObject*

   QBuffer buf;
   buf.open(QBuffer::ReadWrite);
   QDataStream ds(&buf);

   CustomObject obj;
   obj.setObjectName("customsky");
   obj.setProperty("prop", 20);
   QList<CustomObject*> list;
   list << &obj;
   ds << list;

   QList<CustomObject*> list2;
   buf.seek(0);
   ds >> list2;
   Q_ASSERT(list2.size() == list.size());
   for (int i = 0; i < list.size(); ++i) {
      auto *obj1 = list.at(i);
      auto *obj2 = list2.at(i);
      Q_ASSERT(obj1->objectName() == obj2->objectName());
      Q_ASSERT(obj1->dynamicPropertyNames() == obj2->dynamicPropertyNames());
      for (auto &name : obj1->dynamicPropertyNames())
         Q_ASSERT(obj1->property(name) == obj2->property(name));
   }
}

#include "main.moc"
person Kuba hasn't forgotten Monica    schedule 12.06.2014
comment
ваш код работает нормально, но, насколько я понимаю, он не сохраняет QList как есть, а разбивает его на простые элементы, как эта наивная версия: outStream ‹‹ (quint32)m_objectsList.count(); foreach (CustomObject *obj, m_objectsList) { outStream ‹‹ obj; } - person bullet; 13.06.2014
comment
Причина, по которой он разбивает QList, заключается в том, что оператор, который передает сам список, является слишком общим, чтобы предотвратить возможную массовую избыточность вывода. Пока что мой код сохраняет каждое имя свойства только один раз. Возможно дальнейшее применение байт-ориентированного кодирования Хаффмана к индексам имен свойств, дальнейшее сокращение сериализованного представления за счет временного хранения таблиц Хаффмана. - person Kuba hasn't forgotten Monica; 13.06.2014
comment
За универсальность QDataStream & operator<<(QDataStream&, const QList&) приходится платить. Вы должны повторно использовать код, когда это имеет смысл. Здесь повторное использование этого оператора не имеет особого смысла. Помните, что в операторе потока, специфичном для QList, нет ничего особенного. Это не так, как если бы он каким-то образом сериализовал тот факт, что хранится QList, и т. д. Сериализованное представление списка — это просто счет, за которым следует сериализованное представление элементов списка, в порядке от нулевого индекса вверх. - person Kuba hasn't forgotten Monica; 13.06.2014
comment
Итак, то, что вы заметили, правда, но это не повод для беспокойства. Способ его использования по-прежнему заключается в том, что вы просто передаете список как есть, но используете наш пользовательский оператор вместо оператора по умолчанию. Все, что мы делаем, — это заменяем другой оператор: вместо замены оператора, специфичного для CustomObject*, мы заменяем оператор, специфичный для QList<CustomObject*>. - person Kuba hasn't forgotten Monica; 13.06.2014

Ваш код дает сбой, потому что в вашем операторе ввода указатель item на самом деле не указывает на объект и, вероятно, равен нулю. Чтобы исправить это, оператор должен взять ссылку на указатель и создать новый экземпляр CustomObject(). Что-то вроде этого:

QDataStream &operator>>(QDataStream &dataStream, CustomObject *& item)
{
    QVariant var;
    item = new CustomObject();
    for(int i = 0; i < item->metaObject()->propertyCount(); ++i) {
        if(item->metaObject()->property(i).isStored(item)) {
            dataStream >> var;
            item->metaObject()->property(i).write(item, var);
        }
    }
    return dataStream;
}
person Dan Milburn    schedule 12.06.2014
comment
Теперь, используя вашу строку, приложение может успешно передать оператор ››, но я получил segfault при попытке получить свойства объектов, хранящихся в QList: foreach (CustomObject *obj, m_objectsList) { qDebug() ‹‹ obj-›name(); } Отладчик указывает на строку 725 файла qstring.h: inline QString::QString(const QString &other) : d(other.d) { Q_ASSERT(&other != this); d-›ref.ref(); } (name() должен вернуть QString) - person bullet; 13.06.2014