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

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

Рубрика C++

Use of undefined type

3 коммент. »

Давайте поговорим о неприятных граблях связанных с удалением неполного типа, что может привести к крайне неприятным последствиям, например милому сердцу 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 вызван не будет!

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

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

31 марта 2010
21:47

Рубрика: C++

Метки:

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

2 коммент. »

Я регулярно сталкиваюсь с ошибками связанными с невнимательностью или незнанием механизма копирования объектов в 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();
};

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

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

23 марта 2010
0:18

Рубрика: C++

Метки:

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

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

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

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

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

7 марта 2010
22:59

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

Метки:

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

4 коммент. »

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

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

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

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

22 февраля 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
Нет значения

20 февраля 2010
22:45

Рубрика: C++

Метки: ,

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

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

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

#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

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

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

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

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

На мой взгляд поведение крайне не интуитивное, я например в свое время об этом узнал из книги Герба Саттера и Андрея Александреску "Стандарты программирования на C++".

24 января 2010
21:55

Рубрика: C++

Метки: