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

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

Записи с меткой ‘C++’

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

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

Очень удобно всегда знать какой конкретно версии та или иная сборка проекта. Для этого обычно вводят номер версии, состоящий из нескольких цифр. Я являюсь сторонником структуры состоящей из 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++,Новости

Метки:

Использование 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++

Метки:

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

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++

Метки: ,

Освобождение памяти занятой контейнером

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

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

#include <iostream>
#include <vector>
 
int main(int argc, char* argv[])
{
	std::vector<int> data;
	data.resize(200000);
	std::cout << data.capacity() << std::endl;
	data.clear();
	std::cout << data.capacity() << std::endl;
	return 0;
}

Будет выведено:

200000
200000

Почему не возвращает? Потому, что есть метод resize и reserve, которые резервируют память и поэтому освободить память было бы крайне некорректно по отношению к ним.

Что же делать? Не паниковать, а после удаления содержимого использовать следующую идиому:

std::vector<int>().swap(data);

Также может быть полезным освободить незанятую память, например после удаления доброй половины большого контейнера:

std::vector<int>(data).swap(data);

24th Январь 2010
21:55

Рубрика: C++

Метки:

Boost Test, юнит-тестирование и CMake

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

Boost Test

Написанием модульных тестов можно не только повысить скорость разработки за счет экономии времени на отладке, но и повысить качество. Также написание тестов позволяет критично взглянуть на интерфейсы классов и функций, что выливается в создание простых и логичных интерфейсов. Но разработка с применением тестов может не принести ощутимых плодов из-за сложности написания тестов, что выльется в слабое покрытие кода тестами. Поэтому инструмент для тестирования должен быть максимально простым, написание тестов должно происходить с приложением минимального количества усилий. Я пользовался фреймворком для написания тестов UnitTest++ – это очень хороший и удобный инструмент и если вы не используете Boost, я бы порекомендовал обратить на него пристальное внимание. Но в данной заметке речь пойдет не о нем, а о фреймворке Boost Test.

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

24th Январь 2010
19:09