вторник, 27 ноября 2012 г.

Основы protobuf

Если вы еще используете JSON, то Google protobuf идет к вам!

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

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

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

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

На момент написания этой статьи, Google предоставлял технологию protobuf для языков C++, Java и Python. Кроме того, сторонними заинтересованными компаниями, группами и лицами предоставлена реализация этой технологии для пары десятков других известных языков и, даже, специально для сред разработки.

Чтобы начать использование технологии protobuf с помощью средств предоставляемых Google следует скачать необходимый установочный пакет (под вашу операционную систему) со страницы code.google.com/p/protobuf/downloads/list. Для своего Linux Ubuntu 11.10 я взял вот такой пакет с исходными кодами продукта - protobuf-2.4.1.tar.bz2. Это пакет собран в классическом GNU toolchain и для его сборки и установки надо в директории разархивированного пакета выполнить следующий набор команд в консоли.

$ ./configure
$ make
$ make check
$ sudo make install

Подробности сборки читайте в файле README.txt. Тем кто не знаком с такой системой сборки можно дать только один совет - как можно скорее познакомьтесь с ней. Обратите внимание на цель make check. По этой цели вызывается система автоматических тестов собранного пакета.

После установки пакета можно начинать эксперименты по его использованию. Здесь нам, прежде всего, помогут следующие ссылки на оригинальные ресурсы Goggle.

Получив начальное представление о технологии можно познакомиться с конкретной реализацией на простом примере.

Я начал с того, что доверился показательности примера метаописания адресной книги, приведенной в учебнике к C++, и, на его основе, написал простую программку на C++, которая исполняет сериализацию и десериализацию простейшей адресной книги.

package tutorial;

message Person {
    required string name = 1;
    required uint32 id = 2;
    optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
    repeated Person person = 1;
}

В общем, метаописание достаточно понятное. Подробности следует смотреть в руководстве к метаязыку описания данных. Сейчас дадим лишь несколько пояснений.

Объявление пакета (package) вводит в контексте C++ одноименное пространство имен, что предотвращает возможные коллизии данных.

Представленное метаописание содержит три объекта данных: адресная книга (AddressBook), единица записи в адресной книге (Person) и составная часть описания Person - объект телефонного номера (PhoneNumber). По каждому из этих объектов будет формироваться сообщение в пакете сериализации, поэтому для метаописания этих объектов используется ключевое слово message.

Каждый элемент данных сопровождается модификатором: required, optional и repeated.

  1. required - элемент обязательно присутствует в сообщении.
  2. optional - элемент опционален. Может не присутствовать в сообщении.
  3. repeated - элемент может повторяться в сообщении любое количество раз, включая ноль раз.

Таким образом, в нашем описании описана адресная книга (AddressBook), которая может состоять из любого количества элементов Person. Каждый элемент Person, состоит из четрех полей: name, id, email и phone. Поле email - опционально, а поле phone может включать любое число объектов сообщения типа PhoneNamber. Элемент PhoneNumber состоит из двух полей - номера телефона (number) и его типа (type), значение которого определяется специальным типом нумератором PhoneType. Тип номера - опционален.

В метаописании данных допускается использование нескольких примитивных типов, в том числе: bool, int32, uint32, float, double и string. Полный список примитивных типов можно найти в этом разделе руководства метаописания данных. В дополнение к этому, можно составлять типы нумераторы и организовывать иерархию сообщений используя их как типы.

Отдельно следует сказать о тегах, которые определяют уникальность элемента данных внутри записи. Номера тегов записываются в конце каждого элемента сообщения через знак "=". Номера этих тегов делятся на две группы. Группа 1-15 и группа >=16. Различие этих групп в особенностях двоичного кодирования данных, которое мне пока не совсем понятно.

Итак, у нас есть метаописание адресной книги. В нашем случае оно лежит в файле addressbook.proto. Создадим по нему систему классов в пространстве имен tutorial к языку C++. Для этого отдадим файл метаописания утилите protoc следующим образом.

$ protoc --cpp_out=. addressbook.proto

При успешном выполнении этой операции мы получим два файла: addressbook.pb.h и addressbook.pb.cc. Это заголовочный файл и файл реализации для классов составленных по метаописанию. Теперь надо включить эти классы в пространство нашего проекта и можно начинать их использовать в коде проекта.

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

#include "addressbook.pb.h"

int main(int /*argc*/, char */*argv*/[])
{
    // Создаем экземпляр класса адресной книги для сериализации
    tutorial::AddressBook src_book;
    {
        // Создаем и заполняем первую запись в адресной книге
        tutorial::Person * person = src_book.add_person();
        person->set_name("Alexey Knyazev");
        person->set_id(0);
        person->set_email("knzsoft@mail.ru");
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("+7 927-220-35-67");
            pn->set_type(tutorial::Person_PhoneType_MOBILE);
        }
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("+7 962-622-31-67");
            pn->set_type(tutorial::Person_PhoneType_MOBILE);
        }
    }
    {
        // Создаем и заполняем вторую запись в адресной книге
        tutorial::Person * person = src_book.add_person();
        person->set_name("Danilov Dmitry");
        person->set_id(1);
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("8 (8452) 43-96-86");
            pn->set_type(tutorial::Person_PhoneType_HOME);

        }
    }

    std::string msg;
    src_book.SerializeToString(&msg);

    tutorial::AddressBook dst_book;
    dst_book.ParseFromString(msg);

    dst_book.PrintDebugString();

    return 0;
}

Большую часть представленного примера программы составляет заполнение адресной книги. Из кода видно, что API для технологии protobuf достаточно простой и интуитивно понятный.

После заполнения книги мы сериализуем данные созданной адресной книги в строку msg. После чего создаем объект другой адресной книги и заполняем его через десериализацию сообщения msg. В завершении, пользуясь отладочными API, выводим десериализованную адресную книгу в стандартное устройство вывода.

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

person {
  name: "Alexey Knyazev"
  id: 0
  email: "knzsoft@mail.ru"
  phone {
    number: "+7 927-220-35-67"
    type: MOBILE
  }
  phone {
    number: "+7 962-622-31-67"
    type: MOBILE
  }
}
person {
  name: "Danilov Dmitry"
  id: 1
  phone {
    number: "8 (8452) 43-96-86"
    type: HOME
  }
}

Напомню, что во всем этом есть небольшая ложка дегтя - необходимость линковки с библиотекой обеспечивающей фундамент технологии protobuf, с библиотекой libprotobuf. Я делал пример в QtCreator с использованием системы сборки QMake и мой проектный файл выглядит следующим образом.

TARGET = app-1
CONFIG += console
CONFIG -= app_bundle

LIBS += -lprotobuf

TEMPLATE = app

SOURCES += main.cpp \
           addressbook.pb.cc

HEADERS += addressbook.pb.h

OTHER_FILES += \
    addressbook.proto

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

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

Наверное следует добавить, что предоставленные средства дают возможность не только выполнять сериализацию в строку, но и в поток, что позволяет сразу вывести данные в файл, сеть или куда-то еще, что поддерживает обычные C++ потоки типа std::ostream и std::istream.

воскресенье, 4 ноября 2012 г.

Кодирование по алгоритму MD5 при использовании OpenSSL

Пример использования OpenSSL для кодирования по алгоритму MD5

Занимаясь разработкой библиотеки для работы с протоколом SIP на языке C++ я столкнулся с проблемой кодирования в MD5. Собственно, проблему я создал сам, так как при получении нужного мне шифра я допустил хитрую ошибку в одном из преобразований, и при исполнении этого некорректного преобразования я получал правильное шифрование для одних эталонных данных и неправильное для других. Пытаясь найти ошибку я реализовал несколько вариантов разных решений, и, в конечном счете, понял досадный источник проблемы. Забавным было то, что ошибка была не в алгоритме шифрования, а в коде который превращал 128-битовую бинарную последовательность этого шифра в строку из 32-х символов. В общем, как обычно, все оказалось банальным, хотя и выглядело очень мистически.

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

Надо заметить, что в своих проектах я стараюсь избегать линковки на разные малоизвестные сторонние библиотки. Особенно это справедливо для случаев, если все, что из них берется не стоит тех проблем, с которыми могут столкнуться пользователи твоего кода собирая его под другими платформами. Именно поэтому, для этого проекта я подготовил свою реализацию MD5, но в процессе поиска проблемы я, как уже упоминалось, реализовал несколько совершенно разных решений.

