понедельник, 15 июля 2013 г.

Доступ к protected извне: ловкость рук и никакого мошенничества

Не так чтоб каждый день, но иногда всё же требуется получить доступ к protected методам какого-то класса. Простейший пример — QPlainTextEdit. Сразу несколько методов, непонятно для чего сделанных защищёнными (тот же firstVisibleBlock(), например). Конечно, в теории следует наследоваться от базового класса, и таким образом получить доступ. Но не всегда есть возможность это сделать (если, скажем, базовый класс создаётся в какой-то части системы, которая ничего не знает о классе-наследнике), да и не всегда нужно. На днях у меня как раз возникла такая ситуация, поэтому спешу поделиться решением (найденным в интернете, честно говоря, но несколько доработанным).

Итак, простейший случай: метод не перегружен. Тут всё более-менее просто. Смотрим пример:
 class A  
 {  
 protected:  
   void f() {}  
 };  
   
 class B  
 {  
 public:  
   void g()  
   {  
     A a;  
     //делаем что-то с экземпляром A  
     //а вот теперь небольшой фокус  
     struct AHack : A  
     {  
       using A::f;  
       static void fHack(A *a)  
       {  
         if (!a)  
           return;  
         (a->*&AHack::f)();  
       }  
     };  
     //теперь вызываем защищённый метод  
     AHack::fHack(&a);  
     //продолжаем работу  
   }  
 };  
Вот такой трюк с приведением типов. Хоть мы и наследовались от базового класса, но при вызове метода не создавали новых экземпляров.

Теперь представим, что метод A::f() перегружен:
 class A  
 {  
 protected:  
   void f() {}  
   void f(int) {}  
 };  
Тут компилятор выдаст приблизительно такую ошибку:
 ... ошибка: '& A::f' cannot be used as a member pointer, since it is of type '<unresolved overloaded function type>'  
Что делать? Как говорится, "we need to go deeper" (кто не в курсе — погуглите). Слегка изменим нашу структуру AHack:
 struct AHack : A  
 {  
   //using больше не понадобится  
   static void fHack(A *a)  
   {  
     if (!a)  
       return;  
     reinterpret_cast<AHack *>(a)->f();  
   }  
   static void fHack(A *a, int i)  
   {  
     if (!a)  
       return;  
     reinterpret_cast<AHack *>(a)->f(i);  
   }  
 };  
Такой способ является универсальным и будет работать как в случае с перегруженным методом, так и в случае с неперегруженным. Теперь можно вызывать метод A::f() в обоих вариантах:
 AHack::fHack(&a);  
 AHack::fHack(&a, 10);  
К reinterpret_cast обычно прибегать не рекомендуется, так как малейшая ошибка запросто может привести к падению программы, а найти эту ошибку будет довольно сложно. Если бы мы привели тип A * к типу AHack * и использовали бы какой-то метод, отсутствующий в A, то с большой долей вероятности программа бы упала. Но поскольку мы обращаемся только к методам A, то такое приведение типов можно назвать безопасным (но только в данной ситуации). Подробнее о приведении типов можно почитать в одном весьма и весьма интересном блоге, а у меня на сегодня всё.

понедельник, 8 июля 2013 г.

Перевод на лету: так ли всё просто, как кажется?

Сегодня мне хотелось бы рассказать о некоторых приёмах работы с системой перевода интерфейса в Qt. На первый взгляд может показаться, что всё предельно просто (тривиальные примеры в документации этому способствуют). Ну в самом деле, что сложного? Обёртываем строку в tr() или translate(), генернируем файл .ts, переводим, собираем в файл .qm, загружаем его — вот и всё. Но практика, как известно, зачастую несколько расходится с теорией.

Итак, переходим к примеру. Допустим, есть некое приложение, интерфейс которого должен быть переведён на пару-тройку языков. При этом к нему прилагается библиотека, в свою очередь также содержащая элементы, нуждающиеся в переводе. Итого: для каждого языка (кроме, возможно, исходного английского) имеется набор из трёх файлов переводов: appname_xx.qm, libname_xx.qm и (чтобы текст на кнопках подтверждения, отмены и т.д. также был на выбранном языке) qt_xx.qm (тут xx — имя локали для выбранного языка). Это ещё довольно простой случай, ведь иногда основной проект может зависеть от нескольких вспомогательных библиотек, и тогда файлов переводов может быть больше.

