Личный опыт разработки ПО

Сборник рецептов

Рубрика C++

Автоматическое добавление версии билда в код с помощью CMake

26 комментариев »

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

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

 

Все вместе это и формирует полное именование версии (Major.Minor.Pathch.Build):

4.6.12.589

Некоторые используют в качестве build уникальное числовое значение, которое увеличивается каждый раз, например, при ночной сборке. Я считаю, что никакого смысла в этом нет – гораздо удобнее привязать данный номер к ревизии в репозитории. Я использую Subversion и CMake, поэтому продемонстрирую как можно автоматизировать проставлении версии билда с этими инструментами.

Читать заметку полностью »

23rd Июнь 2012
15:31

Рубрика: C++,CMake,Инструменты,Сборка

Метки: ,

Многопоточность, баги и списки инициализации

19 комментариев »

Разработка многопоточных программ – задача нетривиальная. Локализовать место ошибки сложно, отладка затруднена, иногда даже возникает желание все свалить на ошибки в сторонних библиотеках и/или компилятор/операционную систему. Так делать не надо. Конечно вероятность такая есть, но она несравненно меньше того, что виновником ошибки являетесь именно вы.

Например приложение работает хорошо, ну скажем в 95 случаях из 100, но в оставшихся пяти оно может зависнуть или вылететь с segmentation fault. А может упасть.

Посмотрим на следующий код:

#include <condition_variable>
#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
 
class Worker
{
public:
    Worker()
        : shutdown_(false)
        , thread_(&Worker::taskProcessor, this)
    {
    }
 
    ~Worker()
    {
        {
            std::lock_guard<decltype(mutex_)> lock(mutex_);
            shutdown_ = true;
        }
 
        hasTask_.notify_one();
 
        thread_.join();
    }
 
    void addTask(int taskId)
    {
        {
            std::lock_guard<decltype(mutex_)> lock(mutex_);
            std::cout << "Adding task " << taskId << std::endl;
            tasks_.push(taskId);
        }
 
        hasTask_.notify_one();
    }
 
private:
    void taskProcessor()
    {
        while (true)
        {
            int task = 0;
 
            {
                std::unique_lock<decltype(mutex_)> lock(mutex_);
 
                if (shutdown_)
                    return;
 
                while (tasks_.empty())
                {
                    hasTask_.wait(lock);
 
                    if (shutdown_)
                        return;
                }
 
                task = tasks_.front();
                tasks_.pop();
            }
 
            // освободили мютекс, чтобы не блокировать другие потоки и делаем работу
            std::cout << "Processing task " << task << std::endl;
        }
    }
 
private:
    bool shutdown_;
 
    std::queue<int> tasks_;
 
    std::thread thread_;
    std::mutex mutex_;
    std::condition_variable hasTask_;
};
 
int main(int argc, char* argv[])
{
    Worker worker;
 
    for (int i = 1; i < 6; ++i)
    {
        worker.addTask(i);
    }
 
    return 0;
}

Здесь приведен простой класс, объект которого обрабатывает некоторые задачи в параллельном потоке. Из основного потока функцией addTask можно добавить задачу в очередь и известить об этом параллельный поток. Поток проснувшись, возьмет из очереди задачу, обработает ее и если задач больше нет – уснет. В деструкторе, прежде чем объект будет уничтожен происходит ожидание обработки всех задач в очереди.

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

Проблема в списке инициализации. Как мы помним, конструирование членов класса происходит в порядке их объявления. В нашем случае сначала будет создан объект типа std::thread и только после него std::mutex. Если звезды сложатся удачно, то запуск параллельного потока займет некоторое время, за которое в основном потоке будет сконструирован мютекс и программа будет работать. Если со звездами не сложиться, то поток начав работу попробует заблокировать мютекс который еще не сконструирован, со всеми вытекающими. Решение простое, но не самое лучшее, так как делает код хрупким – изменить положение членов в списке инициализации:

boost::mutex Mutex_;
std::condition_variable hasTask_;
std::thread thread_;

Лучше будет разделить конструирование и инициализацию:

class Worker
{
public:
	static std::unique_ptr<Worker> create()
	{
		std::unique_ptr<Worker> worker(new Worker());
		worker->init();
		return worker;
	}
 
	...
 
private:
	Worker()
		: shutdown_(false)
	{
	}
 
	void init()
	{
		thread_ = std::thread(&Worker::taskProcessor, this));
	}
 
	...
 
};

15th Ноябрь 2011
21:38