Думаю, что для даного случая отлично подойдет пример с использованием библиотеки Open SSL. Библиотека, как мне показалось, кроссплатформенная, и те, кто работает под Windows, смогут найти соответствующую реализацию библиотеки. В академических целях я, как и прежде, рекомендую Linux. В Linux особенно удобно программировать, так как из репозитория любого дистрибутива, обычно, легко установить нужные языки и необходимые библиотеки. То, что нам понадобится в этом примере, скорее всего, уже есть у вас по умолчанию.

Сразу поясню, что формирование шифра для диалогов SIP требует шифрования досточно хитрых исходных последовательностей в некоторые из которых могут входить MD5-шифры других. Поэтому при поиске проблемы с которой я столкнулся и о которой упомянул выше, мне надо было понять лежит ли проблема в самом шифровании, в неправильной подготовке исходных последовательностей или в выборе вариантов кодирования предлагаемых разными RFC. Вообще, шифрование для SIP/HTTP требует отдельной статьи, и, возможно, я, как-нибудь, найду время на ее написание.

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

Итак, приведу пример кода, который я подготовил и проверил на Linux Ubuntu 11.10 (g++ v 4.6.1).

#include <iostream>

#include <openssl/md5.h>

//! \brief Преобразуем бинарный шифр в строку из hex-символов
std::string toHex(const char *pchData, int count)
{
    std::string s;
    for(int i=0; i<count; ++i) {
        unsigned char ch = pchData[i];
        unsigned char lo = ch%16;
        unsigned char hi = ch/16;

        s.push_back((hi<10)?(hi+0x30):(hi+87));
        s.push_back((lo<10)?(lo+0x30):(lo+87));
    }

    return s;
}

int main()
{
    // Объявим контекст шифрования
    MD5_CTX Md5Ctx;

    // Результат шифрования всегда составляет 128 бит (16 байт)
    const int hash_length = 16;
    char hash[hash_length];

    // Данные для исходной последовательности
    // Исходная последовательность должна содержать эти значения 
    // разделенные двоеточием
    std::string sUserName("alice");
    std::string sPassword("mielophone");

    // Вариант накопления последовательности в пространстве MD5
    std::string hash_string_v1;
    {
        // Передадим последовательность частями, а потом зашифруем
        MD5_Init(&Md5Ctx);
        MD5_Update(&Md5Ctx, (unsigned char *)sUserName.c_str(), sUserName.size());
        MD5_Update(&Md5Ctx, (unsigned char *)":", 1);
        MD5_Update(&Md5Ctx, (unsigned char *)sPassword.c_str(), sPassword.size());
        MD5_Final((unsigned char *)hash, &Md5Ctx);

        hash_string_v1 = toHex(hash, hash_length);
    }

    // Вариант внешнего накопления последовательности
    std::string input_string;
    std::string hash_string_v2;
    {
        // Накопим последовательность
        input_string.append(sUserName);
        input_string.append(":");
        input_string.append(sPassword);

        // Передадим ее в MD5 и зашифруем
        MD5_Init(&Md5Ctx);
        MD5_Update(&Md5Ctx, (unsigned char *)input_string.c_str(), input_string.size());
        MD5_Final((unsigned char *)hash, &Md5Ctx);

        hash_string_v2 = toHex(hash, hash_length);
    }

    // Выведем результаты шифрования одной и той же последовательности
    std::cout << "INPUT:   " << input_string << std::endl;
    std::cout << "HASH_v1: " << hash_string_v1 << std::endl;
    std::cout << "HASH_v2: " << hash_string_v2 << std::endl;

    return 0;
}

Для компиляции этого примера требуется подключение библиотек libcrypto.so и libexpat.so. Так как я готовил пример для системы сборки qmake, то для подключения этих библиотек мне понадобилось добавить следующую строку в файл проекта.

LIBS +=  -lcrypto -lexpat

После запуска получаем следующий вывод в стандартное устройство вывода.

INPUT:   alice:mielophone
HASH_v1: 98c322b45170287dcff3497ac2ab08ac
HASH_v2: 98c322b45170287dcff3497ac2ab08ac

Для проверки такого простого случая кодирования можно воспользоваться сервисом на странице http://www.pr-cy.ru/md5 или аналогичной (введите в строку поиска Google запрос типа "шифрование md5 онлайн"). Откройте указанную или найденную по запросу страницу, введите исходную последовательность alice:mielophone и запросите вычисление хеша.

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

суббота, 6 октября 2012 г.

Как сделать "живую" кнопку с картинкой в Qt

... или QWidget с использованием CSS и обработка его событий HoverEnter и HoverLeave в обработчике eventFilter()

Задача о "живой" кнопке с картинкой в Qt

Наверное многие периодически сталкиваются с задачей необходимости размещения кнопки с изображением, которое меняется при наведении на него курсора мыши (эффект "живой" кнопки). Видимо тут существует несколько вариантов решения, однако я хочу написать только об одном, в котором можно продемонстрировать сразу несколько редко используемых возможностей Qt, предлагаемых для построения интерфейса. Возможно от этой статьи будет кому-то польза, так как найти соответствующую информацию в одном месте, при решении этой простой задачи, мне не удалось. Кроме того, надеюсь, это будет шпаргалкой и для мне.

Итак, ниже будет продемонстрировано использование следующих техник Qt.

  1. Простейшее использование CSS для виджета Qt.
  2. Простейшее размещение и использование файла с картинкой в ресурсе Qt.
  3. Установка фильтра событий на объект класса QLabel и реализация фильтра событий.
  4. Обработка событий QEvent::HoverEnter и QEvent::HoverLeave.

Использование CSS в Qt

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

    QWidget m_pwgtTitle;
    QString m_sStyleOnExpanded;

    ...

    m_sStyleOnExpanded = "* "
            " { "
            " border-width: 1px; "
            " border-style: solid; "
            " border-top-left-radius: 10px; "
            " border-top-right-radius: 10px; "
            " border-color: white; "
            " padding: 5px; "
            " background: qlineargradient(x1:0.5, y1:0, x2:0.5, y2:1, "
            " stop:0 #33CCCC, stop: 0.4 #33FFFF, stop:1 #339999) "
            " }";

    m_pwgtTitle = new QWidget(this);
    m_pwgtTitle->setStyleSheet(m_sStyleOnExpanded);

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

Чтобы просто разместить на виджете картинку средствами CSS нужна значительно более простая запись стиля. Ниже показаны две строки стиля, которые будут использованы для нашего демо-виджета в обычном состоянии и в состоянии on-hover (под курсором мыши).

    m_sCallNormalStyle = "* { background-image: 
        url(:/images/images/green-phone-button.png); }";
    m_sCallOnHoverStyle = "* { background-image: 
        url(:/images/images/green-phone-button-on-hover.png); }";

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

Создание и использование ресурсов в Qt

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

Я создаю файл ресурсов средствами QtCreator. Так же, с помощью QtCreator я управляю этим файлом. Тех, кто хочет сделать это иначе можно обрадовать. Файл ресурсов это обычный текстовый файл с расширением *.qrc внутри которого содержится XML-описание ресурсов. Для того, чтобы система сборки Qt использовала обработку и линковку файла ресурсов, его надо включить в раздел RESOURCES проектного файла. Так, если вы имеете файл ресурсов с названием resourses.qrc, то он должен быть описан в файле проекта следующим образом.

RESOURCES += \
    resources.qrc

С помощью меню QtCreator, добавьте в проект файл ресурса, откройте его щелчком в панели файлов проекта, создайте там нужный префикс (раздел) и добавьте под этот префикс те файлы, которые вы хотите разместить в ресурсах. Файлы должны лежать в какой-нибудь поддиректории проекта, иначе они будут скопированы в корень проекта. Для размещения картинок я создаю поддиректорию images и файлы из этой директории размещаю под префиксом /images.

Для нашей демонстрации я добавлю в файл ресурсов два файла из директории images и мой результирующий XML-файл с описанием ресурса будет выглядеть следующим образом.

<RCC>
    <qresource prefix="/images">
        <file>images/green-phone-button.png</file>
        <file>images/green-phone-button-on-hover.png</file>
    </qresource>
</RCC>

Фильтры событий в Qt

Если объект не имеет в своем описании сигнала о некотором нужном нам событии, то это не повод для уныния. Прежде всего, можно унаследовать данный класс с целью переопределения виртуального callback-метода с нужным событием. Однако, кроме этого, Qt предоставляет более удобную возможность - установку в данный виджет фильтра события и в реализации этого фильтра поймать и обработать нужное нам событие.

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

  1. Для формы F реализовать виртуальный метод virtual bool eventFilter(QObject *, QEvent *), который изначально было описан для класса QObject.
  2. Передать объект формы F как объект с реализацией фильтра событий в объект виджета W. Это делается с помощью метода installEventFilter(). Теперь объект виджета W будет вызывать метод фильтра объекта F при возникновении в объекте W разных событий.
  3. В реализации eventFilter() необходимо отловить требуемое событие и обработать его.