Уже посложнее, чем пример из документации, верно? А если мы, к примеру, хотим предоставить наиболее продвинутым пользователям возможность самим переводить интерфейс на их родной язык? В таком случае обычно предлагается помещать нужный файл переводов в соответствующую папку в домашнем каталоге. Итак, что мы имеем: несколько файлов переводов для разных частей приложения, расположенных в разных местах. Их обнаружение и загрузка — дело уже далеко не такое простое, как загрузка одного единственного файла перевода с известным путём.

Что же делать? Выход, конечно же, есть — создать класс, который брал бы на себя обязанности по обнаружению, загрузке и дальнейшему использованию переводов. Начнём, пожалуй:
 class Translator  
 {  
 private:  
   QString mfileName;  
   QLocale mlocale;  
   bool minstalled;  
   QList<QTranslator *> mtranslators;  
 public:  
   QString fileName() const  
   {  
     return mfileName;  
   }  
   QLocale locale() const  
   {  
     return mlocale;  
   }  
   bool isValid() const  
   {  
     return !mfileName.isEmpty();  
   }  
   bool isInstalled() const  
   {  
     return minstalled;  
   }  
   void install()  
   {  
     if (minstalled || !isValid())  
       return;  
     QStringList paths; //в эту переменную добавляем все пути к переводам (то есть список абсолютных путей к папкам)  
     foreach (QString path, paths)  
     {  
       QTranslator *t = new QTranslator;  
       if (t->load(mlocale, mfileName, "_", path, ".qm"))  
       {  
         mtranslators << t;  
         QCoreApplication::installTranslator(t);  
       }  
       else  
       {  
         delete t;  
       }  
     }  
     minstalled = true;  
   }  
   void remove()  
   {  
     if (!minstalled)  
       return;  
     foreach (QTranslator *t, mtranslators)  
     {  
       QCoreApplication::removeTranslator(t);  
       delete t;  
     }  
     mtranslators.clear();  
     minstalled = false;  
   }  
   void setFileName(QString fn)  
   {  
     bool wasInstalled = isInstalled();  
     if (wasInstalled)  
       remove();  
     mfileName = fn;  
     if (!isValid())  
       return;  
     if (wasInstalled)  
       install();  
   }  
   void setLocale(QLocale l)  
   {  
     bool wasInstalled = isInstalled();  
     if (wasInstalled)  
       remove();  
     mlocale = l;  
     if (wasInstalled)  
       install();  
   }  
 public:  
   explicit Translator(QString fn = QString(), QLocale l = QLocale())  
   {  
     minstalled = false;  
     setFileName(fn);  
     setLocale(l);  
   }  
   ~Translator()  
   {  
     remove();  
   }  
 };  
Что это мы тут нагородили? Давайте разбираться. Мы обернули список указателей на QTranslator в удобный для работы класс. Этот класс имеет методы для смены имени файла (например, это appname у файла appname_ru.qm или libname у файла libname_fr.qm, то есть, если можно так выразиться, "значащая" часть имени), а также для смены локали. И, конечно же, мы можем устанавливать и убирать экземпляры нашего класса. При этом внутри используется всё тот же знакомый по документации метод QCoraApplication::installTranslator. Данный класс почти готов к использованию, достаточно только указать, в каких папках искать переводы (метод install, переменная paths).

