31 декабря 2006

Properties in C++

Замечательная штука есть в современных языках программирования - properties. Берешь и пишешь: Object.Visible = True и объект отображается на экране. Хочешь проверить, отображен ли объект - пишешь If Object.Visible Then...

Все так просто! Visible - это свойство объекта "быть показанным на экране". Мы можем легко читать и изменять это свойство зная его имя.

В C++ для этого необходимо сделать accessor и mutator, то есть две функции: одна позволяющая получить значение, вторая - изменяющая значение (со всеми последствиями для объекта). Если accessor можно назвать именем свойства - bool Visible() - то как быть с mutator-ом? Назвать его Visible(bool)? Не красиво. SetVisible(bool)? Не очень понятно. Show(bool)? А как запомнить что это mutator к свойству Visible?

Более красивый вариант - прикрутить properties к C++. Загоревшись этой идеей набросал шаблон property:
template <typename T, class C>
class property
{
public:
typedef T (C::*accessor_t)() const;
typedef void (C::*mutator_t)(const T&);

property(C* object, accessor_t accessor, mutator_t mutator) :
_object(object), _accessor(accessor), _mutator(mutator) {}

operator T() const { return ((*_object).*(_accessor))(); }
property& operator=(const T& value) { ((*_object).*(_mutator))(value); return *this; }

private:
C* _object;
accessor_t _accessor;
mutator_t _mutator;
};

class range
{
public:
range(int begin, int end) : _begin(begin), _end(end) {}

int begin() const { return _begin; }
int end() const { return _end; }

public:
property<int, range> size() { return property<int, range>(this, size, setsize); }

int size() const { return _end - _begin; }
void setsize(const int& size) { _end = _begin + size; }

private:
int _begin, _end;
};

void TestProperties()
{
range r(1, 5);
cout << "range is [" << r.begin() << "," << r.end() << ") size=" << r.size() << endl;

r.size() = 2;
cout << "range is [" << r.begin() << "," << r.end() << ") size=" << r.size() << endl;
}

29 декабря 2006

Dynamicaly created SQL statements

Переписываю приложение с VBA на C++. В приложении во всю используются динамически создаваемые SQL-запросы:

Было:
Условие = ""
If VarType(Me![Начальная дата]) > vbNull Then ДобавитьУсловие Условие, "[Дата] >= " & ДатаДляSQL(Me![Начальная дата])
If VarType(Me![Конечная дата]) > vbNull Then ДобавитьУсловие Условие, "[Дата] <= " & ДатаДляSQL(Me![Конечная дата])
If VarType(Me![Тип]) > vbNull Then ДобавитьУсловие Условие, "[Тип] = " & Me![Тип]
If VarType(Me![Эмитент]) > vbNull Then ДобавитьУсловие Условие, "[Эмитент] = " & Me![Эмитент]
If VarType(Me![Структура]) > vbNull Then ДобавитьУсловие Условие, "[Структура] = " & Me![Структура]
Me![Список].RowSource = "SELECT * FROM [События - список]" & OptionalWhereStatement(Условие) & " ORDER BY [Дата], [Название общества]"


Стало:
<dataset name="Events">
Select
[Events].[id],
[Events].[Date],
[Persons].[Name] [Person Name],
[Events: Types].[Name] [Event Type Name],
[Events].[Notes]
From [Events]
Left Join [Persons] On [Persons].[id]=[Events].[Person]
Left Join [Events: Types] On [Events: Types].[id]=[Events].[Type]
Where (1=1)
<where expression="[Events].[Person]" condition="=" param="Person"/>
<where expression="[Events].[Type]" condition="=" param="Type"/>
<where expression="[Events].[Date]" condition="&lt;=" param="Date Max"/>
<where expression="[Events].[Date]" condition="&gt;=" param="Date Min"/>
<where expression="[Persons].[Financial Group]" condition="=" param="Financial Group"/>
Order By [Events].[Date]
</dataset>


Определенно стало намного удобнее писать SQL-запросы.

14 декабря 2006

Memory allocation by compiler

Получил занимательное сообщение от компилятора:

Fatal Error C1076: compiler limit : internal heap limit reached; use /Zm to specify a higher limit

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

12 декабря 2006

Performance issues

Загрузил в свой проект реальные данные и ужаснулся - получил жуткие тормоза. Окно открывалось секунд 10. Нереально долго.

Вначале подумал что SQL запросы тормозят на большом количестве данных. Ан нет, летают. Собрал Release версию. Бегает намного быстрее, но все равно - окно открывает 2-3 секунды. Это конечно лучше чем 10 секунд у Debug версии, но мне 1) в бета-тестирование давать именно Debug версию, 2) самому отлаживат - Debug версию, 3) все равно очень долго.

Методом тыка (без профайлера) выяснил что больше всего тормозит lookup записей из таблиц. Причем в коде стоял простой std::find и комментарий "todo: использовать индексы для быстрого поиска". На тестовых данных все летало, а вот на реальных данных, когда есть 3000 записей к ним нужно lookup-ить 2000 записей получалось O(M*N) = 2000*3000 = 6000000. Реализовал индексирование - стало намного быстрее. Открытие окна - секунды 2. Причем и в debug и в release примерно одинаково.

