У меня есть «User
«класс с более чем 40 закрытыми переменными, включая сложные объекты, такие как закрытые / открытые ключи (библиотека QCA), пользовательские объекты QObject и т. д. Идея состоит в том, что в классе есть функция, называемая sign()
который шифрует, подписывает, сериализует себя и возвращает QByteArray
который затем может быть сохранен в BLOB-объекте SQLite.
Каков наилучший подход для сериализации сложного объекта? Итерация через свойства с QMetaObject
? Преобразовать его в объект protobuf?
Может ли он быть приведен к массиву символов?
Может ли он быть приведен к массиву символов?
Нет, потому что ты будешь кастовать QObject
внутренние компоненты, о которых вы ничего не знаете, указатели, которые недействительны при втором запуске программы и т. д.
TL; DR: реализовать его вручную можно для явных элементов данных и использовать систему метаобъектов для QObject
а также Q_GADGET
занятия помогут некоторые из рутинной работы.
Самым простым решением может быть реализация QDataStream
операторы для объекта и типов, которые вы используете. Обязательно следуйте хорошей практике: каждый класс, который мог бы когда-либо изменить формат данных, которые он содержит, должен выдавать идентификатор формата.
Например, давайте возьмем следующие классы:
class User {
QString m_name;
QList<CryptoKey> m_keys;
QList<Address> m_addresses;
QObject m_props;
...
friend QDataStream & operator<<(QDataStream &, const User &);
friend QDataStream & operator>>(QDataStream &, User &);
public:
...
};
Q_DECLARE_METATYPE(User) // no semi-colon
class Address {
QString m_line1;
QString m_line2;
QString m_postCode;
...
friend QDataStream & operator<<(QDataStream &, const Address &);
friend QDataStream & operator>>(QDataStream &, Address &);
public:
...
};
Q_DECLARE_METATYPE(Address) // no semi-colon!
Q_DECLARE_METATYPE
макрос делает классы известными QVariant
и QMetaType
система типов. Таким образом, например, можно назначить Address
к QVariant
переведите такую QVariant
в Address
, для потоковой передачи варианта непосредственно в поток данных и т. д.
Во-первых, давайте рассмотрим, как сбросить QObject
свойства:
QList<QByteArray> publicNames(QList<QByteArray> names) {
names.erase(std::remove_if(names.begin(), names.end(),
[](const QByteArray & v){ return v.startsWith("_q_"); }), names.end());
return names;
}
bool isDumpable(const QMetaProperty & prop) {
return prop.isStored() && !prop.isConstant() && prop.isReadable() && prop.isWritable();
}
void dumpProperties(QDataStream & s, const QObject & obj)
{
s << quint8(0); // format
QList<QByteArray> names = publicNames(obj.dynamicPropertyNames());
s << names;
for (name : names) s << obj.property(name);
auto mObj = obj.metaObject();
for (int i = 0; i < mObj->propertyCount(), ++i) {
auto prop = mObj->property(i);
if (! isDumpable(prop)) continue;
auto name = QByteArray::fromRawData(prop.name(), strlen(prop.name());
if (! name.isEmpty()) s << name << prop.read(&obj);
}
s << QByteArray();
}
В общем, если бы мы имели дело с данными из User
что не было m_props
член, мы должны быть в состоянии очистить свойства. Эта идиома будет появляться каждый раз, когда вы расширяете хранимый объект и обновляете формат сериализации.
void clearProperties(QObject & obj)
{
auto names = publicNames(obj.dynamicPropertyNames());
const QVariant null;
for (name : names) obj.setProperty(name, null);
auto const mObj = obj.metaObject();
for (int i = 0; i < mObj->propertyCount(), ++i) {
auto prop = mObj->property(i);
if (! isDumpable(prop)) continue;
if (prop.isResettable()) {
prop.reset(&obj);
continue;
}
prop.write(&obj, null);
}
}
Теперь мы знаем, как восстановить свойства из потока:
void loadProperties(QDataStream & s, QObject & obj)
{
quint8 format;
s >> format;
// We only support one format at the moment.
QList<QByteArray> names;
s >> names;
for (name : names) {
QVariant val;
s >> val;
obj.setProperty(name, val);
}
auto const mObj = obj.metaObject();
forever {
QByteArray name;
s >> name;
if (name.isEmpty()) break;
QVariant value;
s >> value;
int idx = mObj->indexOfProperty(name);
if (idx < 0) continue;
auto prop = mObj->property(idx);
if (! isDumpable(prop)) continue;
prop.write(&obj, value);
}
}
Таким образом, мы можем реализовать потоковые операторы для сериализации наших объектов:
#define fallthrough
QDataStream & operator<<(QDataStream & s, const User & user) {
s << quint8(1) // format
<< user.m_name << user.m_keys << user.m_addresses;
dumpProperties(s, &m_props);
return s;
}
QDataStream & operator>>(QDataStream & s, User & user) {
quint8 format;
s >> format;
switch (format) {
case 0:
s >> user.m_name >> user.m_keys;
user.m_addresses.clear();
clearProperties(&user.m_props);
fallthrough;
case 1:
s >> user.m_addresses;
loadProperties(&user.m_props);
break;
}
return s;
}
QDataStream & operator<<(QDataStream & s, const Address & address) {
s << quint8(0) // format
<< address.m_line1 << address.m_line2 << address.m_postCode;
return s;
}
QDataStream & operator>>(QDataStream & s, Address & address) {
quint8 format;
s >> format;
switch (format) {
case 0:
s >> address.m_line1 >> address.m_line2 >> address.m_postCode;
break;
}
return s;
}
Система свойств также будет работать для любого другого класса, если вы объявите его свойства и добавите Q_GADGET
макрос (вместо Q_OBJECT
). Это поддерживается начиная с Qt 5.5.
Предположим, что мы объявили наш Address
Класс следующим образом:
class Address {
Q_GADGET
Q_PROPERTY(QString line1 MEMBER m_line1)
Q_PROPERTY(QString line2 MEMBER m_line2)
Q_PROPERTY(QString postCode MEMBER m_postCode)
QString m_line1;
QString m_line2;
QString m_postCode;
...
friend QDataStream & operator<<(QDataStream &, const Address &);
friend QDataStream & operator>>(QDataStream &, Address &);
public:
...
};
Давайте тогда объявим операторы потока данных с точки зрения [dump|clear|load]Properties
модифицировано для работы с гаджетами:
QDataStream & operator<<(QDataStream & s, const Address & address) {
s << quint8(0); // format
dumpProperties(s, &address);
return s;
}
QDataStream & operator>>(QDataStream & s, Address & address) {
quint8 format;
s >> format;
loadProperties(s, &address);
return s;
}
Нам не нужно менять указатель формата, даже если набор свойств был изменен. Мы должны сохранить обозначение формата на случай, если у нас будут другие изменения, которые больше не могут быть выражены как простой дамп свойства. В большинстве случаев это маловероятно, но следует помнить, что решение не использовать спецификатор формата немедленно устанавливает формат потоковых данных в камне. Впоследствии это невозможно изменить!
Наконец, обработчики свойств — это несколько урезанные и модифицированные варианты тех, которые используются для QObject
свойства:
template <typename T> void dumpProperties(QDataStream & s, const T * gadget) {
dumpProperties(s, T::staticMetaObject, gadget);
}
void dumpProperties(QDataStream & s, const QMetaObject & mObj, const void * gadget)
{
s << quint8(0); // format
for (int i = 0; i < mObj.propertyCount(), ++i) {
auto prop = mObj.property(i);
if (! isDumpable(prop)) continue;
auto name = QByteArray::fromRawData(prop.name(), strlen(prop.name());
if (! name.isEmpty()) s << name << prop.readOnGadget(gadget);
}
s << QByteArray();
}
template <typename T> void clearProperties(T * gadget) {
clearProperties(T::staticMetaObject, gadget);
}
void clearProperties(const QMetaObject & mObj, void * gadget)
{
const QVariant null;
for (int i = 0; i < mObj.propertyCount(), ++i) {
auto prop = mObj.property(i);
if (! isDumpable(prop)) continue;
if (prop.isResettable()) {
prop.resetOnGadget(gadget);
continue;
}
prop.writeOnGadget(gadget, null);
}
}
template <typename T> void loadProperties(QDataStream & s, T * gadget) {
loadProperties(s, T::staticMetaObject, gadget);
}
void loadProperties(QDataStream & s, const QMetaObject & mObj, void * gadget)
{
quint8 format;
s >> format;
forever {
QByteArray name;
s >> name;
if (name.isEmpty()) break;
QVariant value;
s >> value;
auto index = mObj.indexOfProperty(name);
if (index < 0) continue;
auto prop = mObj.property(index);
if (! isDumpable(prop)) continue;
prop.writeOnGadget(gadget, value);
}
}
СДЕЛАТЬ Проблема, которая не была рассмотрена в loadProperties
Реализация должна очистить свойства, которые присутствуют в объекте, но не присутствуют в сериализации.
Очень важно установить, как весь поток данных версионируется, когда дело доходит до внутренней версии QDataStream
форматы. документация является обязательным чтением.
Также необходимо решить, как обрабатывается совместимость между версиями программного обеспечения. Есть несколько подходов:
(Наиболее типичный и неудачный) Нет совместимости: информация о формате не сохраняется. Новые члены добавляются в сериализацию специальным образом. Более старые версии программного обеспечения будут демонстрировать неопределенное поведение, когда сталкиваются с новыми данными. Более новые версии будут делать то же самое со старыми данными.
Обратная совместимость: информация о формате хранится в сериализации каждого пользовательского типа. Новые версии могут правильно работать со старыми версиями данных. Старые версии должны обнаруживать необработанный формат, прерывать десериализацию и сообщать пользователю об ошибке. Игнорирование новых форматов приводит к неопределенному поведению.
Полная обратная и прямая совместимость: каждый сериализованный пользовательский тип хранится в QByteArray
или аналогичный контейнер. Делая это, вы получаете информацию о том, как долго будет длиться запись данных для всего типа. QDataStream
версия должна быть исправлена. Чтобы прочитать пользовательский тип, сначала читается его байтовый массив, затем QBuffer
настроен на то, что вы используете QDataStream
читать с. Вы читаете элементы, которые можете анализировать в известных вам форматах, и игнорируете остальные данные. Это вызывает постепенный подход к форматам, когда более новый формат может добавлять элементы только к существующему формату. Но если более новый формат отказывается от какого-либо элемента данных из более старого формата, он все равно должен сбросить его, но с нулевым или иным безопасным значением по умолчанию, которое делает старые версии вашего кода «счастливыми».
Если вы считаете, что байты формата могут когда-либо заканчиваться, вы можете использовать схему кодирования переменной длины, известную как расширение или расширенный октет, знакомую по различным стандартам МСЭ (например, Q.931 4.5.5 Информационный элемент «Способность носителя». Идея заключается в следующем: старший бит октета (байта) используется, чтобы указать, нужно ли значению больше октетов для представления. Это позволяет байту иметь 7 бит для представления значения и 1 бит для обозначения расширения. Если бит установлен, вы читаете последующие октеты и объединяете их в порядке с прямым порядком байтов к существующему значению. Вот как вы можете сделать это:
class VarLengthInt {
public:
quint64 val;
VarLengthInt(quint64 v) : val(v) { Q_ASSERT(v < (1ULL<<(7*8))); }
operator quint64() const { return val; }
};
QDataStream & operator<<(QDataStream & s, VarLengthInt v) {
while (v.val > 127) {
s << (quint8)((v & 0x7F) | 0x80);
v.val = v.val >> 7;
}
Q_ASSERT(v.val <= 127);
s << (quint8)v.val;
return s;
}
QDataStream & operator>>(QDataStream & s, VarLengthInt & v) {
v.val = 0;
forever {
quint8 octet;
s >> octet;
v.val = (v.val << 7) | (octet & 0x7F);
if (! (octet & 0x80)) break;
}
return s;
}
Сериализация VarLengthInt
имеет переменную длину и всегда использует минимальное число байтов, возможных для данного значения: 1 байт до 0x7FF, 2 байта до 0x3FFF, 3 байта до 0x1F’FFFF, 4 байта до 0x0FFF’FFFF и т. д. Апострофы действительны в C ++ 14 целочисленных литералов.
Это будет использоваться следующим образом:
QDataStream & operator<<(QDataStream & s, const User & user) {
s << VarLengthInt(1) // format
<< user.m_name << user.m_keys << user.m_addresses;
dumpProperties(s, &m_props);
return s;
}
QDataStream & operator>>(QDataStream & s, User & user) {
VarLengthInt format;
s >> format;
...
return s;
}
Двоичная сериализация дампов — плохая идея, она будет включать в себя множество вещей, которые вам не нужны, например указатель v-таблицы объекта, а также другие указатели, содержащиеся непосредственно или от других членов класса, которые не имеют смысла сериализоваться, так как они не сохраняются между сеансами приложения.
Если это всего лишь один класс, просто реализуйте его вручную, это точно не убьет вас. Если у вас есть семья классов, и они QObject
производная, вы можете использовать метасистему, но она будет регистрировать только свойства, тогда как int something
член, который не привязан к свойству, будет пропущен. Если у вас есть много элементов данных, которые не являются свойствами Qt, вам понадобится больше печатать, чтобы представить их как свойства Qt, я бы добавил это излишне, чем если бы вы писали метод сериализации вручную.