Russian Qt Forum
Ноябрь 25, 2024, 15:10 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

Войти
 
  Начало   Форум  WIKI (Вики)FAQ Помощь Поиск Войти Регистрация  

Страниц: 1 [2]   Вниз
  Печать  
Автор Тема: Наследование template  (Прочитано 15618 раз)
Racheengel
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2679


Я работал с дискетам 5.25 :(


Просмотр профиля
« Ответ #15 : Май 12, 2016, 12:12 »

Когда я имею дело с каким-либо классом я в первую очередь буду ожидать от него следующего стандартного поведения (когда речь идёт о чтении/записи)
Код
C++ (Qt)
out << obj;
in >> obj;
 

а не искать у него методов типа Write/storeData и т.д..  Улыбающийся

Что значит "стандартное" поведение? В любом случае Вам придется для каждого поддерживаемого формата перегружать операторы. Сериализация в QDataStream будет отличаться от std::stream, таким образом, в любом случае obj должен явно поддерживать оба контейнера. Поэтому особых преимуществ, честно говоря, не вижу.

Что еще важно - как по мне, если методы определены явно в классе - это повышает читабельность, т.к. человек сразу видит его интерфейс, и то, с чем "умеет" работать класс. В этих же методах явно определяется порядок сериализации и десериализации. А наличие внешних сериалайзеров и их дополнительные специализации - это вводит дополнительные сущности и в итоге усложняет понимание системы классов.
Записан

What is the 11 in the C++11? It’s the number of feet they glued to C++ trying to obtain a better octopus.

COVID не волк, в лес не уйдёт
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #16 : Май 12, 2016, 12:45 »

Цитировать
Что значит "стандартное" поведение?
Я вот о чём:
Код
C++ (Qt)
QVector<MyClass> vector;
out << vector;
 
QVector<YourClass> vector;
for (auto & elem : vector)
{
   elem.write(out);
}
 
Я выбираю первый вариант.
Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #17 : Май 12, 2016, 13:07 »

Я вот о чём:
Код
C++ (Qt)
QVector<MyClass> vector;
out << vector;
...
 
Я выбираю первый вариант.
Какой Вы быстрый Улыбающийся А что бум делать если первый вариант вдруг не устраивает? См напр здесь.

Наивно полагать что нарисовав << и >> мы уже решили все проблемы Улыбающийся Это даже не пол-дела. В действительности поток I/O должен иметь какую-то организацию, а не просто "идущие подряд данные" с которыми потом хрен разберешься.

В любом случае Вам придется для каждого поддерживаемого формата перегружать операторы. Сериализация в QDataStream будет отличаться от std::stream,
Да, именно. Ну может пример с бинарной/текстовой сериализацией проще и выразительнее. В первом случае нужно писать ID и длины, во втором нужны имена и правила разбивки на строки

Что еще важно - как по мне, если методы определены явно в классе - это повышает читабельность, т.к. человек сразу видит его интерфейс, и то, с чем "умеет" работать класс. В этих же методах явно определяется порядок сериализации и десериализации. А наличие внешних сериалайзеров и их дополнительные специализации - это вводит дополнительные сущности и в итоге усложняет понимание системы классов.
Существенно усложняет - но другого не видно, иначе никак не изолировать инклуды ненужных классов.
Записан
Racheengel
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2679


Я работал с дискетам 5.25 :(


Просмотр профиля
« Ответ #18 : Май 12, 2016, 14:37 »

Я выбираю первый вариант.

Но это как раз и означает, что операторы >> << должны быть определены в контексте MyClass, т.к. сериализация QVector<T> явно требует этого. И для этого ВНЕШНИЕ классы в принципе не нужны.
Записан

What is the 11 in the C++11? It’s the number of feet they glued to C++ trying to obtain a better octopus.

COVID не волк, в лес не уйдёт
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #19 : Май 12, 2016, 16:05 »

Цитировать
Но это как раз и означает, что операторы >> << должны быть определены в контексте MyClass, т.к. сериализация QVector<T> явно требует этого
Да, и я  реализую их вместо мемберов write/read. 
Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
ssoft
Программист
*****
Offline Offline

Сообщений: 584


Просмотр профиля
« Ответ #20 : Май 12, 2016, 17:47 »

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

Методы сериализации реализуются с помощью специализаций шаблонов

Код
C++ (Qt)
template < typename _Archive, typename _Value, int _version >
struct Serializer;
template < typename _Archive, typename _Value, int _version >
struct Loader;
template < typename _Archive, typename _Value, int _version >
struct Saver;
 

Для простоты объяснения понятие версии опущу.

По умолчанию реализация методов выглядит так

Код
C++ (Qt)
template < typename _Archive, typename _Value >
struct Serializer
{
   static void invoke ( _Archive & archive, _Value & value )
   {
       archive.corrupt( value );
   }
};
 
template < typename _Archive, typename _Value >
struct Loader
{
   static void invoke ( _Archive & archive, _Value & value )
   {
       Serializer< _Archive, _Value >::invoke( archive, value );
   }
};
 
template < typename _Archive, typename _Value >
struct Saver
{
   static void invoke ( _Archive & archive, const _Value & value )
   {
       Serializer< _Archive, _Value >::invoke( archive, const_cast< _Value & > ( value ) );
   }
};
 

По умолчанию, Loader и Saver вызывают один и тот же метод Serializer::invoke, и если методы сериализации и десериализации ни чем не отличаются (как это обычно и бывает), то достаточно специализировать только Serializer. Если отличия существуют из-за геттеров и сеттеров, например, то специализировать придется и Loader, и Saver (Serializer специализировать в этом случае не требуется).

Предположим, что нам необходимо сериализовать структуру

Код
C++ (Qt)
struct SimpleStruct
{
   int m_int;
   double m_double1;
   double m_double2;
};
 

Для этого необходимо специализировать один единственный метод сериализации

Код
C++ (Qt)
template < typename _Archive >
struct Serializer< _Archive, SimpleStruct >
{
   static void invoke ( _Archive& archive, SimpleStruct & value )
   {
       archive
           .serialize( value.m_int )
           .serialize( value.m_double1 )
           .serialize( value.m_double2 );
   }
};
 

Таким образом, один метод реализует и сериализацию и десериализацию. Но как и куда?
Здесь определяющую роль играет понятие архива. Архив - это ваш произвольный класс реализующий интерфейс для сериализации

Код
C++ (Qt)
class OutputArchive
{
public:
   template < typename _Value >
   Archive & serialize ( const _Value & value );
 
   template < typename _Value >
   void corrupt ( const _Value & value, int version );
};
 

или для десериализации

Код
C++ (Qt)
class InputArchive
{
public:
   template < typename _Value >
   Archive & serialize ( _Value & value );
 
   template < typename _Value >
   void corrupt ( _Value & value );
};
 

Куда/откуда и как происходит сохранение/чтение параметров решает разработчик архива.

Архив может сериализовать в std::stream, в QDataStream, в QTextStream, в разных форматах - XML, JSON, BIN и т.п. и т.д.

Для сериализации данных контейнеров типа std::vector, QVector для этих типов так же необходимо реализовать специализацию Serializer.

Для наглядности - пример архива

Код
C++ (Qt)
 
class XmlOArchive
{
private:
   QTextStream m_stream;
 
public
   XmlOArchive ( QIODevice * device )
   : m_stream( device )
   {
       m_stream.setCodec( "utf-8" );
       // ...
   }
 
public:
   template < typename _Value >
   Archive & serialize ( _Value & value )
   {
       QByteArray open_tag = "<Value>"
       QByteArray close_tag = "</Value>";
       m_stream << open_tag;
       Loader< _Value >::invoke( *this, value );
       m_stream << close_tag;
       return *this;
   }
 
   // Specialize for all POD types, for example.
   Archive & serialize ( int value )
   {
       QByteArray open_tag = "<Int>"
       QByteArray close_tag = "</Int>";
       m_stream
           << open_tag
           << value
           << close_tag;
       return *this;
   }
 
   template < typename _Value >
   void corrupt ( _Value & value )
   {
        qWarning() << "Archive corrupted";
   }
}
 
 

Доступ к приватным членам, нужен не часто, при правильной реализации должно хватать API класса.
Но если этого нет, можно использовать friend зависимости.
Записан
m_ax
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2095



Просмотр профиля
« Ответ #21 : Май 12, 2016, 17:57 »

Т.е. это фактически boost'овский вариант сериализации? Или есть принципиальные отличия?
Записан

Над водой луна двурога. Сяду выпью за Ван Гога. Хорошо, что кот не пьет, Он и так меня поймет..

Arch Linux Plasma 5
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #22 : Май 13, 2016, 06:12 »

Могу рассказать, как у нас решена задача сериализации в общем виде, причем с поддержкой версий структур данных для возможной бинарной совместимости.
Да, обобщить "туды-сюды" у меня руки так и не дошли. Интересно, спасибо

Такой момент: вот "простейший пример"
Код
C++ (Qt)
stream << a << b << c;
И типа "ну вот и вся сериализация" (одним движением). Однако это предполагает жесткий, неизменный порядок следования данных, все они должны быть прочитаны. Также ничего не известно о типе данных, мы должны полагаться что a (b, c) - именно данные тех самых типов что мы хотим. Поэтому так работает хорошо для относительно простых, базовых структур. В более сложных случаях данные должны быть идентифицированы и могут быть сериализованы в различном порядке. Как Вы решаете эту проблему?  
Записан
ssoft
Программист
*****
Offline Offline

Сообщений: 584


Просмотр профиля
« Ответ #23 : Май 13, 2016, 08:19 »

Т.е. это фактически boost'овский вариант сериализации? Или есть принципиальные отличия?

Да, вариант очень похож на boost). Отличия существуют в ведении версий структур данных.
У нас версия сериализации также является специализируемым параметром шаблона, а не входящим параметром в метод сериализации, как в boost. И по-умолчанию, используется самая последняя версия сериализации, а не 0, как в boost. Есть еще непринципиальные отличия - названия методов, использование операторов, но базовые концепции идентичны, а решения очень похожи.

Код
C++ (Qt)
stream << a << b << c;
И типа "ну вот и вся сериализация" (одним движением). Однако это предполагает жесткий, неизменный порядок следования данных, все они должны быть прочитаны. Также ничего не известно о типе данных, мы должны полагаться что a (b, c) - именно данные тех самых типов что мы хотим. Поэтому так работает хорошо для относительно простых, базовых структур. В более сложных случаях данные должны быть идентифицированы и могут быть сериализованы в различном порядке. Как Вы решаете эту проблему?   

1. Кроме линейной/последовательной сериализации, таким же образом решена задача параметрической сериализации.

Код
C++ (Qt)
template < typename _Archive, typename _Key , typename _Value >
struct Parametrizer
{
   static void invoke ( _Archive & archive, const _Key & key, _Value & value )
   {
       archive.parametrize( key, value );
   }
};
 

Выдержка из документации:

Цитировать
Механизмы сериализации можно разделить на:
* линейные
* параметрические

Линейный механизм сериализации подразумевает под собой последовательный порядок сохранения и восстановления данных. Другими словами, чтобы получить пятую запись необходимо восстановить и все четыре предыдущие. Параметрический механизм позволяет получить записи по уникальному параметру - ключу. Таким образом та же 5-я запись может быть получена из десериализованных данных сразу, минуя предыдущие четыре.

Преимуществом линейного способа сериализации является максимальная скорость сохранения/восстановления данных в их полном виде, так как в параметрической форме при восстановлении требуется осуществлять поиск необходимой записи по параметрическому ключу. Тем не менее, в параметрическом виде можно сериализовывать не все записи, а только часть, и иногда осуществлять сериализацию/десериализацию между разными типами.

2. Для понимания, какой тип сохраняется и какая версия метода сериализации используется, эту информацию можно записать в архив.

У нас архив устроен так, что сериализация/десериалзация данных происходит пакетами QByteArray. Зачитываем пакет, из пакета читаем тип данных и версию сериализации, если зачитать не смогли, то пакет может быть проигнорирован, или особо обработан, или процесс десериализации прерван.

Я не описал, как ведется версионность, но это оочень длинный пост получается. К слову сказать, чтобы не заниматься явной специализацией, предусмотрены макросы, упрощающие запись

Код
C++ (Qt)
// version enumerator
SERIALIZE_VERSIONS_BEGIN( SimpleStruct )
   SERIALIZE_VERSION( Version_0 )
SERIALIZE_VERSIONS_END
 
// serialize & deserialize
SERIALIZE_BEGIN( SimpleStruct, Version_0 )
{
   archive
       .serialize( value.m_int )
       .serialize( value.m_double1 )
       .serialize( value.m_double2 );
}
SERIALIZE_END
 
Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #24 : Май 15, 2016, 11:27 »

Цитировать
Параметрический механизм позволяет получить записи по уникальному параметру - ключу. Таким образом та же 5-я запись может быть получена из десериализованных данных сразу, минуя предыдущие четыре.
Может не самый лучший пример - не припомню когда мне была нужна только 5-я запись Улыбающийся Зато
Код
C++ (Qt)
stream >> a >> b >> c;  // было так (версия 1.0)
stream >> a >> a1 >> b >> c;  // появилась новая структура (версия 1.1)
stream >> a >> b >> x >>  c;  // неизвестная структура "x" (версия 1.0 читает данные записанные в 2.0)
 
Таких случаев хоть отбавляй. Создавать новую "версию потока" на каждое изменение - ну не знаю, выглядит накладно.

Но все равно, основательно у Вас в конторе сделано. Спасибо
Записан
Racheengel
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 2679


Я работал с дискетам 5.25 :(


Просмотр профиля
« Ответ #25 : Май 15, 2016, 12:49 »

У нас десериализация работает в 2 этапа:
1. Выгребаются ВСЕ сохраненные параметры в двоичном виде в формате ключ.длина.значения и пакуются в мэп.
2. А уже из мэпа достаются нужные параметры по ключам.
В итоге и очередность не важна, и контент можно конвертировать из предыдущих версий.
Версия архива пишется в хэдер, т.к. она для всех данных едина.
Записан

What is the 11 in the C++11? It’s the number of feet they glued to C++ trying to obtain a better octopus.

COVID не волк, в лес не уйдёт
ssoft
Программист
*****
Offline Offline

Сообщений: 584


Просмотр профиля
« Ответ #26 : Май 15, 2016, 18:52 »

Может не самый лучший пример - не припомню когда мне была нужна только 5-я запись Улыбающийся

Здесь имеется ввиду, что при изменении версии структуры, ее новые данные сериализуются по новым ключам, а удаленные можно не сериализовать совсем.
Тогда старое ПО легко может частично десериализовать новую структуру и не "сломается". В линейном случае так легко это не прокатит.
Хорошим примером такого подхода является protobuf.

Создавать новую "версию потока" на каждое изменение - ну не знаю, выглядит накладно.

Здесь не требуется создавать новую версию потока (архива), требуется изменить только метода сериализации для структуры, а версию структуры можно сохранять в сам архив.
Тогда при чтении по версии можно определить, может ли структура быть десериализована и каким способом.

У нас версию имеет не архив, а сама структура. Это может показаться накладно, однако такой подход обеспечивает бинарную совместимость между различными версиями ПО. Это важно, например, когда ПО состоит из множества разных процессов, взаимодействующих между собой, а при обновлении система не может быть остановлена.

У нас десериализация работает в 2 этапа:
1. Выгребаются ВСЕ сохраненные параметры в двоичном виде в формате ключ.длина.значения и пакуются в мэп.
2. А уже из мэпа достаются нужные параметры по ключам.
...

Это и есть параметризация, когда данные пакуются по ключам.
Записан
_Bers
Бывалый
*****
Offline Offline

Сообщений: 486


Просмотр профиля
« Ответ #27 : Май 17, 2016, 00:36 »

Добрый день

Название темы корявое (не нашел лучшего), пример: вот сейчас я рисую некий template
Код
C++ (Qt)
template <class T>
struct MyTemplateClass {
 
 T * Get( size_t );
 void Set( T *, size_t );
 
 void Read( MyStream & );
 void Write( MyStream & ) const;
};
Первые 2 метода универсальны (не завязаны на др классы), а следующие 2 не очень - совсем необязательно что и в др проекте будет MyStream. Может в данном случае лучше сделать внешние операторы << и >>, но это частное решение. А как в общем случае сделать аккуратнее?

Спасибо

это и есть общее и общепринятое решение.

Записан
Страниц: 1 [2]   Вверх
  Печать  
 
Перейти в:  


Страница сгенерирована за 0.64 секунд. Запросов: 22.