Рубрика: C++

Метки: , ,

C++ 0x == C++ 0xB

4 комментария »

Принят финальный драфт нового стандарта C++.  http://herbsutter.com/2011/03/25/we-have-fdis-trip-report-march-2011-c-standards-meeting/

Ура, товарищи!

Update. Собственно сам драфт: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf

27th Март 2011
14:20

Рубрика: C++,Новости

Метки:

Конечные автоматы в C++

11 комментариев »

В статье рассмотрены простые конечные автоматы и их реализация на C++ с помощью switch-конструкций, таблиц времени исполнения и библиотеки Boost Statechart.

Введение

Грубо говоря, конечный автомат (Finite State Machine), глазами пользователя – это черный ящик, в который можно что-то передать и что-то оттуда получить. Это очень удобная абстракция, которая позволяет скрыть сложный алгоритм, кроме того конечные автоматы очень эффективны.

Конечные автоматы изображают в виде диаграмм состоящих из состояний и переходов. Поясню на простом примере:

Как вы наверное догадались – это диаграмма состояний лампочки. Начальное состояние обозначается черным кружком, переходы стрелками, некоторые стрелки подписаны – это события после которых автомат переходит в другое состояние. Итак, сразу из начального состояния, мы попадаем в состояние Light Off – лампа не горит. Если нажать кнопку, то автомат изменит свое состояние и перейдет по стрелке помеченной Push Button, в состояние Light On – лампа горит. Перейти из этого состояния можно опять же по стрелке, после нажатия кнопки, в состояние Light Off.

Также широко используются таблицы переходов:

Текущее состояние Событие Состояние после перехода Действие
Light Off Push Button Light On Загорается лампа
Light On Push Button Light Off Лампа гаснет

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

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

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

Читать заметку полностью »

13th Февраль 2011
23:05

Рубрика: C++,Алгоритмы

Метки: ,

Использование Cppcheck для статического анализа кода

2 комментария »

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

Цена вопроса

Есть программы которые стоят немалых денег (Coverity Prevent for C/C++, Klocwork), а есть абсолютно бесплатные (Cppcheck).

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

Checking check.cpp... 
[check.cpp:11]: (error) Array 'c[10]' index 10 out of bounds 
[check.cpp:5]: (error) Memory leak: __p 
[check.cpp:17]: (error) Memory leak: a 
[check.cpp:14]: (error) Mismatching allocation and deallocation: A::__p 
[check.cpp:8]: (error) Null pointer dereference 

 

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

Есть версия программы с графическим интерфейсом, где единственное усилие которое нужно сделать – выбрать пункт меню Check->Files для проверки одного файла, или Check->Directory для проверки всей директории, после чего просмотреть результаты работы.

Также есть консольная версия, на вход которой также достаточно подать  имена проверяемых файлов.

Интеграция в систему сборки

Уже упомянутую консольную версию программы легко добавить в скрипт сборки проекта, чтобы автоматически проводить проверку. При этом Cppcheck может в случае нахождения ошибок вернуть определенный код. Используйте для этого ключ —error-exitcode=код_возврата.

Вывод

Cppcheck отличный инструмент, очень простой в использовании, бесплатный и довольно эффективный. Крайне рекомендую к использованию. Есть версии как для Windows, так и для Linux.

Домашняя страница проекта: http://cppcheck.sourceforge.net/

27th Январь 2011
21:29

Рубрика: C++,Инструменты

Метки: ,

Use of undefined type

5 комментариев »

Давайте поговорим о неприятных граблях связанных с удалением неполного типа, что может привести к крайне неприятным последствиям, например милому сердцу Segmentation fault. Связано это с двумя вещами:

  1. Полномочиями данными компилятору автоматически генерировать деструктор, если он не определен в классе
  2. Возможностью вызвать оператор delete для объекта тип которого в точке удаления еще не известен

Рассмотрим простой код и посмотрим, что делает компилятор:

test.h

#ifndef TEST_H
#define TEST_H
 
class Test
{
public:
	Test();
	~Test();
};
 
#endif//TEST_H

test.cpp

#include <iostream>
 
#include "test.h"
 
Test::Test()
{
	std::cout << "Test" << std::endl;
}
 
Test::~Test()
{
	std::cout << "~Test" << std::endl;
}

trouble.h

#ifndef TROUBLE_H
#define TROUBLE_H
 
#include <memory>
 
class Test;
 
class Trouble
{
public:
	Trouble();
 
private:
	std::auto_ptr<Test> Test_;
};
 