Приведем простейший пример обработки клавиатурных событий в объекте строки ввода QLineEdit.

// Описание класса формы со строкой ввода
class MyForm: public QWidget
{
public:
  MyForm();
...

protected:
  virtual bool eventFilter(QObject *, QEvent *)

private:
  QLineEdit *m_pleIn;
...
};

...

// Реализация конструктора
MyForm::MyForm()
{
  // Создаем объект строки ввода
  m_pleIn = new QLineEdit(this);

  // Передаем объект формы делегатом для 
  // получения событий от объекта строки ввода
  m_pleIn->installEventFilter(this);
}

bool MyForm::eventFilter(QObject *obj, QEvent *event)
{
    if (obj != m_pleIn) return false;

    if (event->type() == QEvent::KeyPress) {
        // Обрабатываем событие нажатия на клавишу
        QKeyEvent *ke = static_cast(event);
        int key = ke->key();

        if (key == Qt::Key_Escape) {
            // Обрабатываем клавишу Escape
            ... 
            return true;
        }

        if (key == Qt::Key_Up) {
            // Обрабатываем клавишу стрелка вверх
            ... 
            return true;
        }

        if (key == Qt::Key_Down) {
            // Обрабатываем клавишу стрелка вниз
            ... 
            return true;
        }
    }
    return false;
}

Обработка событий QEvent::HoverEnter и QEvent::HoverLeave

Данные события являются событиями мыши, которые возникают при наведении мыши на виджет (QEvent::HoverEnter) и при уходе мыши с поверхности виджета (QEvent::HoverLeave).

Особенностью обработки событий QEvent::HoverEnter и QEvent::HoverLeave в Qt является то, что для их обработки необходимо, чтобы виджет, для которого мы хотим обработать данные события, имел установленным атрибут Qt:WA_HOVER. Только после установки этого атрибута можно рассчитывать на возможность обработки этих событий в методе фильтра eventFilter().

Для нашей будущей демо-кнопки установка этого атрибута будет выполнена так.

  m_pwgtCall->setAttribute(Qt::WA_Hover);

Решение задачи о "живой" кнопке

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

// заголовочный файл класса SipAgentPanel
class SipAgentPanel : public QWidget
{
    Q_OBJECT
public:
  SipAgentPanel(QWidget *parent = 0);

protected:
  bool eventFilter(QObject *, QEvent *);

private:
  QWidget *m_pwgtCall;

  QString m_sCallNormalStyle;
  QString m_sCallOnHoverStyle;
};

// файл реализации класса SipAgentPanel
SipAgentPanel::SipAgentPanel(QWidget *parent)
    : QWidget(parent)
{
    m_sCallNormalStyle = "* { background-image: 
        url(:/images/images/green-phone-button.png); }";
    m_sCallOnHoverStyle = "* { background-image: 
        url(:/images/images/green-phone-button-on-hover.png); }";

    m_pwgtCall = new QWidget(wgt);
   
    // Установим жестко размер виджета по размеру картинок 72x66
    m_pwgtCall->setFixedSize(72,66);

    m_pwgtCall->setStyleSheet(m_sCallNormalStyle);
    m_pwgtCall->setAttribute(Qt::WA_Hover);
    m_pwgtCall->installEventFilter(this);
}

bool SipAgentPanel::eventFilter(QObject * obj, QEvent * event)
{
    if (obj == m_pwgtCall) {
        QEvent::Type type = event->type();
        if  (type == QEvent::HoverLeave) {

            m_pwgtCall->setCursor(Qt::ArrowCursor);
            m_pwgtCall->setStyleSheet(m_sCallNormalStyle);

        } else if (type == QEvent::HoverEnter) {

            m_pwgtCall->setCursor(Qt::PointingHandCursor);
            m_pwgtCall->setStyleSheet(m_sCallOnHoverStyle);

        } else if (type == QEvent::MouseButtonPress) {

            QMouseEvent *mev = static_cast(event);
            if (mev) {
                if (mev->button() == Qt::LeftButton) {
                    // обработка события щелчка по объекту
                    ...
                }
            }

        }
    }

    return QWidget::eventFilter(obj, event);
}

Для повторения этого проекта не забудьте разместить в файле ресурса соответствующие картинки.

среда, 5 сентября 2012 г.

Формирование QString (Qt) из std::string содержащих национальные символы

Как сделать QString (Qt) из std::string с русскими буквами

Занимался я сегодня написанием некоторого демонстрационно-тестового приложения с использованием Qt. Приложение это должно выполнять роль GUI-обертки над объектами некой специализированной библиотеки, которую я написал в boost для некоторой прикладной области.

Работа выполняется в Linux. Дистрибутив Ubuntu 11.10. Локаль - UTF-8.

И вот возникла у меня проблема. Средствами boost::system::error_code, в ядре моей библиотеки формировалось некоторое локализованное сообщение (т.е. на русском языке), которое мне понадобилось отобразить средствами Qt в экземпляре класса QTextEdit.

Вообще, надо бы давно запретить использование национальных языков в системных сообщениях. Однако, видимо, это кому-то кажется недемократичным, поэтому программисты ежедневно тысячами подрываются на этих проблемах и мы постоянно в готовых приложениях сталкиваемся с кракозябрами там, где могли бы прочитать нормальный английский текст. Ведь в чем проблема? Домохозяйки не удосуживаются читать даже сообщения прикладного уровня, и представить домохозяйку, которая поймет, что надо делать прочитав системное сообщение из сетевой подсистемы ядра типа "Адрес уже используется" на родном языке, мне кажется невероятным. А раз это не для домохозяйки, то зачем усложнять жизнь специалистам, которые все поголовно умеют читать по английски? К этому еще можно добавить проблему некорректных переводов. Понятно, вопрос риторический. Однако, хочется высказаться.

На самом деле проблемы я не понял и был крайне удивлен, когда преобразовав полученное сообщение из std::string в QString и выведя его потом в окно редактора я увидел кракозябры. Действительно. В чем может быть проблема? Если бы я, не дай бог, работал под Windows, то удивляться бы было не чему. Давно не интересовался как там дела сейчас, но во времена Windows XP меня веселило три одновременно используемые кодовые страницы в русских версиях операционной системы. Вот уж действительно, не операционная система а система заплаток. Но откуда могла взяться проблема в Linux? Если в ней, в используемой мной сборке, используется одна кодовая страница UTF-8 во всех подсистемах ядра и пользовательского пространства.

Проблема решется просто. Приведу ниже максимально подробный вариант решения с комментариями.

  QTextCodec *codec = QTextCodec::codecForName("UTF8");
  if (codec)
  {
    std::string str = boost_lib->getLastError(); // Get string from a boost library
    QByteArray ba(str.c_str());                  // Convert to QByteArray
    QString msg = codec->toUnicode(ba);          // Qt magic !!! 

    m_pteLog->append(msg);                       // Append msg to a QTextEdit object
  }

В общем, заплатку я нашел, но в проблеме разбираться не стал. Видимо, по умолчанию, текст получаемый в Qt из строковых однобайтовых типов воспринимаются в кодировке Latin1, поэтому и возникала, в моем случае, неприятная неправильная перекодировка.

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

среда, 1 августа 2012 г.

Как я провалил собеседование по теме C/C++/Linux

Подробности того, как я провалил простое собеседование

Предисловие

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

Сегодня состоялось собеседование по скайпу с сотрудниками компании. Собеседование я, с большой долей вероятности, провалил. Прежде всего из-за элементарного недостатка знаний по некоторым деталям. Хотя, пару-тройку вопросов я элементарно не додумал. Так часто бывает в обстановке экзамена :)

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

Также, хочу добавить, что я крайне отрицательно отношусь к собеседованиям вообще. Во-первых, мало кто в состоянии вести себя на собеседованиях адекватно. Как не крути, а наличествует некоторый стресс. Даже в случае, если ты пришел на собеседование просто так, для "пощекотать нервы" (Думаю, что многие согласятся с тем, что надо всегда находится в поиске и не отказываться априори от того, что может сулить какие-то перспективы. Пусть и несбыточные.) Во-вторых, умение писать код и разбираться в вопросах на собеседовании не проверить. Для этого надо обязательно проводить тестовое задание.

Как бы там не было, думаю, что многим интересны будут вопросы, которые могут задать по профилю C/C++/Linux. Именно этим профилем занимаюсь я, и именно по этому профилю было проведено собеседование, отчёт о котором я хочу здесь представить.

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

Практические вопросы