Пробежал код глазами - обнаружил интересный момент. При загрузке данных с SQL сервера идет преобразование из COM-овского _variant_t в проприетарный CVariant. Строки загружаются очень весело: у CVariant есть метод SetString(const std::wstring&), а у _variant_t достать строку можно только сначала преобразовав его в _bstr_t (при этом строка копируется), затем в const wchar_t*. При передаче ее в SetString() сначала конструируется wstring (а это плюс еще одно копирование), затем идет непосредственно копирование строки внутри CVariant::SetString(). Вобщем куча копирований вместо одного. Придется вручную лезть внутрь _variant_t, благо он унаследован от tagVARIANT, который является структурой - то есть все внутренности - public. Но самое интересное - жуткие тормоза возникают совсем не из-за этого, а где-то в другом месте.

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

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

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

PS. Как в таких случаях жить без профайлера? VTune $700 стоит. Может что попроще и побесплатней есть?

08 декабря 2006

Last element in a container

Поймал себя на использовании конструкции Container[Container.size()-1] для доступа к последнему элементу контейнера.

Ужаснулся, переписал все через *Container.rbegin() Container.back()

01 декабря 2006

assert

assert - утверждать; заявлять, объявлять, декларировать, провозглашать

assert(Pointer != NULL);
"Я утверждаю что в данной точке программы Pointer не равен нулю и пусть тот, кто считает что я не прав, первым бросит в меня камень исключение!

22 ноября 2006

Build optimization

Решил поискать пути ускорения build-а не меняя аппаратную часть компьютера. Перенес includes из boost на RAM disk. Проект который раньше компилировался пять минут теперь компилируется четыре.

Убил RAM disk, перенес весь используемый boost и стандартную библиотеку в precompiled header. Компиляция того проекта укладывается уже в 2 минуты.

21 ноября 2006

Smart pointers rule

Набрел в сети на Process Explorer, побаловался с ним. Посмотрел сколько мое приложение ест памяти и т.п. Провел эксперимент - в приложении несколько раз открыл-закрыл довольно тяжелое по требованиям к памяти окно.

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

Но, спрашивается, если я везде использую умные указатели, то какого хрена получаю утечки памяти? Это напрягло меня вдвойне. Я никогда в жизни не искал утечки памяти - на C++ пишу чуть больше года, до этого писал на VBA с его COM-объектами и отсутствием заморочек на счет освобождения памяти.

Однажды я попробовал некую небольшую утилиту для поиска памяти - она встраивалась в Visual Studio и по завершении приложения выдавала список утечек. Так она мне их выдало такое немерянное количество в моем приложении, что я в это не поверил и снес ее нафиг.

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

Но сейчас мне совсем не хотелось с этим мучаться - я ведь просто игрался с Process Explorer. Но утечки покоя мне не давали.

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

Это был следующий код:
std::wstring CXMLNode::OptionalAttr
(
const std::wstring& AttrName,
const std::wstring& DefaultValue
) const
{
assert( !IsNull() );
const xmlChar* const AttrStr =
xmlGetProp( _Node, WideStringToXMLChars( AttrName ).c_str() );
if ( AttrStr != NULL )
{
return XMLCharsToWideString( AttrStr );
}
else
{
return DefaultValue;
}
}
Это был класс C++-оболочки libxml, написанный не мной, а программистом, работавшим на меня. Я конечно, порадовался, что код не мой ;) И сразу очевидно, утечку дает AttrStr = xmlGetProp(...), так как xmlGetProp() - функция из C-шной библиотеки (libxml), а там про умные указатели не знают.

Так и оказалось - в документации к xmlGetProp значилось:
Returns: the attribute value or NULL if not found. It's up to the caller to free the memory with xmlFree().
А мой программист на xmlFree() забил.

Так как xmlGetProp() постоянно использовалась, пришлось написать для нее "smart pointer", чтобы не напрягать мозги на счет xmlFree(). Что-то типа такого:
// XMLAttr helper for automaticaly memory manage
class CXMLAttr
{
public:
CXMLAttr(xmlNodePtr Node, const std::wstring& AttributeName);
bool IsValid() const;
const std::wstring& Value() const;

private:
bool _IsValid;
std::wstring _Value;
};

inline CXMLAttr::CXMLAttr(xmlNodePtr Node, const std::wstring& AttributeName)
{
std::basic_string Name;
WideStringToXMLChars(AttributeName, Name);

xmlChar* const Attribute = xmlGetProp(Node, Name.c_str());

_IsValid = (Attribute != NULL);

if (Attribute)
{
XMLCharsToWideString(Attribute, _Value);
xmlFree(Attribute);
}
}

inline bool CXMLAttr::IsValid() const
{
return _IsValid;
}

inline const std::wstring& CXMLAttr::Value() const
{
return _Value;
}


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

09 ноября 2006

Вам тултипы какой версии?

Тестируя свое приложение под Windows 2000 обнаружил следующий баг: не работают тултипы. В XP работают, а в Win2k - нет.

Окзалось, засада ждала меня в совершенно безобидном операторе:
toolinfo.cbSize = sizeof(TOOLINFO)

Объясню, почему там была засада. Для начала посмотрим как объявлен TOOLINFO (точнее tagTOOLINFOW, т.к. TOOLINFO - его алиас) в commctrl.h:
typedef struct tagTOOLINFOW {
UINT cbSize;
UINT uFlags;
HWND hwnd;
UINT_PTR uId;
RECT rect;
HINSTANCE hinst;
LPWSTR lpszText;
#if (_WIN32_IE >= 0x0300)
LPARAM lParam;
#endif
#if (_WIN32_WINNT >= 0x0501)
void *lpReserved;
#endif
} TTTOOLINFOW, NEAR *PTOOLINFOW, *LPTTTOOLINFOW;