Уфф, половину проблемы решили. Осталось разобраться с получением списка языков, для которых доступны переводы. Приступим:
 QList<QLocale> availableLocales(QString fileName)  
 {  
   if (fileName.isEmpty())  
     return QList<QLocale>();  
   QList<QLocale> list;  
   QStringList validFiles;  
   QStringList paths; //тут вновь перечисляем пути к папкам, где могут лежать файлы переводов  
   foreach (QString path, paths)  
   {  
     QDir dir(path);  
     QStringList files = dir.entryList(QStringList() << "*.qm", QDir::Files); //получаем список файлов .qm  
     foreach (QString file, files)  
     {  
       //проверяем, что "значимая" часть имени файла соответствует имени, переданному в качестве параметра  
       if (file.left(fileName.length()) == fileName && !validFiles.contains(file))  
         validFiles << file;  
     }  
   }  
   foreach (QString file, validFiles)  
   {  
     int suffixPos = fileName.length() + 1; //индекс, с которого начинается "суффикс" имени файла, содержащий имя локали  
     int suffixLength = validFile.length() - fileName.length() - 4; //длина суффикса: вычитаем длину "значимой" части, длину "_" и длину ".qm"  
     QString localeName = validFile.mid(suffixPos, suffixLength);  
     QLocale l(localeName);  
     list << l;  
   }  
   return list;  
 }  
Вот такая простая, но полезная функция. Тут следует заметить, что при использовании её, а также приведенного выше класса предполагается, что файлы переводов именуются следующим образом: name_xx.qm, где name — "значимая" часть имени, соответствующая названию программы или библиотеки, xx — имя локали, а .qm - стандартный суффикс для файлов переводов, генерируемых средствами Qt.

Однако, пока мы решали проблему удобства, другая, не менее важная проблема — проблема производительности — осталась за кадром. А она имеет место быть. Как известно, перевод интерфейса "на лету" осуществляется путём переопределения метода QWidget::changeEvent, проверки типа события (он должен быть QEvent::LanguageChange) и, собственно, перевода видимых элементов с использованием функции tr. А теперь представьте себе программу, содержащую около тысячи видимых и подлежащих переводу элементов. Пользователь меняет язык интерфейса и все пять (к примеру) файлов перевода сначала убираются, а затем устанавливаются вновь (с другой локалью), вызывая аж десять событий смены языка. Итого — десять тысяч вызовов setText, setToolTip и т.д. Это уже будет заметно на глаз (и едва ли приятно, так как приложение на секунду "подвисает").

Надо что-то делать. Например, можно воспользоваться вот этим примером. Суть его такова: создаётся объект-посредник, к его слоту подсоединяется некий сигнал. При вызове слота запускается два таймера: "промежуточный" (в примере он назван "минимальным", что, на мой взгляд, не совсем корректно) и "максимальный". Если в течение некоторого промежутка времени слот вызывается снова, то "промежуточный" таймер перезапускается. Как только один из таймеров срабатывает, объект испускает соответствующий сигнал. Пример по ссылке выше приспособлен под определённую ситуацию, я же несколько "причесал" его под общий случай. Скачать можно тут.

А теперь то, ради чего было это лирическое отступление. Создадим ещё один простенький класс, который установим в качестве фильтра событий на экземпляр QApplication (или QCoreApplication). Вот он:
 class LanguageChangeNotifier : public QObject  
 {  
   Q_OBJECT  
 private:  
   SignalDelayProxy *proxy;  
 public:  
   explicit LanguageChangeNotifier()  
   {  
     proxy = new SignalDelayProxy(50, 100 this);  
     connect(proxy, SIGNAL(triggered()), this, SIGNAL(languageChanged()));  
   }  
 public:  
   bool eventFilter(QObject *o, QEvent *e)  
   {  
     if (e->type() != QEvent::LanguageChange)  
       return false;  
     proxy->trigger();  
     return true;  
   }  
 signals:  
   void languageChanged();  
 };  
Устанавливаем наш фильтр:
 LanguageChangeNotifier *notifier = new LanguageChangeNotifier;  
 QApplication::installEventFilter(notifier);  
Готово! Теперь в виджетах, где требуется перевод элементов, добавляем слот:
 void OurWidget::retranslateUi()  
 {  
   //тут переводим интерфейс, например так:  
   myLabel->setText(tr("some text"));  
 }  
