30 апреля 2006

x in "C++0x"

В Dr.Djobb's Journal шутят по поводу даты выхода следующего стандарта C++:
The next revision of the C++ Standard, tentatively titled C++0x (the 'x' is a placeholder, intended to suggest that the revision will be finished before 2010, although some people like to remind us that "0x" introduces a hexadecimal constant, so C++0x really has to be finished by 2015), ...

28 апреля 2006

#define is evil

Как часто при написании Windows приложений приходится употреблять следующую строку:
#include <windows.h>

А теперь попробуем следующий код:
// Loader.h
class Loader
{
public:
void LoadString();
}

// Loader.cpp
#include "Loader.h"
#include

void Loader::LoadString()
{
// code here
}

Попробуем откомпилировать:
error C2039: 'LoadStringA' : is not a member of 'Loader'

Вот вам и windows.h со своими #define...
Подобные вещи мешают использовать даже стандартную библиотеку - макросы min и max не дают воспользоваться шаблонами std::min и std::max.

Лучшее лекарство от подобного безобразия:
// SafeWindows.h
#pragma once

#include <windows.h>

#ifdef min
#undef min
#endif

#ifdef max
#undef max
#endif

#ifdef MessageBox
#undef MessageBox
#endif
...

// Any.cpp
#include "SafeWindows.h"

int main()
{
return ::MessageBoxW(NULL, L"#define is evil", L"Note", MB_OK);
}

20 апреля 2006

Передача динамически созданных объектов в DLL

Замечательное свойство shared_ptr заключается в том, что он хранит в себе ссылку на deleter - функцию, которую shared_ptr вызывает для удаления объекта.

То есть если мы создадим shared_ptr в EXE и передадим его в DLL, которая потом будет удалять объект из динамической памяти, то она вызовет operator delete (или другой заданный deleter), который находится в EXE! Это спасет нас от головной боли с несколькими копиями CRT (одна в EXE, другая в DLL), которая была бы если бы мы передали простой указатель, а не shared_ptr.

Правда возникает вопрос: не возникнет ли у shared_ptr проблем с подсчетом ссылок при пересечении границы EXE-DLL?

Clever shared_ptr

Столкнулся с разработкой классов, подобных следующему:
class SomeClass
{
public:
SomeClass(T* Pointer, ...)
: _Pointer(Pointer) { }

private:
T* _Pointer;
}

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

Если мы динамически создали такой объект специально для передачи его в такой конструктор, то неплохо бы, чтобы деструктор этого класса автоматически удалил его из динамической памяти:
class SomeClass
{
public:
SomeClass(T* Pointer, ...)
: _Pointer(Pointer) { }

private:
auto_ptr<T> _Pointer;
}

SomeClass SomeInstance(new T(), ...);

Однако такой вариант предполагает, что мы обязательно будем создавать объекты типа T динамически, и экземпляр класса SomeClass будет владеть ими. Что не позволит:
1. передавать ссылки на объекты, которыми владеет кто-либо другой:
T Array[3];
SomeClass SomeInstance(&Array[2], ...);

2. использовать конструктор копирования и оператор присваивания, генерируемый компилятором по умолчанию (т.к. при копировании auto_ptr "донор" теряет ссылку).

Второй пункт можно обойти заменой auto_ptr на shared_ptr. Тем же самым методом можно обойти и первый пункт, если добавить немного кода:
struct NullDeleter
{
void operator()(const void *p) { /* ничего не делаем */ }
};

class SomeClass
{
public:
SomeClass(shared_ptr<T> Pointer, ...)
: _Pointer(Pointer) { }

private:
shared_ptr<T> _Pointer;
}

// передаем владение
SomeClass SomeInstance(new T(), ...);