Системный вызов fork(), стандартные потоки ввода/вывода

  1. Что вы можете сказать о следующем фрагменте кода? Есть ли в нем ошибки? Дайте максимальные комментарии о работе этого кода.
  2. Откомпилируется ли представленный фрагмент кода? Если откомпилируется, то что будет при запуске?
  3. Нужно ли изменить программу в строке с вызовом fprintf(), чтобы надпись "Hello" печаталась два раза? Если нужно, то как?
  4. Нужно ли изменить программу в строке с вызовом fprintf(), чтобы надпись "Hello" печаталась один раз? Если нужно, то как?
#include <stdio.h>

int main()
{
  fprintf(0, "Hello");
  fork();
  return 0;
}

Знаковые и беззнаковые типы разной ёмкости

Что будет лежать в A после исполнения следующего фрагмента кода?

int  A = 128;
char B = A;
  A = B;

Виртуальный деструктор

Что будет напечатано при исполнении следующего фрагмента кода? Максимально прокомментируйте этот код. Есть ли в нем принципиальные ошибки и, если есть, то как их исправить?

#include <iоstreаm>

using nаmespаce std;

class Fаther
{
    public:
 Fаther() {}
        ~Fаther() 
        {
         cоut << "~Father" << endl;
        } 
};
 
class Sоn : public Fаther
{
    public:
 Sоn() : Fаther() 
 { 
 }
 ~Sоn() 
        { 
         cоut << "~Son" << endl;
        }
};

int mаin()
{
    Fаther* оbject = new Sоn();
    delete оbject;
}

Игры с указателями

Что будет содержаться в массиве a после исполнения представленного ниже кода?

char a[]="111111111111";
*((int*)a+1) = 0;

Односвязный список

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

Теоретические вопросы

Теоретические вопросы я оставлю практически без комментариев. Они достаточно чёткие в своей постановке и если кто-то чего-то не знает, то может найти это в литературе или в сети Интернет без особых проблем.

  1. Что такое виртуальная память? Что в процессоре ответственно за реализацию виртуальной памяти?
  2. Что вы можете рассказать об архитектурах CISC и RISC? Основные особенности и различия.
  3. Как работают системные вызовы? Как переносятся параметры системного вызова?
  4. Расскажите о системном вызове fork(). Что передается в копию процесса?
  5. Что такое виртуальный конструктор?
  6. Сколько таблиц виртуальных методов может быть в объекте класса? От чего это зависит? Что можно сказать о размещении указателей на них?
  7. Можно ли в конструкторе вызывать виртуальные методы?
  8. Что вы можете рассказать о различиях между std::vector и std::list в плане их внутренней реализации?
  9. Как должен быть оформлен класс, чтобы его объекты могли храниться в std::vector?
  10. Чем отличается процесс от потока?
  11. Разделяют ли потоки стек?
  12. Что такое мьютекс? Что такое семафор? Можно ли семафор использовать для синхронизации процессов?
  13. Какие вы знаете средства взаимодействия процессов? Могут ли процессы иметь общую память?

Хочу дать единственный комментарий к вопросу о количестве таблиц виртуальных методов. Прежде чем отвечать наверняка вспомните и подумайте о множественном наследовании. Здесь вас могут легко запутать.

Комментарии к практическим вопросам

Системный вызов fork(), стандартные потоки ввода/вывода

#include <stdio.h>

int main()
{
  fprintf(0, "Hello");
  fork();
  return 0;
}

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

Если в качестве потока вывода указать поток stdout, т.е. написать fprintf(stdout, "Hello"), то надпись будет выведена два раза, так как поток stdout буферизируется и при создании копии процесса, также будет сделана копия буферов stdout. Поэтому, при завершении обоих процессов, буферы каждого будут сброшены и на терминале появятся две надписи "Hello".

Если в качестве потока вывода указать поток stderr, т.е. написать fprintf(stderr, "Hello"), то надпись будет выведена только один раз, так как поток stderr не буферизован и поступившие в него данные сразу будут выброшены на устройство вывода связанное с данным потоком. Функция fork() будет выполнена только после этого.

Знаковые и беззнаковые типы разной ёмкости

Что будет лежать в A после исполнения следующего фрагмента кода?

int  A = 128;
char B = A;
  A = B;

Тип char представляет собой знаковый однобайтовый тип, поэтому число 128, которое будет в него записано будет интерпретироваться как отрицательное число в дополнительном коде. Следовательно, при обратном присвоении мы получим число (-128).

Виртуальный деструктор

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

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

Игры с указателями

char a[]="111111111111";
*((int*)a+1) = 0;

Имя массива в C/C++ является адресом его начального элемента. Приведя этот адрес к указателю на целое число мы получаем указатель на следующие четыре байта целого (для вычислительных систем, где int занимает четыре байта). Инкремент этого указателя сдвигает адрес указателя на четыре байта (столько занимает начальный элемент массива из элементов типа int). Следовательно разыменуемый адрес указывает на четыре байта памяти, сдвинутые относительно исходного адреса на четыре байта. После обнуления этих четырех байт мы получим, что в массиве a будет лежать "1111\000\000\000\0001111". В этой записи, согласно стандарту Си, литерал '\000' представляет собой восьмиричную запись однобайтового значения.

Односвязный список

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

Некоторые из тех, кому я уже рассказал об этой задачке предполагают, что речь идёт о кольцевом односвязном списке. Однако слово "кольцевой" в вопросе, насколько я помню, не звучало. Как умолчание я бы посчитал это нелепицей. Да и если речь идёт о кольцевом односвязном списке, то это было бы слишком просто. Хотя, судя по тому, что все вопросы были достаточно простые, то, может быть, так оно и есть.

И еще один момент. Я специально спрашивал насчёт указателя на начало списка. Указатель не дали. И ещё раз уточнили. Дан односвязный список и указатель только на тот внутренний элемент списка, которые требуется корректно удалить. В общем, выглядит задачка как полный бред. Как заметил один мой коллега, Дубровин Алексей, за эту задачу компании должна быть назначена премия Тьюринга по теме "Как утечку памяти превратить в связанный список" :).

Спустя неделю. Прошла неделя с момента публикации этого сообщения и я подумал, что имеет смысл опубликовать размышление моих коллег по этой задаче. Сначала коллеги из "Мирантис", потом Михаил Сёмичев из "ЕПАМ" (все это наши местные отделения распределенных аутсортинговых компаний) и, наконец, Владимир Легкий из компании "Волга-софт" предположили, что возможно речь идет о следующем решении. Если не заниматься движениями указателей, а выполнить смещение данных относительно удаляемого элемента, то решение получается простое, но имеет одно ограничение. Оно не работает для случая, если указатель задачи указывает на последний элемент.

Поясню решение. Оно может быть в нескольких вариантах, но в одной сути.

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

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

четверг, 19 июля 2012 г.

C++. Методы класса. Интересный вариант вызова

Началась эта маленькая история с того, что один мой молодой коллега, Алексей Заровный, прислал мне на посмотреть вот такую ссылку - http://ideone.com/gyMg9. По этой ссылке размещён очень короткий, но весьма интересный пример на языке C++. Позволю себе привести ниже его копию.

#include <iostream>
class Foo
{
public:
  void foo()
  {
    std::cout << "hello world" << std::endl;
  }
};
 
int main()
{
  Foo& foo = *(Foo*)NULL;
  foo.foo();
}

Наверное, не каждый из читателей сразу поймёт, что несмотря на необычный способ вызова метода, код, тем не менее совершенно трезвый и рабочий, хотя, конечно, вряд-ли кто-нибудь напишет это в реальном проекте. Однако для демонстрации понимания основ ООП пример очень достойный.

Признаюсь сразу, что мне понадобилась подсказка. Мой мозг программиста с более чем двадцатилетним стажем, по самое не хочу загруженный различными стереотипами кода, отказался принять представленную конструкцию даже после того, как я убедился, что это компилируется и работает! Однако, мне на радость, есть у меня один добрый такой коллега, который относительно недавно пополнил армию программистов, оставив не менее достойные ряды армии электронщиков. Зовут этого молодого гения Антон Березин :) Именно он подлил в мои заржавленные мозги капельку масла :) И тогда сразу все стало на свои места! Еще бы! Сколько раз я объяснял ЭТО своим ученикам при объяснении принципов реализациии ООП :)

Для тех, кто еще не понял в чем дело я приведу упрощенный вариант тела функции main().

int main()
{
  Foo *foo = (Foo*)0;
  foo->foo();
}

Есть ли здесь фокус или все совершенно справедливо? Для тех, кто по прежнему не понимает как это может работать приведу пояснения.

Давайте вспомним основы внутренней организации ООП. Если у нас есть некий класс с полями и методами, то что из себя представляет объект этого класса? Для того, чтобы быть понятнее приведу более конкретный пример.

class A
{
  public:
    void m();

private:
  int d;
}

A *a = new A();
a->m();

Итак, что такое a с точки зрения внутренностей языка C++? Наверное, на этот вопрос все ответят правильно. С точки зрения внутреннего устройства, а, это указатель на некую область памяти, где лежат данные класса A. Обратите внимание на очень важную часть ответа - "данные класса".

Конечно же! Экземпляр класса это не более чем распределённые в памяти нестатические данные класса.

Методы класса лежат в памяти отдельно, и в единственном экземпляре для всех объектов класса. Чтобы различать экземпляры данных, каждый метод класса имеет неявный параметр this, через который, в реальности, передается указатель на тот экземпляр данных, к которому относится текущий вызов метода. Таким образом, запись

  a->m();

семантически соответствует некой условно-функциональной записи A::m(a), т.е. вызывается метод m() класса A::, которому передается экземпляр данных класса A по указателю a (простите мне синтаксические вольности :) ).

Таким образом, запись ((Foo *)0)->foo(), соответствующая приведённому примеру означает, что был вызван метод foo() класса Foo:: и в него был передан нулевой указатель на данные. Но, так как метод foo() в своей реализации не использует каких-либо данных класса Foo, то и проблем с исполнением этого метода никаких нет.

Вот такой вот интересные пример на понимание организации ООП в языке C++. Тут надо оговориться, что относительно данного контекста, т.е. хранения данных отдельно от методов, организация ООП одинакова для всех языков. Однако, не в каждом языке возможен такой фокус. Где-то мы встретимся с невозможностью синтаксического описания этого фокуса (например, Python), а где-то мы столкнемся с параноидальной моделью безопасности, которая просто не позволит нам сделать какие бы то ни было операции с нулевым объектом класса (например, Java).

В завершении добавлю пару вариаций на тему того, как может выглядеть функция main() в более изощрённом варианте обсуждаемого примера, предложенных моим коллегой по цеху программистов, Игорем Богомоловым.

int main()
{
  Foo& foo = *(Foo*)NULL;
  void(Foo::*f)(void) = &Foo::foo;
  (foo.*f)();
}
int main()
{
  Foo *foo = (Foo*)0;
  void(Foo::*f)(void) = &Foo::foo;
  (foo->*f)();
}

пятница, 13 апреля 2012 г.

История одного синглтона

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

Моё отношение к паттернам проектирования можно описать так.

Вещь, приносящая пользу исключительно на уровне коммуникаций с коллегами. Разговор "на одном языке" значительно упрощает общение и плодотворно влияет на сроки исполнения проектов. Проще употребить слово "синглтон" (простите, я против употребления в русском языке слова "синглетон"), чем три минуты пояснять особенности создания объектов такого класса и не быть полностью уверенным, что тебя поняли правильно.

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

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

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

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

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

У меня тоже растет такая коллекция решений. И в текущей коллекции есть все более совершенствующаяся система журналирования, которую я цепляю к разным своим проектам.

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

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

Ход мыслей, который привел к такому решению был, примерно, следующий.

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

2. Одно из решений состоит в проверке некоторого глобального указателя, который должен быть всегда объявлен, но может либо иметь значение ноль (при не созданном объекте журнала), либо указывать на объект журнала.

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

4. Теперь, желательно придумать ход, по которому создание экземпляра журнала автоматически инициализировало бы данный указатель. Можно сделать это в конструкторе, инициализируя глобальный указатель по значению this. Однако это может привести к тому, что, при случайном создании второго объекта журнала, размер неприятностей будет зависеть от схем использования журнала и, хочется подумать над тем, как избежать такой случайной ошибки. Следовательно надо запретить конструктор и создавать объект журнала в отдельной функции (или, что более правильно, в статическом методе) с проверкой на существование экземпляра.

4.1. Надо заметить, что, вообще, повторное использование кода тема особенная и даже делая для себя надо делать "для дураков" при всем уважении к себе любимому.

Вот, собственно, и весь синглтон, без претензии на знание соответствующего паттерна.

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


вторник, 10 апреля 2012 г.

Пример заготовки CMakeList.txt для простого проекта


Введение в CMake


Утилита cmake является кроссплатформенной утилитой с открытым исходным кодом (open source), используемой для создания традиционных сценариев сборки проектов для следующих платформ.


  1. Unix/Linux -> Makefile (для утилиты GNU make)
  2. Windows -> VS Projects/Workspaces
  3. Apple -> Xcode

Процесс сборки проекта, при использовании cmake, состоит из следующих этапов.


  1. Создание файла конфигурации CMakeLists.txt в котором через систему команд, свойств и переменных описываются исходные файлы проекта, цель сборки и все сопутствующие сборке детали.
  2. Выполнение утилиты cmake. При это будет произведена обработка файла конфигурации CMakeLists.txt в процессе которой будет сгенерирован файл традиционного для данной операционной системы сценария сборки. Например, Makefile.
  3. Выполнение традиционных сценариев сборки. Например, утилитой make.

Используя специальные генераторы можно привязать систему CMake к определенным системам сборки, таким как Qt, Borland C++ Builder, MS Visual C++ и пр. Это свойство системы, само по себе интересно, но привязка кроссплатформенной сборки к какому-то конкретному продукту, выглядит, в общем случае, неразумно. Не считая, разумеется, вариант с поддержкой Qt SDK. Здесь особый разговор, так как сборка остается кроссплатформенной, но составляет альтернативу собственной системе сборки для Qt, qmake, которая не лишена некоторых недостатков.


Я вспоминаю о CMake всегда, когда не делаю проект в Qt. Вот почему.


  1. Проект не привязывается к какой-то среде разработке и его можно писать хоть в vim. Для небольших проектов это исключительно удобно. Для тех, кто относится к vim с предубеждением путь представят себе вместо vim любой другой редактор плоского кода с подсветкой синтаксиса, который им кажется удобным. Сейчас таких много под любой платформой.
  2. Проект удобно передавать в виде исходников для сборки у заказчика или какого-то иного лица потребляющего этот код. Удобно в контексте его простой компиляции. Не придется ставить среду разработки. Потребуется только наличие CMake, а это уже может быть много проще. Особенно с этим нет проблем в Linux.
  3. Сборка проекта по Makefile, который был сгенерирован системой CMake достаточно зрелищная. Во-первых, выполняется в цвете, а, во-вторых, с отображением процента исполнения.

Из написанного выше не следует, что CMake удобен только для небольших проектов. Просто большие проекты это дело особого обсуждения. Часто тут решение принимает не один человек и, в некоторых случаях, это решение определяется самой постановкой задачи. Зачем, например, городить огород, если задача заказана в MS Visual Studio и будет использована только там. В случае, если CMake допустим для проекта, то это позволит получить удобства описанные в списке выше.


Изучение системы CMake особая тема по которой через Google можно найти много статей и специальной документации. Цель данной статьи - дать простую заготовку конфигурационного файла системы CMake. Создавая в консоли новый проект надо будет просто скопировать эту заготовку и внести в нее изменения характерные для вашего проекта.


CMakeLists.txt для простого проекта


Итак, мы начали простой проект. Создали каталог my-project и создали в нем подкаталог src для размещения источников кода нашего проекта. Напишем заготовку в файл src/main.cpp и теперь настроим CMake для сборки по этому проекту. Потом мы будем добавлять в сборку новые файлы и, возможно, библиотеки. Отразим все эти возможности в нашей заготовке.


Конфигурационный файл для системы CMake должен называться CMakeLists.txt. Расположим его в каталоге my-project и получим следующую систему файлов.


my-project                - каталог проекта
my-project/src            - каталог источников проекта  
my-project/src/main.cpp   - исходный файл (пока один; только *.cpp файла)
my-project/CMakeLists.txt  - конфигурационный файл системы CMake 

Представим шаблон файла CMakeLists.txt для простого проекта.




cmake_minimum_required ( VERSION 2.6 )
# Укажите здесь имя вашего проекта.
# Учитываете установку переменных 
# <project_name>_BINARY_DIR и  <project_name>_SOURCE_DIR
# в значение имени каталога с проектом.
project ( project_name )

set ( SRC_DIR ${PROJECT_SOURCE_DIR}/src )

# Укажите вместо my-project имя своего приложения
# - имя исполняемого файла
set ( APP_NAME my-project )

# Управление типом сборки через задание значения
# предопределенной переменной CMAKE_BUILD_TYPE
# Возможные значения:
# DEBUG|RELEASE|RELWITHDEBINFO|MINSIZEREL
set( CMAKE_BUILD_TYPE RELEASE )