И где-нибудь в конструкторе, ссылаясь на глобальный экземпляр LanguageChangeNotifier:
 connect(notifier, SIGNAL(languageChanged()), this, SLOT(retranslateUi()));  
Теперь мы получим крохотную задержку в одну десятую секунды (которую едва ли можно заметить) перед сменой языка интерфейса, зато избавимся от потенциально гораздо более существенной задержки, а также от зависания приложения из-за десяти тысяч лишних вызовов функций (от которых мы, к слову, тоже избавимся).

четверг, 4 июля 2013 г.

Пароли: безопасный ввод и хранение

Перейду сразу к рассмотрению примера. Допустим, у нас есть некий сервер, к которому подключаются пользователи. Для идентификации используются логин и пароль. Клиентами могут быть как консольные приложения, так и приложения с графическим интерфейсом. Задача: организовать безопасный ввод пароля, а также не менее безопасные хранение (как на сервере, так и в настройках клиента) и передачу этого пароля на сервер. Что ж, приступим.

Начнём с хранения на сервере и передачи. Тут всё достаточно просто: пароль нужно хранить и передавать в зашифрованном виде. Вполне подойдёт, например, алгоритм SHA-1. В Qt есть замечательный класс QCryptographicHash, позволяющий зашифровать пароль при помощи выбранного алгоритма, в том числе и SHA-1. Как это выглядит на практике:
 QString pwd;  
 //вводим пароль  
 QByteArray encryptedPwd = QCryptographicHash::hash(pwd.toUtf8(), QCryptographicHash::Sha1);  
Готово, пароль зашифрован. В таком виде мы его и будем передавать на сервер (скажем, при помощи QTcpSocket) и хранить там, например, в базе данных:
 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");  
 //задаем нужные параметры, такие как имя файла или адрес сервера  
 QSqlQuery q(db);  
 q.prepare("INSERT INTO users (login, password) VALUES (:login, :password)");  
 //биндим логин и прочие необходимые данные пользователя  
 q.bindValue(":password", encryptedPwd); //биндим наш пароль  
 q.exec();  
Теперь в нашей базе хранится пароль в зашифрованном виде. О базах данных и о работе с сетью в Qt много и доходчиво написано в официальной документации, не буду заниматься дублированием.

Поговорим о вводе пароля и его хранении в настройках клиента (вряд ли кому-то приятно каждый раз при открытии приложения заново вводить пароль). Тут в целом всё довольно просто. Используем QSettings:
 QSettings s;  
 s.setValue("password", encryptedPassword);  
Вот и всё. Но есть один нюанс, о котором будет сказано дальше. Пока же перейдём к вводу.

Начнём, пожалуй, с ввода из консоли (или из терминала, кому как больше нравится). Казалось бы, делаем std::cin >> pwd и радуемся жизни, но не тут то было. Если вы хоть раз пользовались консольными приложениями, требующими ввода пароля, то скорее всего помните, что при его вводе символы, которые вы печатали, не отображались в окне. Сделано это, как нетрудно догадаться, для того, чтобы никто не подсмотрел пароль. Попробуем сделать так же? Попробуем:
 #include <cstdio>  
   
 #if defined(Q_OS_LINUX) || defined(Q_OS_MAC)  
 #include "termios.h"  
 #elif defined(Q_OS_WIN)  
 #include "windows.h"  
 #endif  
   
 void setStdinEchoEnabled(bool enabled)  
 {  
 #if defined(Q_OS_MAC) || defined(Q_OS_LINUX)  
   struct termios tty;  
   tcgetattr(STDIN_FILENO, &tty);  
   if(enabled)  
     tty.c_lflag |= ECHO;  
   else  
     tty.c_lflag &= ~ECHO;  
   tcsetattr(STDIN_FILENO, TCSANOW, &tty);  
 #elif defined(Q_OS_WIN)  
   HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);  
   DWORD mode;  
   GetConsoleMode(hStdin, &mode);  
   if(enabled)  
     mode |= ENABLE_ECHO_INPUT;  
   else  
     mode &= ~ENABLE_ECHO_INPUT;  
   SetConsoleMode(hStdin, mode);  
 #endif  
 }  