#endif//TROUBLE_H

trouble.cpp

#include "test.h"
 
#include "trouble.h"
 
Trouble::Trouble()
	: Test_(new Test())
{
}

main.cpp

#include "trouble.h"
 
int main(int argc, char* argv[]) 
{
	Trouble trouble;
	return 0;
}

Итак, имеется два класса: Test и Trouble, причем в целях ускорения компиляции разработчик решил сделать предварительное объявление класса Test в trouble.h, но не написал деструктор для класса Trouble, а значит компилятор заботливо создаст деструктор сам. Когда он это сделает? Вообще компилятор не мечется по коду, а последовательно его анализирует.

Начнет он с функции main в main.cpp, определит, что создается объект trouble типа Trouble из trouble.h, обнаружит конструктор.

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

Что будет в этом деструкторе? Естественно удаление членов класса, то есть в данном случае std::auto_ptr<Test>, в деструкторе которого будет соответственно вызван оператор delete для указателя на Test.

Внимательно следим за руками! В данном месте компилятор еще ничего не знает о типе Test, поскольку еще не дошел до test.h и поэтому оператор delete будет применен к неполному типу, что вызовет неопределенное поведение.

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

Далее подробности и методы борьбы с данным явлением.

Читать заметку полностью »

31st Март 2010
21:47

Рубрика: C++

Метки:

О копировании объектов в C++

10 комментариев »

Я регулярно сталкиваюсь с ошибками связанными с невнимательностью или незнанием механизма копирования объектов в C++. Поэтому первое правило:

Не копируйте!

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

class Port
{
public:
	Port();
	virtual ~Port();
 
private:
	Port(const Port&);
	Port& operator=(const Port&);
};

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

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

#include <boost/noncopyable.hpp>
 
class Port
	: private boost::noncopyable
{
public:
	Port();
	virtual ~Port();
};

На этом можно остановиться, но если же вы решили, что класс должен поддерживать копирование, то читайте дальше.

Читать заметку полностью »

23rd Март 2010
0:18

Рубрика: C++

Метки:

Архивирование с библиотеками zlib и bzip2/libbzip2 используя Boost Iostreams

Прокомментировать »

Недавно у меня возникла задача добавить сжатие данных в программу. Сразу нашлось две свободных библиотеки – zlib и libbzip2 написанные на C. Изучив вопрос глубже, оказалось что писать удобные обертки над функциями на C не нужно, так как в Boost IOStreams все уже написано.

О том как добавить данный функционал и правильно собрать проект я расскажу в данной заметке. Также будет приведен пример кода для сжатия и распаковки файлов и сравнение zlib (алгоритм DEFLATE, методы gzip и zlib) и libbzip2 (алгоритм bzip2) по скорости работы и уровню сжатия тестовых, бинарных и исполняемых файлов.

Читать заметку полностью »

7th Март 2010
22:59

Рубрика: C++,Разработка

Метки:

Чтение настроек приложения

8 комментариев »

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

Недавно (с версии 1.41.0) в Boost появилась библиотека Property Tree, предназначенная для решения данной задачи. Помимо поддержки XML, также поддерживаются форматы INI, JSON и свой формат INFO.

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

Читать заметку полностью »

22nd Февраль 2010
17:23

Boost Optional

Прокомментировать »

В C# при объявлении переменной можно приставить знак вопроса, после чего в дальнейшем проверять была ли переменная инициализирована или нет:

int? a;
...
if (a.HasValue)
{
	...
}

Иногда это бывает очень полезно, например при работе с базами данных.

В C++ к сожалению такого удобства нет, но как известно – программисты C++ отличаются верностью и если язык не предоставляет какую либо возможность, они ее добавляют сами. Например в Boost данный функционал присутствует:

#include <boost/optional.hpp>
...
boost::optional<int> a;
std::cout << (a.is_initialized() ? "Есть значение" : "Нет значения") << std::endl;
boost::optional<int> b(3);
std::cout << (b ? "Есть значение" : "Нет значения") << std::endl;
a = 5;
std::cout << (a ? "Есть значение" : "Нет значения") << std::endl;
int result = a.get() + b.get();
std::cout << result << std::endl;
b.reset();
std::cout << (b ? "Есть значение" : "Нет значения") << std::endl;

Вывод:

Нет значения
Есть значение
Есть значение
8
Нет значения

20th Февраль 2010
22:45

Рубрика: C++

Метки: ,