# Снять комментарий для добавления пути на 
# заголовочные файлы, которые следует добавить 
# для компиляции проекта
#include_directories ( /path/to/headers_1 )
#include_directories ( /path/to/headers_2 )
# ...
#include_directories ( /path/to/headers_N )

# Снять комментарий для добавления пути на 
# библиотеки, которые следует добавить 
# для линковки проекта
#link_directories ( /path/to/lib_1 )
#link_directories ( /path/to/lib_2 )
# ...
#link_directories ( /path/to/lib_N )

list(APPEND SRC ${SRC_DIR}/main.cpp )
# снять комментарий для добавления других *.cpp файлов
# list(APPEND SRC ${SRC_DIR}/file_1.cpp )
# list(APPEND SRC ${SRC_DIR}/file_2.cpp )
# ...
# list(APPEND SRC ${SRC_DIR}/file_N.cpp )

add_executable ( ${APP_NAME} ${SRC} )

# Снять комментарий для добавления имени библиотеки
# требуемой для линковки проекта
#target_link_libraries( ${APP_NAME} name_1 )
#target_link_libraries( ${APP_NAME} name_2 )
# ...
#target_link_libraries( ${APP_NAME} name_N )



Рассмотрим значение представленных в файле команд.


cmake_minimum_required - выставляет требование по минимальной версии CMake, требуемой для обработки даного файла конфигурации.


project(<project_name>) - задает имя проекта. Необходимо учитывать, что дополнительной функцией данной команды является установка переменных <project_name>_BINARY_DIR и <project_name>_SOURCE_DIR в значение имени каталога проекта.


set - задает значение переменной для дальнейшего использования. Здесь создаем переменную SRC_DIR по значению подставленному из предопределенной переменной PROJECT_SOURCE_DIR и подстроки "/src". Чтобы получить значение переменной, её идентификатор надо поместить в фигурные скобки и предварить знаком доллара - ${...}.


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


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


list с первым аргументом APPEND выполняет добавление к списку, указанному вторым аргументом, элемента заданного третьим аргументом. В нашем случае, к списку под именем SRC добавлено имя файла main.cpp с путем, указанным значением переменной SRC_DIR.


add_executable - для сборки исполняемого файла представленного первым аргументом добавляет компиляционные листы, которые будут образованы по списку файлов источников, заданных вторым аргументом - значением списка SRC.


target_link_libraries - задает библиотеки для линковки. Исполняемый файл указывается первым аргументом. Например, если в Linux требуется указать для линковки приложения my-project библиотеки с именем файла libm.so, то следует написать target_link_libraries(my-project m), что соответствует общим правилам задания имени библиотек в опциях для линковки. В одной команде можно указать несколько библиотек, разделив их пробелом.


Представленный вариант файла CMakeList.txt можно взять как шаблон для своих проектов. Чтобы собрать такой проект, надо в каталоге с файлом CMakeLists.txt выполнить команду "cmake ." (не забудьте про точку, обозначающую синоним для имени текущего каталога - в нем будет осуществлён поиск файла CMakeLists.txt). По этой команде будет создан Makefile, который потом следует обработать утилитой make. Для этого надо просто выполнить команду "make".


Полезные ссылки


  1. http://www.cmake.org - официальный сайт системы CMake
  2. http://symmetrica.net/cmake/ - интересные подробности по CMake. В том числе использование для проектов wxWidget и Qt.
  3. При работе в Linux, документацию по установленной системе CMake можно посмотреть в каталоге /usr/share/doc/cmake-data.

Мои статьи по CMake.


  1. Boost и CMake для Windows
  2. Boost и CMake для Windows (подробности)

вторник, 20 марта 2012 г.

Схема управления файлом в редакторе (New - Open - Save - Save as)

Вступление


При создании как простых так и сложных редакторов любого типа файлов, как правило, возникает необходимость реализации схемы New - Open - Save - Save as. Каждый раз, реализуя эту схему с нуля я обратил внимание на то, что код не всегда получается одинаково красивым. Тут нет претензии на абсолютный вариант красоты, просто, в очередной раз, написав относительно удачную схему, я решил увековечить её в своём блоге. Как минимум - для личного использования. Не исключено, что кому-нибудь из читателей такая схема покажется удачной. Если же нет - то будет, что обсуждать и совершенствовать.


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


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


Действующие лица

Для начала, опубликуем список действующих лиц схемы.




bool canCurrentScriptJobFinish()


Метод, проверяющий возможность нормального (неаварийного) завершения текущего сеанса редактирования файла.


bool save()


Метод, собственно, выполняющий сохранение файла.


bool saveAs()


Метод, исполняющий диалог выбора имени файла для сохранения. Сохранение выполняется через метод save().


bool openFile(const QString &sFileName)


Метод, выполняющий загрузку файла по указанному имени.


void setModified(bool)


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


void setFileName(const QString &sFileName)


Метод, сохраняющий имя сохранённого файла и влияющий на разного рода сопутствующие виджеты в пользовательском интерфейсе.


void clearFileName()


Метод, выполняющий очистку поля с именем файла для сохранения содержимого окна редактора.


void slotNewClicked()


Метод, обработки отклика на событие запроса на создание нового файла.


void slotOpenClicked()


Метод, обработки отклика на событие запроса на открытие существующего файла.


void slotSaveClicked()


Метод, обработки отклика на событие запроса на сохранение редактируемого файла.


void slotSaveAsClicked()


Метод, обработки отклика на событие запроса на сохранение редактируемого файла под другим именем.


void slotScriptChanged()


Метод, обработки отклика на событие изменения файла.

Реализация


Суть схемы заключена в том, что сохранение файла выполняется только в одном методе save(). В нем выполняется сохранение файла при известном имени. Если имя не известно, то из save() вызывается saveAs(). Так же, saveAs() вызывается если требуется сменить имя файла. Сам метод saveAs() не сохраняет файл, а лишь запрашивает имя для его сохранения и сохраняет это имя внутри класса, вызывая потом метод save(), который и выполняет сохранения файла при определённом имени для сохранения. Таким образом, может образоваться рекурсия из вызовов save()->saveAs()->save()... или saveAs()->save()->saveAs()....


Другим "фокусом" схемы является централизация запросов о необходимости сохранения текущего измененного файла в специальном методе canCurrentScriptJobFinish(). Все запросы к интерфейсу по смене текущего файла зависят от исполнения данного метода. Это относится и к интерфейсному запросу на создание нового файла и на открытие существующего файла.



bool canCurrentScriptJobFinish()


В этом методе необходимо ответить на вопрос о том, можно ли закончить работу с текущим файлом (скриптом).


Если файл не был изменен, то мы отвечаем на этот вопрос положительно, возвращаем true.


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


bool ScriptForm::canCurrentScriptJobFinish()
{
    while (m_bIsModified) {
        QMessageBox msgBox;
        msgBox.setText("The document has been modified.");
        msgBox.setInformativeText("Do you want to save your changes?");
        msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | MessageBox::Cancel);
        msgBox.setDefaultButton(QMessageBox::Save);
        int ret = msgBox.exec();
        switch (ret) {
        case QMessageBox::Save:
            save();
            break;
        case QMessageBox::Discard:
            setModified(false);
            break;
        case QMessageBox::Cancel:
            return false;
        default:
            return false;
        }
    }

    return true;
}

bool save()


Выполняем сохранение файла. Если имя файла не определено, то сохраняем через saveAs(). Сохранение файла выполняем по простой последовательности.


  1. Получаем скрипт целиком в виде строковой переменной.
  2. Открываем файл.
  3. Связываем файл с текстовым потоком.
  4. Отправляем скрипт в поток.
  5. Закрываем файл файл.

Как только файл был сохранен, необходимо сбросить флаг наличия несохранённых изменений в файле.


bool ScriptForm::save()
{
    if (m_sFileName.isEmpty()) return saveAs();

    // Скрипт целиком в строковую переменную script
    QString script = m_pceScript->toPlainText().trimmed();
    if (script.isEmpty()) {
        QMessageBox::information(this, tr("Save Script"), tr("Qt Script is empty"));
        return false;
    }

    QFile file(m_sFileName);

    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        return false;
    }

    // Собственно, выполняем сохранение строковой переменной в файл
    QTextStream out(&file);
    out << script;
    file.close();

    // Сбросим флаг изменений в файле
    setModified(false);
    return true;
}

bool saveAs()

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

  1. Получаем имя файла в диалоге с пользователем. Если пользователь отменил диалог, то завершаем метод возвратив false
  2. Сохраняем имя файла через специальный метод (свойство).
  3. Так как теперь имя файла для сохранения определено, то можно воспользоваться для сохранения уже готовым методом save().