В параметрах компиляции у меня стоит _WIN32_WINNT = 0x0501, чтобы была возможность использовать все возможности WinXP, такие как XP themes и прочие. Как видно выше, при таких настройках я получаю sizeof(TOOLINFO), учитывающее lpReserved, про который Win2000 не знает. И встретив toolinfo.cbSize больше того размера TOOLINFO, которое известно Win2000, функции WinAPI отказываются работать.

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

Ну да ладно, не проглатывает и пусть. Но как быть? Установить _WIN32_WINNT меньше 0x0501 я не могу. Остается "вручную" (вычесть указатель на tooltip из указателя на tooltip.lpReserved) считать размер TOOLINFO для Win2000 и использовать его. Здесь надо заметить, что сам я sizeof(TOOLINFO) не считаю, так как он за меня считается внутри WTL::CToolInfo::Init() ;-) Выход я, конечно найду (откажусь от WTL::CToolTipCtrlT или внесу в WTL::CToolInfo::Init() поправку), но осадок остается.

01 ноября 2006

Unit tests rule

Сегодня на собственной шкуре убедился в ценности юнит-тестов.

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

Но не задел ли я при этом ничего другого?

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

Не будь этого теста, количество ошибок в модуле осталось бы таким как было, просто одна ошибка сменилась на другую ;)

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

31 октября 2006

Отбросьте ваши тени в сторону

В минимальных требованиях к окружению (харду и софту) разрабатываемой мною системы стоит "Windows 2000 и выше". Так что приходится тестировать систему под Win2k. О некоторых таких приключениях я уже писал, а недавно Win2k ввел меня в очередной ступор.

При нажатии на кнопку, раскрывающую список combobox'а, всегда вылетаем по assert(hwnd == NULL). Под WinXP все отлично. Тут надо заметить, что Win2k стоит у меня на виртуальной машине на ноуте. На эту виртуальную машину для отладки пришлось поставить Visual Studio (предварительно установив кучу всяких IE Service Pack, .NET, etc, которые требовал VS).

Проект компилировался на виртуальной машине минут 30-60!!! Оказалось что не может зарегистрироваться оконный класс раскрывающегося списка combobox-а, причем GetLastError() выдает "Недопустимый параметр". Какой параметр - не говорит. Кстати, ради добавления этого самого GetLastError() в код его пришлось перекомпилировать - а это опять полчаса дикого жужжания вентилятора процессора (на ноуте у меня Celeron M 1.4).

Класс не регистрировался из-за стиля CS_DROPSHADOW, которого видимо нет на Win2000. Вот так все оказалось просто. А времени на отладку ушло просто офигеть сколько.

PS. Вообще надо памятник поставить изобретателям assert()-ов. Мега-полезная вещь.

30 октября 2006

Как легко и не принужденно держать себя за волосы

В моем проекте в части GUI написаны собственные классы типа CEdit, CButton и etc, которые имеют интерфейсы, используемые бизнес-логикой. Непосредственно реализация этих контролов представлена в классах типа CEditImpl, CButtonImpl и т.п., которые наследуются от ATL::CWindow. Эти C*Impl содержатся в виде private членов внутри соответствующих C*.

Все было отлично. Но вот по событию "нажатие кнопки", приходящему синхронно от CButton в бизнес-логику, эта бизнес-логика закрывает окно, в котором содержится кнопка и все рушится по assert-у в недрах ATL::CWindow. Фигня вся в том, что все это происходит в обработчике WM_LBUTTONDOWN, а закрытие окна ведет к удалению объекта этого самого окна, которое грохает коллекцию его контролов (CEdit, CButton, ...), они в свою очередь грохают объекты CEditImpl, CButtonImpl, ... А деструктор класса ATL::CWindow (от него унаследованы все CButtonImpl) ругается на то, что виндовый контрол "кнопка" еще не уничтожен - внутри его обработчика WM_LBUTTONDOWN мы сейчас сидим.

В ATL/WTL безопасно можно грохать оконный объект в виртуальной OnFinalMessage(), то есть объект CButtonImpl должен пережить CButton, а в CButtonImpl::OnFinalMessage() прибить себя. Но все же CButton тоже должен уметь убивать CButtonImpl, например если виндовый контрол еще не был создан - в таком случае до CButtonImpl::OnFinalMessage() дело не дойдет.

Вобщем было много головной боли по поводу того кто и когда должен грохнуть объект CButtonImpl: то ли сам CButtonImpl, то ли CButton, они должны между собой согласовать действия чтобы:
1. не упать по assert-у в недрах деструктора ATL::CWindow
2. все же удалить CButtonImpl
3. не удалить его несколько раз ;)

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

Решение получилось примерно такое:
class CButtonImpl : ATL::CWindow...
{
public:
void OnFinalMessage() { SharedPointer.release(); }

boost::shared_ptr<CButtonImpl> SharedPointer;
}

class CButton
{
public:
CButton() : _ButtonImpl(new CButtonImpl)
{
}

void Create()
{
_Button->Create();
_Button->SharedPointer = _Button;
}

private:
boost::shared_ptr<CButtonImpl> _ButtonImpl;
};

То есть условно говоря, CButtonImpl держит себя за волосы пока он существует как виндовый контрол.

Ссылка по теме: delete this

26 октября 2006

Easy constructors

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

В простейшем случае все выглядит достаточно просто:
class Foo : public Bar
{
public:
void NewMember();
}