Выглядит достаточно монструозно, давайте немного разберёмся. Назначение функции довольно простое: она включает или отключает отображение вводимых в консоль символов. Директивы #define нужны потому, что на разных платформах это делается по-разному (и подключаемые заголовки тоже разные). Подробнее об использованных структурах и функциях можно почитать в интернете, а пока посмотрим, как это использовать на практике:
 std::cout << "Enter password: ";  
 setStdinEchoEnabled(false);  
 std::string pwd;  
 std::cin >> pwd;  
 setStdinEchoEnabled(true);  
 std::cout << "\n";  
Последняя строчка нужна для того, чтобы ввести "съеденный" при вводе пароля с отключённым "эхом" символ конца строки.

Чтобы не возиться с std::cin и std::cout, можно ввести следующие функции:
 void write(const QString &s)  
 {  
   static QTextStream out(stdout, QIODevice::WriteOnly);  
   out << s;  
   out.flush();  
 }  
   
 QString readLine()  
 {  
   static QTextStream in(stdin, QIODevice::ReadOnly);  
   return in.readLine();  
 }  
Подробнее о QTextStream и его использовании написано в документации. Теперь ввод пароля может выглядеть так:
 write("Enter password: ");  
 setStdinEchoEnabled(false);  
 QString pwd = readLine();  
 setStdinEchoEnabled(true);  
 write("\n");  
На мой взгляд, это чуточку проще и понятнее, а также избавляет от необходимости производить преобразования между типами стандартной библиотеки C++ и типами Qt.

А теперь перейдём к последнему и самому, как мне кажется, интересному — вводу пароля с использованием графического интерфейса. За основу возьмём QLineEdit. Создадим следующий класс:
 class PasswordWidget : public QWidget  
 {  
   Q_OBJECT  
 private:  
   QByteArray encPwd;  
   QLineEdit *ledt;  
 public:  
   explicit PasswordWidget(QWidget *parent = 0)  
   {  
     QHBoxLayout *hlt = new QHBoxLayout(this);  
     hlt->setContentsMargins(0, 0, 0, 0);  
     ledt = new QLineEdit;  
     ledt->setEchoMode(QLineEdit::Password);  
     connect(ledt, SIGNAL(textChanged(QString)), this, SIGNAL(passwordChanged()));  
     hlt->addWidget(ledt);  
   }  
 public:  
   void setPassword(const QString &pwd)  
   {  
     encPwd.clear();  
     ledt->setPlaceholderText("");  
     ledt->setText(pwd);  
   }  
   void setEncryptedPassword(const QByteArray &pwd, int charCountHint = 0)  
   {  
     encPwd = pwd;  
     ledt->clear();  
     ledt->setPlaceholderText(QString().fill('*', (charCountHint > 0) ? charCountHint : 0));  
   }  
   void clear()  
   {  
     encPwd.clear();  
     ledt->setPlaceholderText("");  
     ledt->clear();  
   }  
   void restoreState(const QByteArray &ba)  
   {  
     QDataStream in(ba);  
     bool enc = false;  
     in >> enc;  
     if (enc)  
     {  
       QByteArray pwd;  
       int cc = 0;  
       in >> pwd;  
       in >> cc;  
       setEncryptedPassword(pwd, cc);  
     }  
     else  
     {  
       QString pwd;  
       in >> pwd;  
       setPassword(pwd);  
     }  
   }  
   QString password() const  
   {  
     return ledt->text();  
   }  
   QByteArray encryptedPassword(QCryptographicHash::Algorithm a) const  
   {  
     if (!ledt->text().isEmpty())  
       return QCryptographicHash::hash(ledt->text().toUtf8(), a);  
     else if (!encPwd.isEmpty())  
       return encPwd;  
     else  
       return QByteArray();  
   }  
   int сharCountHint() const  
   {  
     return ledt->placeholderText().length();  
   }  
   QByteArray saveState() const  
   {  
     QByteArray ba;  
     QDataStream out(&ba, QIODevice::WriteOnly);  
     out << false;  
     out << password();  
     return ba;  
   }  
   QByteArray saveStateEncrypted(QCryptographicHash::Algorithm method) const  
   {  
     QByteArray ba;  
     QDataStream out(&ba, QIODevice::WriteOnly);  
     out << true;  
     out << encryptedPassword(a);  
     out << charCountHint();  
     return ba;  
   }  
 signals:  
   void passwordChanged();  
 };  