bool ScriptForm::saveAs()
{
    QString sFileName = QFileDialog::getSaveFileName(this, tr("Save Qt Script"), ".");
    if (sFileName.isEmpty()) return false;

    setFileName(sFileName);
    return save();
}

bool openFile()

Назначение данного метода в открытии файла, заданного формальным параметром метода. Для этого:

  1. Открываем файл.
  2. Связываем файл с текстовым потоком.
  3. Получаем все содержимое файла из текстового потока в строковую переменную.
  4. Устанавливаем значение строковой переменной как содержимое виджета (элемента управления) редактора.

После того, как файл открыт на редактирование, необходимо, сохранить его имя как имя для сохранения изменений и сбросить флаг текущих изменений в файле.

bool ScriptForm::openFile(const QString &sFileName)
{
    QFile file(sFileName);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        g_pLog->addError(tr("Can't open file: %1").arg(sFileName));
        return false;
    }

    m_pceScript->clear();
    QTextStream in(&file);
    QString s = in.readAll();
    m_pceScript->setPlainText(s);
    file.close();

    setFileName(sFileName);
    setModified(false);

    return true;
}

void setModified()

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

void ScriptForm::setModified(bool flag)
{
    if (m_bIsModified != flag) {
        m_bIsModified = flag;
        if (m_bIsModified) {
          // делаем что-то, чем надо обозначить изменение файла
        } else {
          // делаем что-то, чем надо обозначить закрытие изменений в файле
        }
    }
}

void setFileName(const QString &sFileName)

Процедура обслуживания свойства fileName на запись. В зависимости от приложения может выполнять разные фокусы. В примере, данная процедура изменяет виджет с именем загруженного файла.

void ScriptForm::setFileName(const QString &sFileName)
{
    m_sFileName = sFileName;
    m_pleFileName->setText(tr("File name: %1").arg(m_sFileName));
}

void clearFileName()

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

void ScriptForm::clearFileName()
{
    m_sFileName.clear();
    m_pleFileName->setText(tr("File name:"));
}

void slotNewClicked()

Обработка события пользовательского интерфейса на создание нового файла. Проверяет возможность завершения работы с текущим файлом и, если такое разрешение есть, то выполняет очистку поля виджета (элемента управления) редактора, сброс имени файла для сохранения файла и сброс флага изменений.

void ScriptForm::slotNewClicked()
{
    if ( ! canCurrentScriptJobFinish()) return;

    m_pceScript->clear();
    clearFileName();
    setModified(false);
}

void slotOpenClicked()

Обработка события пользовательского интерфейса на открытие файла. Проверяет возможность завершения работы с текущим файлом и, если такое разрешение есть, то выполняет загрузку нового файла.

void ScriptForm::slotOpenClicked()
{
    if ( ! canCurrentScriptJobFinish()) return;

    QString sFileName = QFileDialog::getOpenFileName(this, tr("Open Qt Script"), ".");
    if (sFileName.isEmpty()) return;

    openFile(sFileName);
}

void slotSaveClicked()

Обработка события пользовательского интерфейса на сохранение файла. Просто вызывает процедуру save(), в которой может быть запущена последовательность вызовов saveAs()->save()....

void ScriptForm::slotSaveClicked()
{
    save();
}

void slotSaveAsClicked()

Обработка события пользовательского интерфейса на сохранение файла под именем, которое должно быть выбрано в диалоге с пользователем. Просто вызывает процедуру saveAs(), в которой может быть запущена последовательность вызовов save()->saveAs()....

void ScriptForm::slotSaveAsClicked()
{
    saveAs();
}

void slotScriptChanged()

Обработка события пользовательского интерфейса по изменению в редактируемом файле.

void ScriptForm::slotScriptChanged()
{
    setModified(true);
}

среда, 8 февраля 2012 г.

Конструкторы Си++ и загадки std::vector



Случилось мне недавно объяснять особенности применения конструкторов Си++. Ситуация рядовая - если вы пришли из языков оперирующих ссылками на объекты (как то Java, например), то конструктор копирования для вас в новинку.

Сначала мы написали наглядную программу, которая демонстрирует работу разного рода конструкторов и оператора присваивания. Далее, используя эту программу как инструмент для дальнейших исследований, мы коснулись использования класса стандартного вектора, std::vector.

Результаты получились интересными наглядными и поучительными. Поэтому я решил изложить их в своем блоге. Написанное далее может оказаться интересным не только новичкам, но и может быть использовано как материал для собеседования по языку Си++, так как требует не только понимания использования конструкторов, но и особенностей работы контейнеров типа std::vector.

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

#include <iostream>
#include <vector>

using namespace std;

class A
{
public:
    A();           // конструктор по умолчанию
    A(int x);      // конструктор с параметром
    A(const A &a); // конструктор копирования
    ~A();          // деструктор

    void operator = (const A &a);  // оператор присваивания

    int x() { return m_x; }
    void set_x(int x) { m_x = x; }

    int id() { return m_id; }

private:
    int m_x;

    static int counter;
    int m_id;
};

int A::counter = 0;

A::A(): m_x(0), m_id(++counter) {
    cout << "  Default constructor A. x = " << m_x 
         << ", id = " << m_id << endl;
}

A::A(int x): m_x(x), m_id(++counter) {
    cout << "  Param constructor A. x = " << m_x 
         << ", id = " << m_id << endl;
}

// В конструкторе копирования умышленно изменяем x
A::A(const A &a): m_x(a.m_x + 3), m_id(++counter) {
    cout << "  Copy constructor A. x = " << m_x 
         << ", id = " << m_id << endl;
}

A::~A() {
    cout << "  Destructor A. x = " << m_x 
         << ", id = " << m_id << endl;
}

// В операторе присваивания умышленно изменяем x
void A::operator = (const A &a) {
    m_x = a.m_x + 1;
    // id не копируем. Сохраним оригинальный  
    cout << "  Class A. Operator =. dest x = " << m_x 
         << ", src x = " << a.m_x << endl; 
} 

int main (int argc, char *argv[])
{
    cout << "Constructors test" << endl;

    cout << "\nMark 1" << endl;
    cout << "Log for 'A a1;'" << endl;
    A a1;
    cout << "Result: a1.x = " << a1.x() 
         << ", a1.id = " << a1.id() << endl;
 
    cout << "\nMark 2" << endl;
    cout << "Log for 'A a2(5);'" << endl;
    A a2(5);
    cout << "Result: a2.x = " << a2.x() 
         << ", a2.id = " << a2.id() << endl;

    cout << "\nMark 3" << endl;
    cout << "Log for 'A a3 = a2;'" << endl;
    A a3 = a2;
    cout << "Result: a3.x = " << a3.x() 
         << ", a3.id = " << a3.id() << endl;

    cout << "\nMark 4" << endl;
    cout << "Log for 'A a4; a4 = a3;'" << endl;
    A a4;
    a4 = a3;
    cout << "Result: a4.x = " << a4.x() 
         << ", a4.id = " << a4.id() << endl;

    cout << "\nMark 5. 'vector<A> vA;'" << endl;
    vector<A> vA;
 
    cout << "\nLog for 'vA.push_back(a1);'" << endl;
    vA.push_back(a1);    

    cout << "\nLog for 'vA.push_back(a2);'" << endl;
    vA.push_back(a2);    

    cout << "\nLog for 'vA.push_back(a3);'" << endl;
    vA.push_back(a3);    

    cout << "\nLog for 'vA.push_back(a4);'" << endl;
    vA.push_back(a4);    

    cout << "\nLog for 'vA.push_back(A(100));'" << endl;
    vA.push_back(A(100)); 
    {
        vector<A>::iterator it;
        for(it = vA.begin(); it != vA.end(); ++it) {
            cout << "Item vA: x = " << (*it).x() 
                 << ", id = " << (*it).id() << endl; 
        }
    }

    cout << "\nMark 6. 'vector vpA;'" << endl;
    vector<A*> vpA;

    cout << "\nLog for 'vpA.push_back(&a1);'" << endl;
    vpA.push_back(&a1);        

    cout << "\nLog for 'vpA.push_back(&a2);'" << endl;
    vpA.push_back(&a2);        

    cout << "\nLog for 'vpA.push_back(&a3);'" << endl;
    vpA.push_back(&a3);        

    cout << "\nLog for 'vpA.push_back(&a4);'" << endl;
    vpA.push_back(&a4);        

    cout << "\nLog for 'vpA.push_back(new A(200));'" << endl;
    vpA.push_back( new A(200)); 
    {
        vector<A*>::iterator it;
        for(it = vpA.begin(); it != vpA.end(); ++it) {
            cout << "Item vpA: x = " << (*it)->x() 
                 << ", id = " << (*it)->id() << endl;
        }
    }
 
    cout << "\nMark 7. EOP\n" << endl;

    return 0;
}