Однако в таком случае унаследованы все публичные методы, но не конструкторы Bar (разве что кроме конструктора по умолчанию). Вручную "наследовать" все конструкторы может быть довольно утомительно, поможет парочка шаблонных конструкторов:
class Foo : public Bar
{
public:
Foo() : Bar() {}
template <typename T> Foo(T t) : Bar(t) {}
template <typename T1, typename T2> Foo(T1 t1, T2 t2) : Bar(t1, t2) {}
...

void NewMember();
}

11 октября 2006

Precompiled headers fun

Сегодня упорно боролся с компилятором. Ну не видет он некоторые классы и всё тут, хотя нужные header-файлы указаны.
#include "Event.h"
#include "Control.h"
Event event;
Вот на последней строчке он и ругается - не знаю я ваш Event и все тут.

После некоторого времени ковыряний до мнея дошла замечательная мысль - файл Control.h указан как файл, по которому нужно использовать precompiled header.

То есть встречая Control.h компилятор грузит precompiled header и напрочь забывает про header-ы, которые были до него.

Поставил #include "Control.h" первой строкой - компилятор стал доволен.

10 октября 2006

XP Themes and Windows 2000

В текущем у меня проекте активно используются XP-темы и для простоты их использования я создал небольшую иерархию классов:
CSkin - CBaseSkin - CXPThemeSkin

CSkin представляет из себя интерфейс, CBaseSkin рисует без XP-тем (то есть вполне должен выполняться на Windows 2000), CXPThemeSkin соответственно рисует с помощью XP-тем (если не удается - рисует с помощью своего базового класса - CBaseSkin).

CXPThemeSkin использует всякие OpenThemeData(), CloseThemeData() из uxtheme.dll, CBaseSkin ничего этого не использует.

При старте программа определяет версию Windows и при WinXP создает объект CXPThemeSkin, в противном случае - CBaseSkin.

И я наивно полагал, что это будет работать на Windows 2000. А вчера поставил Win2k на виртуальную машину и... разочаровался - при старте приложение вылетало с ошибкой "Unable to find uxtheme.dll". Самое интересное, что дело не доходило даже до WinMain() и создания объекта CxxxSkin. То есть XP-темы еще не использовались, а приложение уже вылетало.

При разборе полетов выяснил, что любое упоминание в программе функций из uxtheme.dll приводит к ее загрузке uxtheme.dll, даже такой безобидный код:
void foo()
{
HTHEME Theme = OpenThemeData(NULL, NULL);
}

void WindMain(...)
{
return 0; // do not call foo()
}

Я уж было предполагал, что придется отказаться от простых вызовов OpenThemeData() и подобных, и переписать весь код с использованием LoadLibrary("uxtheme.dll"), вручную получением указателей на функции типа OpenThemeData().

Оказалось, все гораздо проще: достаточно указать в настройках проекта
Linker/Input/Delay Loaded DLLs: uxtheme.dll

01 октября 2006

Edit_SelectAll()

Казалось бы, простая задача - при получении фокуса edit box'ом выделить в нем весь текст. У стандартного edit-а в Windows есть даже такое сообщение EM_SETSEL, которое позволяет выделить любую часть текста. Можно и весь текст выделить. Только при любом выделении текста через EM_SETSEL курсор встает после выделения. Таким образом если текст в edit-е введен длинный - то мы увидим хвост этого текста, а не начало. А такой вариант - полный идиотизм: например, при переходе в ячейку, в которую введено "Иванов Петр Григорьевич" мы увидим [етр Григорьевич] вместо [Иванов Петр Гр], хотя последний вариант намного информативный. К слову сказать, в Microsoft Office сделано правильно, но там свои контролы, а у нас - стандартный edit.

Облазив DejaNews Google.Groups я понял, что цивилизоанного способа попросить стандартный edit поставить курсор в начале выделения люди так и не нашли. Пришлось написать свой трехколесный велосипед:
1. Ставим курсор в конец текста
2. Нажимаем Shift-Home

Как я не люблю такие извращения... Давно пора написать свой edit control или перейти на Scintilla.
void Edit_SelectAll(HWND Edit)
{
ATLASSERT(::IsWindow(Edit));

// Move cursor to the end of text
int Len = ::GetWindowTextLength(Edit);
::SendMessage(Edit, EM_SETSEL, Len, Len);

// Set [Shift] key state as pressed
BYTE KeyboardState[256];
::GetKeyboardState(KeyboardState);
BYTE ShiftState = KeyboardState[VK_SHIFT];
KeyboardState[VK_SHIFT] = ShiftState | 0x80;
::SetKeyboardState(KeyboardState);

// Press [Home] key
::SendMessage(Edit, WM_KEYDOWN, VK_HOME, 0x01470001);

// Restore [Shift] key state
KeyboardState[VK_SHIFT] = ShiftState;
::SetKeyboardState(KeyboardState);
}

28 сентября 2006

Calling base class virtual method

Предположим, у нас есть иерархия классов:
class Base
{
public:
virtual void Do() { cout << "Base::Do()" << endl; }
virtual ~Base() {}
};

class Derived : public Base
{
public:
virtual void Do() { cout << "Derived::Do()" << endl; }
};
Класс Derived переопределяет метод Do(), и код, оперирующий объектом этого класса, при вызове Do() будет всегда вызвать Base::Do().

Даже если преобразовать указатель или ссылку на объект класса Derived к указателю или ссылке на Base, то будет вызываться Derived::Do() - за это отвечает таблица виртуальных функций или другой подобный механизм, встроенный в язык:
Derived d;
d.Do(); // Derived::Do()
Base& b = d;
b.Do(); // тоже Derived::Do()

А теперь вопрос: как вызвать Base::Do() для объекта типа Derived?

Во-первых, разберемся корректно ли это. По сути, void Base::Do() представляет собой void Do(Base* This), а void Derived::Do() представляет void Do(Derived* This). Так как можно легко преобразовывать Derived* к Base*, то есть вызов Base::Do() для Derived является корректным. Такой вызов часто используется при напимании методов унаследанного класса:
void Derived::DoSomething()
{
// do something
Base::Do();
// do something else
}
Прекрасно, "изнутри" Derived (то есть из методов самого Derived) мы можем вызывать Base::Do(), но как быть если нужно вызвать этот метод "снаружи"?

Оказывается, синтаксис такой:
Derived d;
d.Base::Do();

19 сентября 2006

Is "for each" possible with current C++?

Всем поклонникам C++ советую прочитать эту статью: Conditional Love: FOREACH Redux, написанную одним из C++-geek-ов - Эриком Ниблером из Boost Consulting.

Вы поймете какую радость приносит людям язык C++ с кучей своих недостатков и нереализованных в нем возможностей. Сможете ли Вы написать макрос FOREACH(item, container)? Будет ли он нормально работать с любыми контейнерами, в том числе и с rvalue? Возможно ли вообще его написать на современном C++ не дожидаясь выхода новых стандартов?

Полученный Эриком FOREACH, по-моему, представляет чисто академический интерес (зато какой интерес!). У меня и так время компиляции - узкое место, а с такими конструкциями мой компилятор вообще коньки отбросит наверно...

PS. А вообще эта статья про мой любимый условный оператор ?:

09 сентября 2006

Эволюция WinAPI

Каким образом нужно использовать ту или иную возможность WinAPI - сильно зависит от времени, когда она появилась. Если она была введена в Win3.1 - получите доступ через сообщения, если в Win2000 - через COM объекты. Полагаю, новые возможности Vista будут доступны только через .NET.

Пример - настройка combobox. Показывать в нем свои элементы можно давно, и реализовывается это через обработку сообщений (WM_DRAWITEM). Сделать собственное автодополнение и автозаполнение - сравнительно недавно, и делать это надо уже через написание COM-объекта (IAutoComplete). Новые возможности комбобокса, думаю будут доступны через managed code.

30 августа 2006

Контролы от Microsoft снова рулят

Оказывается, нельзя менять стиль стандартного Windows editbox-а. Хотя так естественно было менять стиль multi-line/single-line при обработке WM_SIZE. А не получится.

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

Экономия на мелочах?

Некоротые товарищи пытаются сэкономить несколько байт, используя вместо int тип signed char или unsigned char. Их мотивация такая: если у нас числа не больше 127 (255), то зачем использовать четырехбайтный int (на 32 битной платформе), когда можно обойтись всего одним байтом? Экономия - 400%!

Но на деле есть а) выравнивание данных компилятором, б) размер процессорного слова.

Выравнивание может быть настроено на 4 или 8 байт и структура
struct { signed char x, y; } займет 8 или 16 байт, а не 2.

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

То есть никакой экономии памяти при переводе единичных переменных (не массивов) с int на char скорее всего не будет. Зато возможно будут некоторые проблемы.

Ситуация - чтение данных из текстового потока:
int value;
textstream >> value;


При входном потоке "0" мы получим value == 0. А при замене int на char получим value == 48. Сэкономили несколько байт, а получили лишнюю головную боль.

27 августа 2006

Откуда растут руки у программистов Windows?

Интересно посмотреть в глаза людям, которые писали стандартные контролы в Windows.

Стандартный button позволяет рисовать на кнопке иконку (BS_ICON window style + BM_SETIMAGE message). Все замечательно, пока у вас Windows 2000. Когда у вас Windows XP, то вы понимаете, что кнопка с иконкой не поддерживает темы XP. И поддержку тем можно сделать только вручную. А уж если что-то делать вручную, то лучше уж тогда полностью свою кнопку написать. Чтобы не ожидать очередных подстав со стороны Microsoft.

09 августа 2006

Как закрыть форточку?

WinAPI - вещь для посвященных. Если нужно что-то сделать, мало прочитать документацию (хорошо что хоть она легко доступна). Чтобы что-то сделать правильно, нужно либо долго помучаться, либо перелопатить форумы. Либо и то, и другое.

Задача: закрыть MDI-child окно.
Решение "в лоб": казалось бы, чего проще: DestroyWindow(hWnd)
И даже окно закрывается, на первый взгляд. Но фокус на другое MDI-child окно не переходит, а должен бы. Но это еще не беда. Беда начинается тогда, когда это окно (child) максимизировано. Тогда происходят вообще чудеса: окно закрывается, но от окна в строке меню остаются значки maximize-minimize-close и значок системного меню! Красота, да и только. Если так закрыть несколько окон, то будет полнейший сюрреализм:



Hint: У MDI-child-а обработчик WM_CLOSE по умолчанию закрывает окно (причем делает это грамотно).

Простой вызов SendMessage(hWnd, WM_CLOSE) подходит, но не во всех случаях. Представьте себе, вы пишете обработчик WM_CLOSE, в котором пишете такую конструкцию ;) Да мы просто зациклимся.

В результате у меня получилась подобная конструкция:
bool CMDIFormImpl::Destroy()
{
// This is temporary solution!!!
// todo: replace it by something more suitable

HWND hWnd(m_hWnd);
// call default MDI-child WM_CLOSE handler: it does MDI-destroying better than us
DefWindowProc(WM_CLOSE, 0, 0);
return !::IsWindow(hWnd);
}

01 августа 2006

Убей родителя, и он убъет тебя

В ATL/WTL конструкция delete this; - дело довольно обычное. Так можно в OnFinalMessage() удалить собственный объект.

А теперь представьте ситуацию: WTL-контролы (кнопки, edit box-ы и т.п.) хранятся в контейнере указателей у их Parent-а (диалога, etc.). В своем OnFinalMessage() родитель делает delete this, а его деструктор грохает все объекты WTL-контролов.

Все хорошо до тех пор, пока мы не захотим по нажатию кнопки мыши на контроле закрыть окно родителя. В обработчике контрола по WM_LBUTTONDOWN (скажем, в CControl::OnLButton()) мы вызываем Parent->DestroyWindow() и... падаем по assert-у, так как parent удаляет наш объект, а мы еще не вышли из CControl::OnLButton(), и наш объект еще может понадобится (не в OnLButton(), а в вызвавшем его коде).

Можно, конечно, сделать PostMessage(Parent, WM_CLOSE или WM_DESTROY), и тогда родитель будет удален уже потом, после выхода из CControl::OnLButton(), так как WM_DESTROY дойдет до него не сразу, а просто встанет в очередь. Но иногда нужно грохнуть родителя здесь и сейчас.

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

28 июля 2006

Есть ли жизнь после assert()?

assert() - полезный макрос для документирования pre-conditions и отладки кода. Однако, не стоит забывать про release version, когда все эти ассерты превратяться в тыкву. Одно дело, когда условие assert-а зависит от внутреннего состояния программы, другое дело - если от внешних данных. Тут нужно менять assert() на что-то, что выживет в release.

Config = LoadConfig("config.cfg");
assert(Config != NULL);
Config->GetValue(...)


Вот в таком случае явно не место assert-у. Как минимум - throw std::exception("wtf w/config?")

19 июля 2006

Template Template Parameters

То, что параметры шаблона могут не только типами и классами, а еще и шаблонами, я узнал только читая Александреску (нет чтобы стандарт C++ почитать ;)).

В кратце суть шаблонных параметров шаблона:

template <typename T>
class Vector { ... }

template <typename T>
class Deque { ... }

template <template <typename> class Container>
class SomeClass
{
Container<SomeDataType> Data;
}

SomeClass<Vector> VectorObject;
SomeClass<Deque> DequeObject;

13 июля 2006

delete this

Конструкция delete this; выглядит довольно необычно. Выглядит, как будто мы рубим сук, на котором сидим. Однако, все довольно безопасно, если выполнять меры предосторожности (мыть руки перед едой ;)).

Что из себя представляют методы класса? Да это обычные функции, которым неявно передается указатель this.

void vector::remove(ItT it)
на самом деле выглядит примерно как
void ::remove(vector* this, ItT it)

Кстати, int vector::size() const
выглядит как int ::size(const vector* this).

Соответственно, внутри метода можно спокойно удалить какой-то объект, а называется он this или как-то по-другому - это все равно. Главное, не пользоваться этим объектом после удаления. Причем (внимание!) нужно перестать им пользоваться совсем, то есть не только в этом методе после выполнения delete this;, но и там, откуда этот метод вызван.

Но это еще не все предосторожности. Перед тем как сделать delete this; естественно надо убедиться, что объект создан через new, а не на стеке или еще как-то (каким-нибудь нестандартным аллокатором). Как убедиться в этом - вопрос дизайна (design). Можно просто написать в документации: "этот объект нужно создавать через new" или сделать конструкторы приватными и создат static-member-ов для создания объектов. Или вообще использовать boost:shared_ptr, и тогда вместо delete this; будет sp.release(), который удалит объект как ему предписывает назначенный deleter.

Ссылка по теме: ATL's CWindowImpl::OnFinalMessage() crash

04 июля 2006

STL and C++ Standard Library

Многие путают STL и стандартную библиотеку C++. Некоторые думают что это одно и то же.

На самом деле STL - библиотека, разработанная SGI, часть из которой включили в стандарт C++. Уже из этого вытекают два факта:
1. STL ≠ стандартная библиотека C++,
2. не все, что есть в STL, есть в стандартной библиотеке C++.

Если объяснять на пальцах: парни, разрабатывающие стандарт C++, в один день подумали - вон какие хорошие вещи есть в STL, давайте скопируем некоторые из них в наш стандарт. Скопировали не всё, например, в стандартной библиотеке C++ нет алгоритма copy_n, хотя он есть в STL.

Третье. Стандартная библиотека C++ намного шире STL: в ней есть потоки ввода-вывода, auto_ptr, и много чего другого. Так что говоря про стандартную библиотеку не стоит называть ее "STL".

Наиболее полную реализацию STL можно найти в STLPort.

PS. В следующем стандарте в стандартную библиотеку C++ будет включено много вещей из Boost-а, но это не повод называть стандартную библиотеку Boost-ом :)

28 июня 2006

ATL::CWindow

ATL/WTL почему-то не до конца следует принципу «Resource Acquisition Is Initialization». Деструктор класса ATL::CWindow не удаляет окно (не вызывает DestroyWindow()). То есть CWindow - это не окно, это некая прокси для управления окном.

21 июня 2006

Coding Standards: Smart pointers