// владение не передаем
T Array[3];
SomeClass SomeInstance(shared_ptr<T>(&Array[2], NullDeleter(), ...);

Плюс такого решения еще в том, что можно помимо двух крайностей "владеет"/"не владеет", наш класс получает третью возможность - "совладеет":
vector<shared_ptr<T> > v;
v.insert(new T());
...
SomeClass SomeInstance(v[0], ...);
...
v.clear();

Минус этого решение в том, что shared_ptr имеет много оверхеда (как по использованию памяти, так по процессорному времени) по сравнению с написанием специализированного класса, поддерживающего только два состояния: "владеет"/"не владеет".

17 апреля 2006

Текстовые файлы в 21 веке

Стандартная библиотека С++ имеет хороший инструментарий для работы с текстовыми файлами. Однако, этот инструментарий немного устарел.

Вот пример кода, написанного с использованием стандартной библиотеки:
void process_file(const char *filename)
{
std::ifstream f(filename);
std::string s;
while (!f.eof())
{
std::getline(f, s);
process_line(s);
}
f.close();
}

На первый взгляд все замечательно, но только на первый. В реальности и в имени файла, и в самом тексте могут встретиться символы, не входящие в стандартный ASCII алфавит.

А проблема в приведенном фрагменте кода в двух местах:
1. const char *filename
2. std::string s;

С первым разобраться достаточно просто:
void process_file(const wchar_t *filename)
{
std::ifstream f(_wfopen(filename, L"rb"));
...
}

Казалось бы – и второе достаточно просто решается, заменой ifstream на wifstream. Однако, не все так просто как кажется. А загвоздка вот в чем – текстовые файлы в unicode могут быть в различных кодировках: UTF-8, UTF-16, ... Да и в принципе могут встретиться «старые добрые» файлы в ANSI кодировке.

Как и следовало полагать, «велосипед» по чтению unicode-файлов уже давно изобрели. Вообще, я ожидал его увидеть в boost-е, но его там не оказалось. Я нашел его... в contributes к boost-у. Причем эта библиотека датирована 2003 годом, но так и не попала в boost!

Библиотека отлично работает, сама разпознает кодировку по BOM. Самое интересное – в ней сразу нашлась небольшая ошибка:
facet_type* facet;
switch(is.get())
{
case ...:
case ...:
}
if(!facet)
{
...
}
return facet;

Присвоение переменной facet значения идет только в случаях, если выполняется один из case-ов, в любых других случаях она остается неинициализированной, о чем мне и сообщил VC 7.1. Вылечилось просто:
facet_type* facet = NULL;

Использование библиотеки очень простое:
std::wifstream input_file(file_name, std::ios_base::binary);
// autodetect the UTF encoding of this file
boost::utf::imbue_detect_from_bom(input_file);

// some reading
std::wstring s;
input_file >> s;

Только возникают небольшая (легко устранимая) проблема с интерпретацией конца строки в файле, открытом в режиме binary (CR LF уже не становится автоматически LF).

Вызывает сомнения то, что библиотека с 2003 года не обновлялась и так и не включилась в boost, да еще эта ошибка... Поиск в конференциях показал, что используют эту библиотеку очень редко. Значит используют что-то другое? Скорее всего тяжеловесную ICU.

10 апреля 2006

AlphabeticalLess

А теперь, если кому интересно, вариант предиката less для регистронезависимого сравнения строк - для использования в std::map, std::set и т.п.:

struct AlphabeticalLess
{
template <typename CharType>
bool operator ()
(
basic_string<CharType> lhs,
basic_string<CharType> rhs
) const
{
boost::to_upper( lhs );
boost::to_upper( rhs );

return std::use_facet< std::collate< CharType > >( std::locale() )
.compare( lhs.data(), lhs.data() + lhs.size(),
rhs.data(), rhs.data() + rhs.size() ) == -1;
}
};

Приходится все переводить в upper case, а то иначе получается, что "HELLO" < "hello".

Для сортировки сойдет и версия из предыдущего поста, без to_upper.

Сортировка строк по алфавиту

Задача:
отсортировать контейнер строк по алфавиту.

Попытка 1:
std::sort( container.begin(), container.end() );


Результат получаем сортированный. Но не по алфавиту. std::sort по умолчанию использует для сортировки предикат less<T>, который в свою очередь использует operator<() для типа T, и если у нас строки типа basic_string<Ch>, то operator<() сравнивает посимвольно две строки, считая их значениями типа Ch, а не символами алфавита.

Единственное, кто может знать про понятие "алфавит", это локаль.

Попытка 2:
struct AlphabeticalCompare
{
template <typename CharType>
bool operator ()(const basic_string<CharType> & lhs, const basic_string<CharType> & rhs) const
{
return std::use_facet< std::collate< CharType > >( std::locale() )
.compare( lhs.data(), lhs.data() + lhs.size(),
rhs.data(), rhs.data() + rhs.size() ) == -1;
}
};

std::sort(container.begin(), container.end(), AlphabeticalCompare());


Вот теперь сортирует как надо.

Используемый мною тест:

#include <iostream>
#include <string>
#include <locale>
#include <vector>
#include <algorithm>

struct AlphabeticalCompare
{
template <typename CharType>
bool operator ()(const basic_string<CharType> & lhs, const basic_string<CharType> & rhs) const
{
return std::use_facet< std::collate< CharType > >( std::locale() )
.compare( lhs.data(), lhs.data() + lhs.size(),
rhs.data(), rhs.data() + rhs.size() ) == -1;
}
};

int main()
{
std::locale::global(std::locale("Ukrainian_Ukraine.1251"));

std::vector< std::wstring > container;

std::wstring s1; s1 += 0x456; // cyrillic "л"
std::wstring s2; s2 += 0x43b; // cyrillic "і"
// 0x456 is greater than 0x43b, but "і" comes before "л" in Ukrainian

container.push_back(s1);
container.push_back(s2);

std::sort(container.begin(), container.end(), AlphabeticalCompare());
std::cout << (container[0] == s1 ? "passed" : "failed") << std::endl;

return 0;
}

Update: хорошая статья на эту тему - How To Do Case-Insensitive String Comparison

08 апреля 2006

try-блоки конструкторов

Читаю Саттера. Оказывается у конструкторов можно писать try-блоки для перехвата исключений, генерируемых в конструкторах member-ов и родительских классов:

class C : private A
{
B b_;
};

C::C()
try
: A ( /*...*/ ) // optional initialization list
, b_( /*...*/ )
{
}
catch( ... )
{
// We get here if either A::A() or B::B() throws.

// If A::A() succeeds and then B::B() throws, the
// language guarantees that A::~A() will be called
// to destroy the already-created A base subobject
// before control reaches this catch block.
}

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

28 марта 2006

Фабрики объектов v.2

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

Немного изменился синтаксис:
typedef CFactory<IdType, ReturnType(ParameterType1, ParameterType2, ...)> SomeFactory;
...
SomeFactory::Execute(Id)(Parameter1, Parameter2, ...)

25 марта 2006

Object Factory

Задача
В проекте, над которым я сейчас работаю, постоянно стали требоваться так называемые "фабрики объектов" - глобальные объекты, создающие экземпляры каких-либо классов по указанию определенного идентификатора. Например, бизнес-логика, генерирующая отчеты. На входе имеем ID отчета, который нужно создать, и параметры отчета - на выходе нужно получить сформированный отчет. Причем в виде switch (ReportID) { ... } это реализовывать неудобно - по мере добавления отчетов будет расти количество case и все они собраны в одном месте, то есть нужно каждый раз перекомпилировать этот switch... Идеальное решение, чтобы можно было написать реализацию определенного отчета в отдельном модуле (скажем, файле исходного текста), и чтобы он сам "зарегистрировался" в фабрике отчетов под своим ID. Тогда запросив у фабрики отчет по ID, она сама найдет "создателя" нужного отчета.

Решение
Я попытался общий создать шаблон для подобных фабрик объектов. В частном случае от фабрики требуется вызвать функцию, зарегистрированную для определенного ID типа IdType. Функция принимает в качестве параметра объект типа ParameterType, а возвращает значение типа ReturnType.

Например, для отчетов может быть IdType = string, ParameterType = map<string, _variant_t>, ReturnType = shared_ptr<Report>. То есть отчет идентифицируем по его названию, передаем ему параметры, где каждый параметр = имя (string) + значение (_variant_t), а получаем – умный указатель на созданный отчет.

Шаблон я создал следующий: Factory.h

Пример использования:
typedef CFactory
<const string &, shared_ptr<Report>, const map<string, _variant_t> & >
CReportFactory;

shared_ptr<report> Generator(const map<string, _variant_t> &)
{
// generator code
}

// register Generator as "test report"
bool Registered = CReportFactory::Register("test report", Generator);

int main()
{
// fill parameters
map<string, _variant_t> Parameters;
Parameters.insert(...);

// generate report
shared_ptr<Report> TestReport =
CReportFactory::Execute("test report", Parameters);
}

Нюансы
В случае, если все генераторы отчетов компилируются все в одном месте с самой фабрикой (то есть не разнесены по разным библиотекам), то регистрация на фабрике выполняется достаточно просто – введением глобальной переменной:
bool SomeGeneratorRegistered = SomeFactory.Register(SomeID, SomeGenerator);

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

В библиотеке:
bool SomeLibrary1Registered =
SomeGenerator11Registered &&
SomeGenerator12Registered &&
...;

В основном модуле (исполнимый файл или DLL):
extern bool SomeLibrary1Registered;
extern bool SomeLibrary2Registered;
...
bool LibrariesRegistered =
SomeLibrary1Registered &&
SomeLibrary2Registered &&
...;

12 марта 2006

decimal vs float

В C++ если написать 12, то это будет воспринято компилятором как (int)12, а 12.0 дает (float)12.

MS SQL Server 2000 понимает 12 как int, а 12.0 - ...нет, не как float, а как decimal с шестью знаками после запятой.

Мне нужно было посчитать коэффициент 1/12, чтобы результат получился float. 1/12 дает 0, и я использовал 1.0/12.0 наивно полагая, что числа будут восприняты как float. Ан нет, при выполнении запросов вылезали погрешности. Пришлось написать так: 1e0/0.12e2 (или, как вариант - 1e0/12).

07 марта 2006

deque vs vector

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

Преимущества deque перед vector:
1. Меньше overhead. Вектор растет сразу в два раза, а дека - фиксированными страницами. Если храним мегабайт с копейкой, то вектор будет двухмегабайтный, а дека - мегабайт плюс маленький хвостик.
2. Отстуствие копирования старого содержимого на новое место при расширении контейнера.

Недостатки:
1. Отсутствие доступа к содержимому как к линейному массиву.

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

А вот отсутствие ненужного копирования при realloc-е у деки существенно улучшает performance.

Достоинство вектора - доступ к содержимому как к линейному массиву. То есть можно накопить кучу данных в векторе, а потом передать все содержимое какой-нибудь API-шной функции типа WriteFile или SendData с параметрами (void *buf, size_t size).

Но это является плюсом вектора только в теории. Сравним два варианта:
1. Накапливаем данные в vector, затем за один вызов "сливаем" их в нашу WriteFile(void *buf, size_t size)
2. Накапливаем данные в deque, затем в цикле "сливаем" часть в буфер ограниченного размера, а потом передаем этот буфер нашей WriteFile.

Предполагаем, что изначально мы не знаем сколько элементов придется поместить в контейнер (иначе, естественно, vector::reserve() всех порвёт).

Пусть в результате в контейнер окажутся помещенными N элементов. Ближайшие к N степени двойки назовем 2n и 2n+1. То есть 2n < N <= 2n+1.

В первом случае на этапе накопления произойдет 1+2+4+8+...+2n = 2n+1-1 копирований. Назовем это количество С1.

Во втором случае будет копирований элементов ровно столько, сколько элементов в контейнере. Происходить все они будут в цикле (когда все они будут копироваться в буфер), а при накоплении данных дек ничего не перекладывает с одного места в другое. Количество копирований C2=N.

С1=2n+1-1 всегда больше С2=N, кроме двух случаев, которые можно особо не учитывать. А если предположить, что в среднем N где-то посередине межде 2n и 2n+1, то второй вариант на 25% быстрее первого.

То есть даже тот факт, что вектор потом можно удобно (без дополнительных копирований) "слить" API-шной функции, не дал вектору преимуществ перед декой.

Исключения: при маленьких N или в случаях, когда N заранее известно - тут вектор выигрывает.

24 февраля 2006

Велосипеды

Как часто, однако, делаешь что-либо, а потом понимаешь, что изобретаешь велосипед. Недавно изобрел один такой.

Некие объекты имели контейнеры. Но не контейнеры объектов, а контейнеры указателей. И, как водится, перед удалением этих контейнера в деструкторе был подобный код:
CTableContainer::iterator TableIt;
for (TableIt = Tables.begin();
TableIt != Tables.end();
TableIt++)
{
delete *TableIt;
*TableIt = NULL;
}
Недолго думая, прикрутил к контейнерам (через шаблон) деструктор, который все это делает сам. Получились такие "умные контейнеры указателей" - сами удаляют за собой свои объекты.

И вот только тогда, когда все это реализовал, подумал - ведь не я первый с этой проблемой сталкиваюсь - контейнеры указателей. Раз их в стандартной библиотеке нет, то видимо они долны быть в Boost. Немного полистав документацию Boost, я нашел то, что было нужно - Pointer Container Library.