Чтобы код проще читался, попробуйте скопировать его в редактор, который умеет делать подстветку для кода на языке Си++.

Итак, что есть в коде.

Использованы все три вида конструкторов языка Си++ (конструктор по умолчанию, параметрический конструктор и конструктор копирования). Так же использован оператор присваивания без которого нельзя продемонстрировать присваивание объектов. Для новичков следует пояснить, что инициализация отличается от присваивания в Си++ и выглядит следующим образом.

A a3 = a2; // инициализация (используется конструктор копирования)
A a4;      // создание объекта класса через конструктор по умолчанию
a4 = a3;   // копирование (используется оператор копирования)

Теперь обратимся к результатам работы этой программы на моем компьютере. Я проверял все это на ноутбуке с установленным Linux Ubuntu 11.04.

Итак, по первому блоку.

Mark 1
Log for 'A a1;'
  Default constructor A. x = 0, id = 1
Result: a1.x = 0, a1.id = 1

Все предсказуемо. Вызывается конструктор по умолчанию.

Далее, блок два.

Mark 2
Log for 'A a2(5);'
  Param constructor A. x = 5, id = 2
Result: a2.x = 5, a2.id = 2

Тоже все как в учебнике. Вызывается параметрический конструктор.

Блок три

Mark 3
Log for 'A a3 = a2;'
  Copy constructor A. x = 8, id = 3
Result: a3.x = 8, a3.id = 3

Здесь может быть ловушка для новичков. Это не присвоение, а инициализация. Как результат - вызов конструктора копирования.

Блок четыре

Mark 4
Log for 'A a4; a4 = a3;'
  Default constructor A. x = 0, id = 4
  Class A. Operator =. dest x = 9, src x = 8
Result: a4.x = 9, a4.id = 4

Тоже все логично. Создаем объект конструктором по умолчанию, а потом выполняем присваивание через перегруженный оператор присваивания объектов данного типа.

А вот теперь блок пять и первые сюрпризы.

Mark 5. 'vector<A> vA;'

Log for 'vA.push_back(a1);'
  Copy constructor A. x = 3, id = 5

Log for 'vA.push_back(a2);'
  Copy constructor A. x = 8, id = 6
  Copy constructor A. x = 6, id = 7
  Destructor A. x = 3, id = 5

Log for 'vA.push_back(a3);'
  Copy constructor A. x = 11, id = 8
  Copy constructor A. x = 9, id = 9
  Copy constructor A. x = 11, id = 10
  Destructor A. x = 6, id = 7
  Destructor A. x = 8, id = 6

Log for 'vA.push_back(a4);'
  Copy constructor A. x = 12, id = 11

Log for 'vA.push_back(A(100));'
  Param constructor A. x = 100, id = 12
  Copy constructor A. x = 103, id = 13
  Copy constructor A. x = 12, id = 14
  Copy constructor A. x = 14, id = 15
  Copy constructor A. x = 14, id = 16
  Copy constructor A. x = 15, id = 17
  Destructor A. x = 9, id = 9
  Destructor A. x = 11, id = 10
  Destructor A. x = 11, id = 8
  Destructor A. x = 12, id = 11
  Destructor A. x = 100, id = 12
Item vA: x = 12, id = 14
Item vA: x = 14, id = 15
Item vA: x = 14, id = 16
Item vA: x = 15, id = 17
Item vA: x = 103, id = 13

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

Объяснение элементарное. Дело в работе механизма распределения памяти в векторе std::vector. Точнее, в его конкретной реализации. А работает он, в данном примере по следующей простой схеме. Каждый раз он удваивает размер контейнера для хранения объектов начиная с распределения памяти под один элемент. Т.е. размер контейнера для размещения вектора сначала был нулевым при создании, потом при помещении первого элемента стал равным единицы, потом увеличился до двух, потом до четырех и, закончился рост контейнера нашего вектора на восьми при добавлении пятого элемента.

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

Теперь блок шесть и опять сюрпризы.

Mark 6. 'vector<A*> vpA;'

Log for 'vpA.push_back(&a1);'

Log for 'vpA.push_back(&a2);'

Log for 'vpA.push_back(&a3);'

Log for 'vpA.push_back(&a4);'

Log for 'vpA.push_back(new A(200));'
  Param constructor A. x = 200, id = 18
Item vpA: x = 0, id = 1
Item vpA: x = 5, id = 2
Item vpA: x = 8, id = 3
Item vpA: x = 9, id = 4
Item vpA: x = 200, id = 18

Организовав в векторе хранение указателей на объекты вместо самих объектов мы изменили количество вызываемых конструкторов и деструкторов при заполнении вектора. Почему? Вектор стал по другому работать?

Конечно же все просто. Ловушка для неискушенных :) Вектор по прежнему работает в схеме распределения памяти { 0, 1, 2, 4, 8 }, однако теперь, при копировании элементов из старого контейнера в новый, копируются не сами объекты, вызовом соответствующих конструкторов копирования и уничтожением скопированных объектов, а копируются указатели на эти объекты. Разумеется, копирование указателей не связано с такими затратами.

Просто имейте это в виду при написании собственных программ. Собственно в этом и состоит цель этой заметки :)

В программе есть еще один вывод. Блок семь. Автоматическое уничтожение объектов при завершении программы. Видим деструкторы всех оставшихся в памяти объектов.

Mark 7. EOP

  Destructor A. x = 12, id = 14
  Destructor A. x = 14, id = 15
  Destructor A. x = 14, id = 16
  Destructor A. x = 15, id = 17
  Destructor A. x = 103, id = 13
  Destructor A. x = 9, id = 4
  Destructor A. x = 8, id = 3
  Destructor A. x = 5, id = 2
  Destructor A. x = 0, id = 1

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

Если для блока пять мы заменим std::vector на std::list (стандартный контейнер работающий по схеме связанного списка) то мы получим следующий вариант вывода по блоку пять.

Mark 5. 'list<A> vA;'

Log for 'vA.push_back(a1);'
  Copy constructor A. x = 3, id = 5

Log for 'vA.push_back(a2);'
  Copy constructor A. x = 8, id = 6

Log for 'vA.push_back(a3);'
  Copy constructor A. x = 11, id = 7

Log for 'vA.push_back(a4);'
  Copy constructor A. x = 12, id = 8

Log for 'vA.push_back(A(100));'
  Param constructor A. x = 100, id = 9
  Copy constructor A. x = 103, id = 10
  Destructor A. x = 100, id = 9
Item vA: x = 3, id = 5
Item vA: x = 8, id = 6
Item vA: x = 11, id = 7
Item vA: x = 12, id = 8
Item vA: x = 103, id = 10

Обратите внимание, что изменить надо и строку описания объекта vA, и строку описания итератора по vA.

Если же все-таки использовать контейнер std::vector, то избавиться от троекратного (для нашего случая) перераспределения памяти можно с помощью метода reserve(). Так как нам нужно зарезервировать место под пять объектов, то можно сразу после определения объекта vA написать следующее.

vector<A> vA;
vA.reserve(5);

Не путайте метод reserve() c методом resize()!!! Интереса ради можете посмотреть, что получится и попробовать объяснить результат.

пятница, 27 января 2012 г.

Размещение виджета в центре QScrollArea

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

Задача такая. Необходимо разместить виджет в скроллингуемой области QScrollArea. При этом, возможно два случая. В первом случае, размер размещаемого виджета больше размеров QScrollArea. Это нормальный случай использования скроллингуемых областей. Здесь нужно просто настроить работу скроллеров согласно документации на QScroolArea. Во втором случае, размер размещаемого виджета меньше размеров QScrollArea. В этом, не таком уж редком случае использования QScrooArea, хочется видеть размещаемый виджет в центре скроллингуемой области. И вот на эту простую задачу я и потратил неправомерно много времени, о чем и решил сейчас рассказать.

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

Решение выглядит следующим образом.

QScrollArea *area = new QScrollArea(parent_widget);

    MyWidget myWidget = new MyWidget(area);

    // ключ к решению - менеджер размещения с необходимой настройкой по выравниванию
    QVBoxLayout *vb = new QVBoxLayout(area->viewport());
    vb->addWidget(myWidget);
    vb->setAlignment(myWidget, Qt::AlignCenter);

Таким образом, как видно из представленного кода, типичное для Qt решение по внутреннему размещению виджетов лежит в правильном использовании менеджеров размещения.