Добавил в свой Coding Standards раздел про использование указателей.

Интеллектуальные указатели

Любое использование ресурсов должно следовать принципу «Resource Acquisition Is Initialization».

Таким образом, любое выделение памяти через new должно инициализировать умный указатель:
char *Node = new char[Size];
scoped_array<char> Buffer( new char[Size] );

XMLNode* CreateNode(CFile File) { return new XMLNode(File); }
auto_ptr<XMLNode> CreateNode(CFile File) { return new XMLNode(File); }

Это правило относится к любым ресурсам, не только в динамической памяти. Например, если необходимо воспользоваться функциями WinAPI для работы с файлами CreateFile() и CloseFile() необходимо написать класс «оболочку» над этими функциями, который в деструкторе будет закрывать файл.

Можно использовать любые классы их используемых библиотек:
− Standard C++ Library (auto_ptr, fstream, ...)
− boost (shared_ptr, scoped_ptr, ...)
− WTL (CDC, CClientDC, CFont, ...)

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

12 июня 2006

Указатели

Стиль программирования может быть разный. Но по-моему такие конструкции остались далеко в прошлом:
char *Text = new char[TextLength];

Почувствуйте разницу:
boost::scoped_array<char> Text(new char [TextLength]);

29 мая 2006

Pessimization is evil

Многие знают, что ранняя оптимизация - корень всех бед. Но и явная пессимизация - тоже большое зло. Зачем писать явно неоптимально, когда можно написать нормально? Правильно, незачем.

Классический пример:
for
(
iterator It(begin()), End(end());
It != End;
++It
)
{
...
}
Здесь:
1. Использование для инициализации копирующего конструктора вместо присваивания. При написании iterator It = begin() компилятор может создать временный объект, хотя может и соптимизировать.

2. Префиксная форма инкремента. Постфиксный инкремент как правило реализуется через префиксный и временную переменную:
iterator operator++(int)
{
iterator _Tmp = *this;
++*this;
return (_Tmp);
}
В таком случае получаем создание временных объектов, которыми мы не пользуемся. Так что лучше сразу написать префиксную форму.

3. Сохранение вычисленного end(). Если написать условие в виде It != end(), то end() может вычисляться при каждой итерации заново. Поэтому лучше его прокэшировать.

Сеегодня нашел еще одну явную пессимизацию, которую я, оказывается, всегда использовал (WinAPI specific):
DC.FillSolidRect(..., WindowColor);
...
DC.SetBkMode(TRANSPARENT);
DC.SetTextColor(TextColor);
DC.DrawText(...);
То есть заполнял фон, затем писал на нем текст, используя "прозрачный" цвет фона при отрисовке текста.

А теперь подумаем о том, что сейчас у большинства пользователей Windows включен тот стандартный или ClearType антиалиасинг шрифтов. При такой отрисовке многие выводимые пиксели надписи должны отрисовываться с учетом их предыдущего цвета. DrawText() не знает, что мы до этого всю область окна зарисовали каким-то одним цветом. И при отрисовке текста в режиме TRANSPARENT будет считывать то, что уже отрисовано, накладывать на него текст... А ведь можно от этих мучений легко избавиться:
DC.FillSolidRect(..., WindowColor);
...
DC.SetBkMode(OPAQUE);
DC.SetBkColor(WindowColor);
DC.SetTextColor(TextColor);
DC.DrawText(...);

25 мая 2006

C# beats C++

Забавный текст в update history програмки Launchy:
This is the first C++ release. As I became frustrated with C#’s large .NET framework requirements and users lack of desire to install it, I decided to switch back to the faster language. This release is functionally and visually equivalent to the 0.5 C# release. The C# CVS version had some new features that I will soon add to the C++ branch.

Вобщем, Welcome back! ;)

Auto-sized editbox

При создании grid-а понадобилось автоматически расширять редактируемую ячейку (стандартный editbox) по высоте в зависимости от содержимого. A-la редактирование таблиц в Microsoft Word.

Загвоздка вся в том, что editbox - стандартный Windows control, в исходники которого залезть нельзя, а функциональность у него весьма ограниченная.

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

Что имеется у стандартного edit-а:
1. Область format rectangle, которая задает область редактирования. Как правило, чуть меньше чем client area. При изменении размера edit-а, эта format rectangle ставится как ей угодно. (Нет чтобы сделать format margins, которые не меняются...)
2. Сообщение EM_GETLINECOUNT - можно узнать текущее количество строк в контроле.
3. Уведомления EN_UPDATE и EN_CHANGE - можно отслеживать изменения содержимого контрола. EN_UPDATE приходит до прорисовки, EN_CHANGE - после.

При реализации сразу начались проблемы.

Проблема 1. Не приходит уведомление EN_UPDATE. Решилось просто:
EditBox.SendMessage(EM_SETEVENTMASK, 0, ENM_UPDATE);

Проблема 2. После изменения размера контрола происходит изменение format rectangle, приходится возвращать его на место:
EditBox.GetClientRect(ClientRect);
EditBox.SendMessage(EM_SETRECTNP, 0, (LPARAM)(&ClientRect));


Проблема 3. Самая большая проблема. Суть ее такова: необходимая нам высота client area равна КоличествоСтрок * ВысотаСтроки. Количество строк мы получить можем, а вот как высоту строки от edit control-а получить? Долго пытался использовать разумные значения, но никак не выходило правильной отрисовки во всех случаях. Перекопал весь Google.Groups, советов много, но ни один точно не соответсвует тому, как рисует текст editbox.