Посмотрим, что тут к чему. Во-первых, мы инкапсулировали QLineEdit внутри нашего класса, чтобы к нему не было прямого доступа (например, чтобы нельзя было менять текст). Далее, мы сделали так, чтобы вместо реальных символов всегда отображались звёздочки. Пароль при этом можно задавать не только в явном ("как есть"), но и в зашифрованном виде. Во втором случае можно также указать, из скольких символов состоит пароль, чтобы отображалось именно столько звёздочек (charCountHint). Сигнал passwordChanged() оповещает о том, что пароль был каким-либо образом изменён (программно, либо при вводе или удалении символов пользователем).

Теперь о главном — о хранении. Метод saveStateEncrypted() сериализует параметры пароля в массив байтов. Указывается тип пароля (зашифрованный или явный), а также собственно пароль и, в случае, если он зашифрован, подсказка о количестве символов. Десериализация происходит с точностью до наоборот. Посмотрим, как это применить на практике:
 //создание виджета  
 PasswordWidget *pwgt = new PasswordWidget;  
   
 //сохранение пароля при закрытии приложения  
 QSettings s;  
 s.setValue("password", pwgt->saveStateEncrypted(QCryptographicHash::Sha1));  
   
 //восстановление пароля при открытии приложения  
 QSettings s;  
 pwgt->restoreState(s.value("password").toByteArray());  
Вот, собственно, и всё. Для большинства приложений, где не требуются крайние меры по защите информации, приведённых способов работы с паролями должно хватить. Имеющиеся функции и класс PasswordWidget, конечно же, могут быть улучшены, дополнены разными возможностями, но это уже, как говорится, совсем другая история.

среда, 3 июля 2013 г.

Сериализация и легко расширяемые протоколы в Qt

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

Сразу скажу, что эта заметка в первую очередь ориентирована на сериализацию при передаче данных по сети и едва ли пригодится в приложениях, где ведётся работа с большими объёмами данных, когда каждый байт на счету и оптимизация превыше всего. Если же предполагается, что храниться или передаваться будут небольшие порции данных, то изложенное ниже может быть вам полезно. Впрочем, гениальных решений и "срыва покровов" ждать не стоит.

Итак, сериализация в Qt. Обычно для этой цели используется класс QDataStream. Документация, на мой взгляд, достаточно полно освещает все аспекты его применения, поэтому ограничусь кратким обзором. QDataStream сериализует объекты (тут и далее к объектам я буду причислять также данные таких типов как int, bool и т.д., хотя это и не совсем корректно) разных типов в поток данных, что понятно из его названия. При этом данные могут как добавляться к QByteArray, так и записываться сразу в QIODevice (например, QFile или QTcpSocket). Естественно, присутствует возможность десериализовать объекты обратно из потока данных.

Казалось бы, все замечательно, пишем данные в поток, забираем обратно, тишь да гладь, да божья благодать. Но вот нам потребовалось расширить протокол, добавив новый объект. И тут начинается хаос. Для наглядности предположим, что у нас есть следующий класс:
 class Book  
 {  
 public:  
   QString title;  
   QString text;  
 public:  
   friend QDataStream &operator <<(QDataStream &stream, Book book)  
   {  
     stream << book.title;  
     stream << book.text;  
     return stream;  
   }  
   friend QDataStream &operator >>(QDataStream &stream, Book &book)  
   {  
     stream >> book.title;  
     stream >> book.text;  
     return stream;  
   }  
 };  
Тут мы добавили дружественные операторы, которые позволяют нам делать следующее:
 //на стороне отправителя  
 QTcpSocket *s = new QTcpSocket;  
 //подключение и т.д.  
 QDataStream out(s);  
 for (int i = 0; i < 10; ++i)  
 {  
   Book b;  
   //задаем какие-то значения b.title и b.text  
   out << b; //сериализуем наш объект  
 }  
   
 //на стороне получателя  
 QDataStream in(s);  
 for (int i = 0; i < 10; ++i)  
 {  
   Book b;  
   out >> b; //десериализуем наш объект  
 }  
А теперь мы вдруг вспомнили, что у книги есть ещё и автор:
 class Book  
 {  
 public:  
   QString title;  
   QString text;  
   QString author; //новая переменная класса  
 };  
Добавили переменную, модифицируем оператор сериализации:
 friend QDataStream &operator <<(QDataStream &stream, Book book)  
 {  
   stream << book.title;  
   stream << book.text;  
   stream << book.author;  
   return stream;  
 }  
Ура, мы сломали совместимость! Вот просто так взяли, и сломали. Если получатель не обновил программу, то первый объект будет десериализован нормально, у второго же вместо названия окажется автор первого, а вместо текста - название первого. Чтобы убедиться воочию, попробуйте собрать и запустить пример.

Как же быть? Документация предлагает нам использовать версионирование. Не буду заниматься повторением, однако замечу, что такой подход рано или поздно приведёт к конструкциям вида:
 switch (version)  
 {  
 case 1:  
   //50 строк  
   break;  
 case 2:  
   //55 строк  
   break;  
 case 3:  
   //56 строк  
   break;  
 //ещё около 30 версий  
 }  
Думаю, не надо говорить о поддерживаемости подобного кода (проще застрелиться, чем заниматься этим).

Но что же тогда делать? Выход есть. Не самый оптимальный с точки зрения трафика/места на диске, но идеальный с точки зрения удобства. Воспользуемся замечательным классом QVariantMap (если всё же хочется сэкономить хотя бы на ключах, используем QMap<quint16, QVariant> или даже QMap<quint8, QVariant>). Волшебным образом, неподдерживаемый код превращается в...
 friend QDataStream &operator <<(QDataStream &stream, Book book)  
 {  
   QVariantMap m;  
   m.insert("title", book.title);  
   m.insert("text", book.text);  
   m.insert("author", book.author);  
   m.insert("publisher", book.publisher);  
   //сколько угодно добавляем или удаляем сериализуемые переменные  
   stream << m;  
   return stream;  
 }  
Да, вот в такой удобный, полностью совместимый код. Кто не знаком с шаблоном QMap, поясню: это так называемый словарь, где каждому ключу (идентификатору) соответствует некоторое значение. При попытке обратиться к несуществующему значению возвращается объект этого типа, для создания которого используется конструктор по умолчанию. В нашем случае используется QVariantMap, то есть в качестве типа ключей выступает QString, а в качестве типа значений - QVariant (который может содержать любой зарегистрированный тип данных).

Аналогично поступаем с оператором десериализации:
 friend QDataStream &operator >>(QDataStream &stream, Book &book)  
 {  
   QVariantMap m;  
   stream >> m;  
   book.title = m.value("title").toString();  
   book.text = m.value("text").toString();  
   book.author = m.value("author").toString();  
   book.year = m.value("year").toUInt(); //новая переменная  
   //сколько угодно добавляем или удаляем переменные  
   return stream;  
 }  
В данном случае book.year будет присвоено значение для unsigned integer по умолчанию (то есть 0). А имеющееся значение publisher просто будет проигнорировано. При этом все остальные книги также будут десериализованы корректно.

Кончено, такой протокол является несколько избыточным, ведь даже если получатель никак не использует часть данных, они всё равно ему передаются. Кроме того, размер передаваемых данных увеличивается из-за наличия ключей и из-за прослойки в виде QMap. Если требуется передавать много объектов с большим числом полей, каждое из которых имеет небольшой размер, то издержки приведённого подхода будут особенно заметны. Однако я ещё не встречал более простого в поддержке способа сохранить совместимость при независимой модификации сервера и клиента.