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. Ложка дегтя.