Магическая формула оказалась такая:
ТребуемаяВысота = КоличествоСтрок * TEXTMETRIC.tmHeight + 2

Нет, представляете - "+ 2". Что это за magic number я не знаю, но его я нашел где-то в ньюсгруппах.

Итого получилось:
void AdjustEditSize()
{
int nLines = (int) EditBox.SendMessage(EM_GETLINECOUNT);
// if (nLines == PrevLines) return;

CRect ClientRect, WinRect;
EditBox.GetClientRect(ClientRect);
EditBox.GetWindowRect(WinRect);
EditBox.SendMessage(EM_GETRECT, 0, (LPARAM)(&FormatRect));

CClientDC DC(EditBox);
DC.SelectFont((HFONT) EditBox.SendMessage(WM_GETFONT));

TEXTMETRIC tm;
DC.GetTextMetrics(&tm);
int LineHeight = tm.tmHeight;

MapWindowPoints(HWND_DESKTOP, EditBox.GetParent(), (LPPOINT)&WinRect, 2);
WinRect.bottom = WinRect.top
+ (WinRect.Height() - ClientRect.Height()) // adding non-client area
+ nLines * LineHeight + 2; // what da hell is "2" here?
EditBox.MoveWindow(WinRect);

// restore fucking format margins. set format rect = client area (i.e. margins=0)
EditBox.GetClientRect(ClientRect);
EditBox.SendMessage(EM_SETRECTNP, 0, (LPARAM)(&ClientRect));
}

15 мая 2006

throw()

Забавная дискуссия получилась по поводу спецификатора throw().

Начали с throw, закончили спором на счет оптимизации.

10 мая 2006

Scintilla

Копаясь в исходниках TortoiseSVN обнаружил хороший edit control, да еще с очень правильной лицензией.

PS. Ложка дегтя.

09 мая 2006

BitBlt does clipping. It does it bad.

Цитата из MSDN: "BitBlt does clipping on the destination DC".
Так вот при этом clipping-е BitBlt забывает про viewport, и режет как будто его нет.
Пришлось извратиться:
void RenderBuffer(HDC PaintDC, const RECT& SourceRect, const POINT& Destination)
{
// BitBlt does clipping on the destination DC.
// But BitBlt's clipping know nothing about view ports :(
// So, we need to set viewport to (0,0)
// and handle original viewport manually when calling BitBlt
POINT OriginalViewport;
::SetViewportOrgEx(PaintDC, 0, 0, &OriginalViewport);

// perform rendering
::BitBlt(PaintDC,
SourceRect.left,
SourceRect.top,
SourceRect.right - SourceRect.left,
SourceRect.bottom - SourceRect.top,
BufferDC,
Destination.x + OriginalViewport.x,
Destination.y + OriginalViewport.y,
SRCCOPY);

// restore original viewport
::SetViewportOrgEx(PaintDC, OriginalViewport.x, OriginalViewport.y, NULL);
}

06 мая 2006

WTL revisited

Вчера я было разочаровался в WTL. Причины были следующие:
1. У него нет класса CIcon. Пришлось хранить HICON и прибивать его (DeleteObject()) собственноручно в деструкторе объекта.
2. У класса CDC нет метода ReleaseDC(). Пришлось делать руками, через WinAPI:
int LogPixelsY;
{
CDCHandle ScreenDC(::GetDC(NULL));
LogPixelsY = GetDeviceCaps(ScreenDC, LOGPIXELSY);
::ReleaseDC(NULL, ScreenDC.Detach());
}

Сегодня почесав репу и пошерстив Гугл понял, что остал от жизни - вышел уже WTL 7.5, а у меня все 7.1 стоит. Скачал, поставил.

1. CIcon появился.
2. Понял, что CDC не предназначен для ReleaseDC(). Для него есть CClientDC:
int LogPixelsY;
{
CClientDC ScreenDC(NULL);
LogPixelsY = GetDeviceCaps(ScreenDC, LOGPIXELSY);
}

Жизнь налаживается...

05 мая 2006

Имя файла и его расширение

Почему-то boost::filesystem::path не понимает wide chars :(
Пришлось писать свои шаблоны:

template <typename StringType>
typename StringType::const_iterator FindFileName(const StringType& FilePath)
{
typename StringType::const_reverse_iterator rend(FilePath.rend()), rit(FilePath.rbegin());
for (; rit != rend; ++rit)
{
switch (*rit)
{
case '/':
case '\\':
return rit.base();
}
}

return FilePath.begin();
}

template <typename StringType>
typename StringType::const_iterator FindFileExtension(const StringType& FilePath)
{
typename StringType::const_reverse_iterator rend(FilePath.rend()), rit(FilePath.rbegin());
for (; rit != rend; ++rit)
{
switch (*rit)
{
case '/':
case '\\':
return FilePath.end();

case '.':
return rit.base();
}
}

return FilePath.end();
}

template <typename StringType>
StringType ExtractFileName(const StringType& FilePath)
{
return StringType(FindFileName(FilePath), FilePath.end());
}

template <typename StringType>
StringType ExtractFileExtension(const StringType& FilePath)
{
return StringType(FindFileExtension(FilePath), FilePath.end());
}

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

std::wstring Path = L"c:\\foo\\bar.xml";
std::wcout << L"file: " << ExtractFileName(Path) << L", ext: " << ExtractFileExtension(Path) << std::endl;


Используемо с basic_string, vector и другими контейнерами.
Понимает как char, так и wchar_t.

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.