Разноцветные окошки: виртуальный конструктор, CRTP и забористые шаблоны
С достаточно давних времён известен нетривиальный шаблон проектирования, когда производный класс передаётся в параметре базового:
template class Base
{
…
};
class Derived : public Base
{
…
};
Этот шаблон имеет своё собственное название — CRTP: Curiously Recurring Template Pattern, что переводится как «странно повторяющийся шаблон». Я же к этой и без того странной конструкции добавил ещё больше странностей: обобщил её на целую цепочку наследований. Да, это действительно можно сделать, но ради этого придётся отдать душу заплатить большую цену. Чтобы узнать, как это у меня получилось и какую цену придётся заплатить, за подробностями приглашаю читать дальше эту статью. Здесь мы будем заниматься страшными извращениями различными странными методами и прочими нехорошими вещами.
Сразу хочу предупредить: не воспринимайте описываемый здесь материал как что-то серьёзное. Уверен, что в 95–99% случаев вам всё это ни разу не пригодится на практике. Это — нечто вроде занимательной математики, разминки для ума. На практике вряд ли пригодится, но уделить этому время интересно. Только в данном случае в качестве математики выступает язык С++ и его возможности. Предупреждаю заранее, т.к. если вы ищете здесь чего-то серьёзного и практически ориентированного, вы можете разочароваться.
Ещё сразу настраивайтесь на экзотику, будто вы внезапно попали в страну, где две луны, три солнца, листья растений синие или сиреневые, да и вообще многие привычные вещи здесь какие-то… странные и необычные… Если вы погрязли в серых буднях и давно не читали чего-нибудь этакого, то вы пришли по адресу…
Разноцветные окошки
Это было очень давно. Почти три года назад. Я тогда сидел на тяжёлой траве только постигал дзен основы С++11/14 по книге Мейерс С. — «Эффективный и современный С++». В ней тоже встречается упоминание этого шаблона. После этого, как я почувствовал, что достиг просветления освоил основы нового стандарта и готов смотреть на старые вещи по-новому, я стал освежать в памяти книгу по Windows API: Щупак Ю. — «Win32 API. Эффективная разработка приложений». В самом начале в ней описывается минимальная программа на языке С для создания и вывода окна:
#include
HWND hMainWnd;
TCHAR szClassName[] = TEXT("MyClass");
MSG msg;
WNDCLASSEX *wc;
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
RECT rect;
switch(uMsg)
{
case WM_CREATE:
SetClassLongPtr(hWnd, -10, (LONG)CreateSolidBrush(RGB(200, 160, 255)));
break;
case WM_PAINT:
hDC = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rect);
DrawText(hDC, TEXT("Hello, world!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
break;
case WM_CLOSE:
DestroyWindow(hWnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
if(!(wc = new WNDCLASSEX))
{
MessageBox(NULL, TEXT("Ошибка выделения памяти!"), TEXT("Ошибка"), MB_OK | MB_ICONERROR);
return 0;
}
wc->cbSize = sizeof(WNDCLASSEX);
wc->style = CS_HREDRAW | CS_VREDRAW;
wc->lpfnWndProc = WndProc;
wc->cbClsExtra = 0;
wc->cbWndExtra = 0;
wc->hInstance = hInstance;
wc->hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc->hCursor = LoadCursor(NULL, IDC_ARROW);
wc->hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc->lpszMenuName = NULL;
wc->lpszClassName = szClassName;
wc->hIconSm = LoadIcon(NULL, IDI_APPLICATION);
//регистрируем класс окна
if(!RegisterClassEx(wc))
{
MessageBox(NULL, TEXT("Не удается зарегистрировать класс для окна!"), TEXT("Ошибка"), MB_OK | MB_ICONERROR);
return 0;
}
delete wc;
//создаём главное окно
hMainWnd = CreateWindow(szClassName, TEXT("A Hello1 Application"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, (HWND)NULL, (HMENU)NULL, (HINSTANCE)hInstance, NULL);
if(!hMainWnd)
{
MessageBox(NULL, TEXT("Не удается создать окно!"), TEXT("Ошибка"), MB_OK | MB_ICONERROR);
return 0;
}
//показываем наше окно
ShowWindow(hMainWnd, nCmdShow);
//UpdateWindow(hMainWnd);
//выполняем цикл обработки сообщений до закрытия приложения
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
//MessageBox(NULL, TEXT("Application is going to quit."), TEXT("Exit"), MB_OK);
return 0;
}
Я уже делал это много раз, выводя разные окошки по образцу этой книги. И внезапно задумался: я ж только буквально вчера читал про С++! Я ведь могу написать свой класс для вывода этого окна!
Сказано — сделано:
class WindowClass //класс окна Windows
{
//данные
HWND hWnd = NULL; //дескриптор класса окна
WNDCLASSEX wc = { 0 }; //структура для регистрации класса окна внутри Windows
const TCHAR *szWndTitle = nullptr; //заголовок окна
static const TCHAR *szWndTitleDefault; //строка заголовка по умолчанию
static List wndList; //статический список, единый для всех классов
//функции
static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); //оконная процедура (статическая функция)
bool CreateWnd(WNDCLASSEX& wc, bool bSkipClassRegister = false, const TCHAR *szWndTitle = nullptr); //инициализирует и создаёт окно (вызывается из конструкторов)
virtual void OnCreate(HWND hWnd); //обработка WM_CREATE внутри оконной процедуры
virtual void OnPaint(HWND hWnd); //обработка WM_PAINT внутри оконной процедуры
virtual void OnClose(HWND hWnd); //обработка WM_CLOSE внутри оконной процедуры
virtual void OnDestroy(HWND hWnd); //обработка WM_DESTROY внутри оконной процедуры
//привилегированные классы
friend List;
public:
//функции
WindowClass(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr); //конструктор для инициализации класса по умолчанию
WindowClass(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr); //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию
WindowClass(WindowClass&); //конструктор копирования
virtual ~WindowClass(); //виртуальный деструктор
};
Структура класса тривиальна: объявляются несколько конструкторов (с передачей как только основных параметров, так и ссылки на более подробно заполненную структуру WNDCLASSEX), функция CreateWnd собственно регистрации класса окна и создания окна, вызываемая из конструкторов, а также набор виртуальных функций-членов, выполняющих действия по обработке каждого из сообщений Windows внутри оконной процедуры обратного вызова.
Члены данные класса тоже минимальны: дескриптор окна hWnd; структура WNDCLASSEX, используемая при создании класса; и строка-заголовок окна.
Оконная процедура обратного вызова объявляется как static, чтобы избежать неявной передачи указателя this на объект класса и таким образом нарушить соглашение на тип (сигнатуру) функции оконной процедуры, принятой в Windows (вспоминаем, что эту функцию будет вызывать не мы сами, а Windows, потому параметры и возвращаемый тип этой функции строго заданы).
Оконная процедура и указатель this
Из С++ известно: если член-функция определяется как статическая, указатель на объект класса ей должен передаваться явно. Однако мы не можем передать статической оконной процедуре указатель на объект класса, поскольку формат этой функции не допускает эту передачу. В связи с этим возникает фундаментальная проблема: если имеется несколько объектов класса WindowClass, то как единственная статическая оконная процедура узнает, какому именно объекту класса пришло сообщение?
Выход один: нужно эту связь тем или иным способом установить.
Windows идентифицирует то или иное окно по его дескриптору HWND hWnd. Объект класса, соответствующий этому окну, можно идентифицировать по указателю на этот объект. Следовательно, необходимо установить связь hWnd указатель на объект WindowClass. Например, оконная процедура, будучи одновременно членом класса, могла бы иметь ссылку или указатель на некоторую тоже статическую структуру данных, устанавливающую связь между hWnd и указателем на объект для каждого окна и обновляемую при каждом создании объекта класса. Структура данных должна быть статической, чтобы, во-первых, к ней можно было получить доступ изнутри статической оконной процедуры, не имея указателя на любой объект класса, во-вторых, чтобы она была единственной для всех объектов класса (что логически вытекает из её назначения), и в третьих, чтобы она всё-таки была привязана к классу с соответствующим уровнем доступа, а не являлась некой внешней глобальной переменной.
Теперь, после выяснения того, как эту структуру описать и зачем она нужна, осталось выяснить, что должна представлять собой эта структура.
Можно объявить два динамических массива: один — для дескрипторов окон HWND, второй — для указателей на объекты WindowClass. Однако это не лучшее решение: неясно, каким выбрать размер массива, какие будут сценарии использования окон, не окажутся ли массивы почти пустующими при неверном выборе их размера, что вызовет перерасход памяти. Либо, наоборот, когда при создании окон их объем исчерпается, потребуется увеличивать их размеры и т.п.
Более лучшим (и даже я бы сказал — идеальным) решением в этой ситуации является список (список! ). Список — это динамическая структура данных, состоящая из набора связанных попарно узлов. Каждый узел (в случае двусвязного списка) имеет указатели на предыдущий и следующий узлы списка, а также дополнительные хранимые данные. В нашей ситуации каждому узлу списка соответствует каждое из окон, а полезные данные — это дескриптор окна и указатель на объект класса WindowClass.
Таким образом, при каждом создании нового окна создаётся новый узел списка и добавляется в его конец (становится последним). При закрытии — узел удаляется, а указатели предыдущего и следующего узлов настраиваются друг на друга, чтобы заместить удалённый узел. При этом нет никакого перерасхода памяти — создаётся ровно столько узлов, сколько создано окон, и удаляются они также одновременно с закрытием окна.
Следовательно, в класс WindowClass следует добавить также новый статический член:
static List wndList; //статический список, единый для всех классов
и объявить его привилегированным, чтобы дать возможность ему обращаться к членам WindowClass:
friend List;
(Я не буду здесь сейчас давать определение класса списка и узла, их функций, поскольку это не относится непосредственно к классу WindowClass, а логика реализации этого класса известна и достаточно тривиальна.)
Таким образом, оконная процедура при поступлении нового сообщения в случае, если оно принадлежит к числу обрабатываемых ею, по переданному ей из Windows дескриптору окна hWnd обращается к списку, выполняет в нём поиск узла по заданному hWnd и, найдя, получает требуемый указатель на объект класса WindowClass. Затем вызывает по указателю виртуальную функцию, соответствующую обрабатываемому сообщению: у переопределённого класса виртуальная функция с тем же именем может выполнять другие действия.
LRESULT CALLBACK WindowClass::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
//оконная процедура
ListElement * pListElem = nullptr;
switch (uMsg)
{
case WM_CREATE:
{
//lParam содержит указатель на структуру типа CREATESTRUCT, содержающую помимо всего прочего указатель на объект класса WindowClass, который нам
//нужен (см. функцию WindowClass::CreateWnd)
CREATESTRUCT *cs = reinterpret_cast(lParam);
WindowClass *p_wndClass = reinterpret_cast(cs->lpCreateParams);
p_wndClass->hWnd = hWnd; //инициализируем hWnd объекта класса значением, переданным в оконную процедуру
//заносим созданное окно в список
pListElem = wndList.add(p_wndClass);
if (pListElem)
pListElem->p_wndClass->OnCreate(hWnd); //вызываем виртуальную функцию, соответствующую данному дескриптору
}
break;
case WM_PAINT:
pListElem = wndList.search(hWnd); //ищем в списке объект класса по заданному дескриптору окна
if (pListElem)
pListElem->p_wndClass->OnPaint(hWnd); //вызываем виртуальную функцию, соответствующую данному дескриптору
break;
case WM_CLOSE:
pListElem = wndList.search(hWnd); //ищем в списке объект класса по заданному дескриптору окна
if (pListElem)
pListElem->p_wndClass->OnClose(hWnd); //вызываем виртуальную функцию, соответствующую данному дескриптору
break;
case WM_DESTROY:
pListElem = wndList.search(hWnd); //ищем в списке объект класса по заданному дескриптору окна
if (pListElem)
pListElem->p_wndClass->OnDestroy(hWnd); //вызываем виртуальную функцию, соответствующую данному дескриптору
break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return 0;
}
Здесь есть один тонкий момент. Он касается инициализации класса и обработки сообщения WM_CREATE.
При создании окна функцией CreateWindow, на момент её вызова, дескриптор окна hWnd ещё не известен: окно ведь ещё не создано! Следовательно, чтобы иметь возможность вызывать виртуальную OnCreate, нужно знать указатель на объект класса. Делается это довольно рискованной передачей указателя this из функции WindowClass: CreateWnd в функцию CreateWindow через указатель lParam. Оконная процедура при обработке WM_CREATE получает из параметра этот указатель, с его помощью инициализирует внутри объекта член hWnd, а затем создаёт новый узел списка для данного окна по указателю на объект класса. После чего вызывает виртуальную OnCreate по указателю.
Для остальных же сообщений выполняется описанная выше логика: поиск узла списка по текущему переданному из Windows дескриптору окна hWnd, а затем вызов нужной виртуальной функции по указателю на объект класса из узла списка.
Скомпилировав программу и убедившись, что всё работает правильно, я, потирая руки от чувства собственного величия от проделанной работы, принялся читать дальше. А там на следующей же странице указывается функция изменения свойств окна:
DWORD SetClassLong(HWND hWnd, int nIndex, LONG dwNewLong);
Я тут же на месте решил создать новое окно на основе старого:
class WindowClassDerived : public WindowClass //построение нового класса с другой логикой работы на основе старого
{
static unsigned short int usiWndNum; //количество объектов класса
public:
WindowClassDerived(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr); //конструктор для инициализации класса по умолчанию
WindowClassDerived(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr); //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию
WindowClassDerived(WindowClassDerived&); //конструктор копирования
virtual ~WindowClassDerived() override; //виртуальный деструктор
virtual void OnCreate(HWND hWnd) override; //обеспечивает обработку WM_CREATE внутри оконной процедуры
virtual void OnPaint(HWND hWnd) override; //обеспечивает обработку WM_PAINT внутри оконной процедуры
virtual void OnDestroy(HWND hWnd) override; //обеспечивает обработку WM_DESTROY внутри оконной процедуры
};
Производный класс отличается от базового добавлением статического счётчика окон, а также изменением OnCreate, OnPaint и OnDestroy: функция OnCreate меняет цвет фона окна, OnPaint выводит другое сообщение, а OnDestroy уменьшает статический счётчик окон. Всё очень просто и понятно. Собрал и запустил. Текст сообщения стал другим…
…а цвет окна не изменился.
Виртуальный конструктор
Я тогда ещё понял, что уже ступил на тонкий лёд. Не все нюансы описаны в базовом материале основных книг. Одна из таких — виртуальный конструктор. Я думал, что вызову из конструктора виртуальную функцию производного класса точно так же, как и всюду в других частях программы. Выяснилось, что этого сделать нельзя.
Проблема заключается в том, что виртуальная функция, вызываемая из конструктора, вызывается как не виртуальная: создан только объект базового класса, и то не до конца, а объект производного ещё не создан, и таблица виртуальных функций не сформирована. В нашем случае получается цепочка: конструктор производного → конструктор базового → CreateWnd → CreateWindow → оконная процедура → OnCreate, то есть OnCreate вызывается действительно из конструктора. Производный объект ещё не создан, следовательно, вызывается OnCreate для базового класса! Её переопределение в производном, получается, не имеет смысла! Что же делать?
Из С++ известно, что любую переопределённую функцию можно вызвать по её полному имени: имя_класса: имя_функции. Имя класса — это не просто имя: оно идентифицирует собой, фактически, тип объекта. Также из С++ известно, что класс (и функцию) можно сделать шаблонным (шаблонной), передавая ему (ей) тип в качестве параметра. Следовательно, если функцию оконной процедуры сделать шаблонной и передать ей каким-нибудь образом тип производного класса, можно добиться вызова нужной переопределённой функции напрямую в конструкторе базового класса.
Стоп-стоп-стоп!!! Так же делать нельзя!!! Производный класс ещё не создан, его данные не инициализированы: какие функции ты тут собрался вызывать?
Если нельзя, но очень хочется, то можно. Конечно, я не нацеливался на полноценное обращение к производному классу. Я имел ввиду, чтобы вызвать совершенно стороннюю функцию WinAPI, которая не имеет никакого отношения к классу. «Но это ведь можно сделать совершенно другими способами, и гораздо проще!» — скажете вы. Да. Можно. И я напишу об этом в конце статьи. Но в тот момент я отбросил всё это в сторону и сосредоточился на чисто технической стороне вопроса:, а всё-таки, можно ли в принципе в конструкторе базового класса вызывать что-нибудь из производного? Это был чисто спортивный интерес, если хотите. О какой-либо практической стороне я в тот момент не думал. Это была нетривиальная задача, и мне стало интересно, смогу ли я её решить.
Шаблонный класс окна — способ 1
Итак, возникает сложность: как передать оконной процедуре тип производного класса?
Делать весь базовый класс WindowClass шаблонным я сразу не хотел: для каждого производного класса будет генерироваться свой собственный базовый. Кроме того, поскольку WindowClass станет шаблонным, то и узлы списка, и сам список тоже придётся делать шаблонными: они имеют указатели на объекты класса, а чтобы пользоваться этими указателями, они должны знать их тип, то есть WindowClass и то, чем он будет параметризован. На момент определения класса списка и узла это неизвестно, следовательно, этот тип тоже необходимо передавать как параметр (из WindowClass). Отсюда вытекает, что для каждого производного класса будет создаваться свой собственный список, соответствующий этому производному классу (и только ему)! Да и указатели теперь на базовые классы, соответствующие разным производным, в один массив не засунешь: у них типы разные.
Поэтому я стал искать способ всё же передать тип производного класса, не параметризуя весь класс целиком. Тип базовому классу можно передать только через конструктор: это единственная функция, к которой происходит обращение при создании объекта. Следовательно, она должна быть шаблонной. Однако выяснилось, что указать параметры шаблона ей явно нельзя: это будет выглядеть так же, как передача параметров самому шаблонному классу, а не его конструктору. Поэтому тип может быть только выведен из переданных конструктору параметров. Но добавлять специальный параметр конструктора, служащий только для выведения типа, я тоже не хотел: загромождение списка аргументов чисто служебным параметром. А если пользователь забудет его передать, например, посредством хотя бы банального (DerivedClass *)nullptr? Это ещё не страшно — компилятор выведет сообщение об ошибке, что не может инстанцировать класс. Хуже, если пользователь создаст иерархию классов и передаст указатель не того производного класса: всё будет с точки зрения компиляции верно, однако получим неверно работающую программу с непонятной ошибкой.
Короче, это просчёт проектирования — такое решение. Таким образом перекладывается ответственность за правильное инстанцирование даже не на создателя производного класса, а на того, кто будет им пользоваться! А тот может быть ни сном, ни духом относительно таких нюансов и искренне не понимать, где находится ошибка.
В конечном итоге, сдавшись, я решил всё же, не меняя параметров конструктора, параметризовать всё же сам WindowClass и заодно с ним связанные классы списка и узла списка.
Шаблонный класс WindowClass:
template struct ListElement //узел списка
{
//данные узла
HWND hWnd; //дескриптор окна Windows
WindowClass *p_wndClass; //указатель на объект класса WindowClass
ListElement *pNext; //указатель на следующий элемент списка
ListElement *pPrev; //указатель на предыдущий элемент списка
};
template class WindowClass //класс окна Windows
{
using WndProcCallback = LRESULT (*)(HWND, UINT, WPARAM, LPARAM); //тип функции оконной процедуры
protected: //изменение для производных классов!
//данные
HWND hWnd = NULL; //дескриптор класса окна
WNDCLASSEX wc = { 0 }; //структура для регистрации класса окна внутри Windows
const TCHAR *szWndTitle = nullptr; //заголовок окна
static const TCHAR *szWndTitleDefault; //строка заголовка по умолчанию
static List wndList; //статический список, единый для всех классов
//функции
bool CreateWnd(WNDCLASSEX& wc, bool bSkipClassRegister = false, const TCHAR *szWndTitle = nullptr); //инициализирует и создаёт окно (вызывается из конструкторов)
static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); //оконная процедура (статическая функция)
template void LaunchOnCreate(HWND hWnd, T *p_wndClass) //ошибка! см. проект FirstWin32CPP_DerivedTemplate2
{
//выполняет запуск OnCreate для класса WndCls, если OnCreate определена в нём
T::OnCreate(hWnd);
}
template void LaunchOnCreate(HWND hWnd, T *p_wndClass) //выполняет запуск OnCreate с помощью механизма виртуальных функций по указателю на класс
{
p_wndClass->OnCreate(hWnd); //запуск с помощью механизма виртуальных функций
}
void OnCreate(HWND hWnd); //обеспечивает обработку WM_CREATE внутри оконной процедуры
virtual void OnPaint(HWND hWnd); //обеспечивает обработку WM_PAINT внутри оконной процедуры
virtual void OnClose(HWND hWnd); //обеспечивает обработку WM_CLOSE внутри оконной процедуры
virtual void OnDestroy(HWND hWnd); //обеспечивает обработку WM_DESTROY внутри оконной процедуры
//привилегированные классы
friend List;
public:
//функции
WindowClass(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr); //конструктор для инициализации класса по умолчанию
WindowClass(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr); //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию
WindowClass(WindowClass&); //конструктор копирования
virtual ~WindowClass(); //виртуальный деструктор
};
Производный класс:
class WindowClassDerived : public WindowClass
{
static unsigned short int usiWndNum; //количество объектов класса
public:
WindowClassDerived(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr);
WindowClassDerived(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr);
WindowClassDerived(WindowClassDerived&); //конструктор копирования
virtual ~WindowClassDerived() override; //виртуальный деструктор
void OnCreate(HWND hWnd); //обеспечивает обработку WM_CREATE внутри оконной процедуры
virtual void OnPaint(HWND hWnd) override; //обеспечивает обработку WM_PAINT внутри оконной процедуры
virtual void OnDestroy(HWND hWnd) override; //обеспечивает обработку WM_DESTROY внутри оконной процедуры
};
Оконная процедура, будучи шаблонным членом шаблонного класса и имея доступ к переданному типу производного класса, вызывает OnCreate производного класса.
Вот мы и приходим естественным образом к шаблону CRTP. Здесь он получился сам собой. Только много позже я узнал, что эта конструкция — известный шаблон с соответствующим именем. Но тогда я этого не знал, и мне казалось, что я получил его впервые.
Уже сразу я понял, что это — только половина решения. Я ведь легко могу захотеть создать ещё один класс на основе этого производного. А всё: он — не шаблонный и больше не принимает никаких параметров. Так я пришёл к идее передачи второго производного класса через первый производный в базовый. (Тонкий лёд под моими ногами стал давать трещину… Я уже шёл туда, откуда нет возврата.) Но если я сделаю это один раз, я смогу делать так сколько угодно: даже если у меня будет десять производных классов, я смогу десятый по счёту (самый последний) передать по цепочке в базовый, и он вызовет там нужную мне функцию этого последнего производного (а вообще говоря — и любого промежуточного при желании). Задача была ясна. Оставалось только это сделать.
Параметризированный класс окна — способ 2
На втором заходе я поставил себе три задачи:
- всё-таки избежать параметризации базового класса целиком;
- обеспечить возможность повторного наследования;
- сохранить параметры конструктора прежними, без служебных спецпараметров.
Разумеется, для соблюдения указанных требований шаблонным придётся всё-таки сделать конструктор и всё-таки добавить в него спецпараметр. Однако это означает нарушение другого требования.
Какой здесь выход?
Можно разделить исходный базовый класс WindowClass на две составляющие: сам WindowClass (назовём его теперь WindowClassBase), представляющий собой единую незыблемую основу, и дополняющий его производный класс (который можно назвать всё тем же первоначальным именем WindowClass).
Дополняющий класс отвечает за реализацию OnCreate, и, кроме того, его можно параметризировать самого целиком. А он в своём конструкторе паредаст переданный ему тип через спецпараметр в конструктор класса WindowClassBase.
В любом случае, в WindowClassBase относительно исходного теперь придётся внести некоторые изменения. Во-первых, помимо собственно удаления из него OnCreate придётся добавить член-указатель на дополняющий его класс (и, в будущем, производные от него), а также функцию вызова, вызывающую OnCreate по этому указателю: мы не можем вызвать по указателю на базовый, потому что OnCreate в нём уже нет, а OnCreate дополняющего и производных от него классов лучше всё же вызывать по правильному указателю на нужный класс, а не пытаться что-то нахимичить с указателем this базового. В конечном итоге, спецпараметр конструктора WindowClassBase будет нужен не только для вывода типа, но и для сохранения с последующим обращением через него к OnCreate нужного класса.
К сожалению, тип этого указателя пришлось сделать void:
- класс не шаблонный, и указать компилятору создать указатель с неизвестным типом нельзя;
- от базового класса наследуются множество производных, у всех них разный тип — какой тип указателя использовать?
В конечном итоге я просто объявил его в стиле С: в любой непонятной ситуации используй указатель на void. Указатель физически хранится как на бестиповый, но в момент вызова OnCreate приводится к типу вызываемого класса. Делается это в специальной шаблонной функции вызова, которая принадлежит WindowClassBase и тип-параметр которой на момент вызова известен:
template void LaunchOnCreate(HWND hWnd)
{
//выполняет запуск OnCreate для класса WndCls, если OnCreate определена в нём
if (p_drvWndCls)
(static_cast(p_drvWndCls))->WndCls::OnCreate(hWnd);
}
(Первоначально в качестве второго параметра применялся std: true_type или std: false_type для выбора нужного варианта переопределения функции. Используя метод SFINAE, выяснялось на этапе компиляции, имеет ли класс WndCls функцию-член OnCreate. Если имеет, то вызывается вышеприведённый вариант функции. Если не имеет, то обращение к OnCreate производилось в виде:
(static_cast(p_drvWndCls))->OnCreate(hWnd);
Впоследствии выяснилось, что в SFINAE нет необходимости: класс, дополняющий WindowClassBase, в любом случае имеет функцию-член OnCreate, потому, даже если переданный класс-параметр WndCls не имеет определённой в нём OnCreate, она есть в одном из базовых по отношению к нему классов, и проверка даст true во всех случаях. Если же каким-то чудом дополняющий класс будет изменён так, что OnCreate будет из него удалена, и во всех производных от него классах её тоже не будет, то тогда нет никакого смысла вызывать её по второму варианту: такой код просто компилироваться не будет. Потому в конечном итоге здесь приведён вышеприведённый вариант.)
Логика приёма и использования типа базового класса в WindowClassBase достаточно проста: тип выводится из указателя на объект производного класса, передаваемый конструктору WindowClassBase, в этом конструкторе этот указатель сохраняется, а переданным типом инстанцируется указатель на шаблонную оконную процедуру, а из неё происходит обращение к вышеуказанной LaunchOnCreate.
Таким образом, класс WindowClassBase примет теперь такой вид:
class WindowClassBase //класс окна Windows
{
protected: //изменение для производных классов!
//данные
HWND hWnd = NULL; //дескриптор класса окна
WNDCLASSEX wc = { 0 }; //структура для регистрации класса окна внутри Windows
const TCHAR *szWndTitle = nullptr; //заголовок окна
void *p_drvWndCls; //указатель на производный класс, дополняющий этот основной (т.к. шаблонные данные-члены допустимы только
//статические, то используем (по старинке) указатель без типа, т.е. указатель на void
static const TCHAR *szWndTitleDefault; //строка заголовка по умолчанию
static List wndList; //статический список, единый для всех классов
//функции
bool CreateWnd(WNDCLASSEX& wc, bool bSkipClassRegister = false, const TCHAR *szWndTitle = nullptr); //инициализирует и создаёт окно (вызывается из конструкторов)
template void LaunchOnCreate(HWND hWnd)
{
//выполняет запуск OnCreate для класса WndCls
if (p_drvWndCls)
(static_cast(p_drvWndCls))->WndCls::OnCreate(hWnd);
}
virtual void OnPaint(HWND hWnd); //обеспечивает обработку WM_PAINT внутри оконной процедуры
virtual void OnClose(HWND hWnd); //обеспечивает обработку WM_CLOSE внутри оконной процедуры
virtual void OnDestroy(HWND hWnd); //обеспечивает обработку WM_DESTROY внутри оконной процедуры
//привилегированные классы и функции
friend List;
template friend LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); //оконная процедура
public:
//функции
template WindowClassBase(WndCls *p_wndClass, HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr); //конструктор для инициализации класса по умолчанию
template WindowClassBase(WndCls *p_wndClass, WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr); //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию
WindowClassBase(WindowClassBase&); //конструктор копирования
virtual ~WindowClassBase(); //виртуальный деструктор
};
Ну и приведу код самого короткого конструктора:
template WindowClassBase::WindowClassBase(WndCls *p_wndClass, WNDCLASSEX& wc, const TCHAR *szWndTitle)
{
//создаём окно, инициализируя его параметрами, переданными через wc
//на вход: p_wndClass - указатель на производный класс, по типу которого будет выводиться тип шаблонного конструктора, wc - ссылка на структуру класса
//окна для регистрации внутри Windows, szWndTitle - строка заголовка окна
WindowClassBase::wc = wc;
WindowClassBase::wc.lpfnWndProc = WndProc;
WindowClassBase::szWndTitle = szWndTitle;
p_drvWndCls = p_wndClass; //сохраняем указатель на производный класс, чтобы вызывать OnCreate() этого класса при обработке сообщения WM_CREATE
//создаём окно
CreateWnd(WindowClassBase::wc, false, szWndTitle);
}
Внутри же оконной процедуры обращение к LaunchOnCreate происходит так:
p_wndClass->LaunchOnCreate(hWnd);
Саму оконную процедуру решил вынести из класса вовне, объявив её привилегированной в классе WindowClassBase. Возможно, в этом не имело особого смысла: какая разница, где плодить её инстанцирования — вовне или внутри класса? Сегмент кода-то один! Хотя, признаю, с точки зрения той же инкапсуляции, возможно, следовало всё же оставить её внутри класса статической.
Осталось определить дополняющий класс:
class WindowClass : public WindowClassBase //класс, дополняющий WindowClassBase до полноценно функционирующего класса
{
public:
//конструктор для инициализации класса по умолчанию
WindowClass(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(this, hInstance, szClassName, szWndTitle) {}
//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию
WindowClass(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(this, wc, szWndTitle) {}
virtual void OnCreate(HWND hWnd) {} //обеспечивает обработку WM_CREATE внутри оконной процедуры
};
Класс имеет конструктор, имеющий такой же вид, как и у исходного WindowClass до разделения, то есть без спецпараметра, а этот спецпараметр генерируется внутри при обращении к конструктору WindowClassBase передачей указателя this.
Этот WindowClass в такой форме — это практически эквивалент исходного WindowClass. В таком виде он не поддерживает наследование с переопределением OnCreate. Тем не менее, это — исходная отправная точка для поддержки наследования (как будет показано ниже). В таком виде:
- базовый класс WindowClassBase не является шаблонным сам по себе, а это значит, что он будет единственным для всех производных классов, какие бы они ни были; список List для обеспечения корректной обработки всех остальных сообщений Windows также будет единственным;
- конструктор WindowClass не имеет лишнего спецпараметра.
Как видим, два требования из трёх удовлетворены. Осталось разобраться с последним: с наследованием.
Цепочечная передача типа производного класса в WindowClassBase, контрольный тип
Рассмотрим для начала однократное наследование, когда логика инициализации WindowClass нас не устраивает, и мы хотим изменить её через создание производного класса (пока хотя бы одного). Что нужно изменить в WindowClass для обеспечения этого?
Новый вариант дополняющего класса становится шаблонным. Это не страшно, поскольку он фактически не содержит никаких данных, а только функцию OnCreate и конструкторы:
template class WindowClassTemplate : public WindowClassBase
{
public:
//конструктор для инициализации класса по умолчанию
WindowClassTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast(this), hInstance, szClassName, szWndTitle) {}
//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию
WindowClassTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast(this), wc, szWndTitle) {}
virtual void OnCreate(HWND hWnd) {} //обеспечивает обработку WM_CREATE внутри оконной процедуры
};
Этот класс принимает параметр типа DerWndCls и, преобразуя к нему указатель this, передаёт в WindowClassBase.
Обратите внимание на static_cast. Это важно, потому что первоначально у меня преобразование было написано в стиле С так:
template class WindowClassTemplate : public WindowClassBase
{
public:
//конструктор для инициализации класса по умолчанию
WindowClassTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase((DerWndCls *)this, hInstance, szClassName, szWndTitle) {}
//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию
WindowClassTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase((DerWndCls *)this, wc, szWndTitle) {}
virtual void OnCreate(HWND hWnd) {} //обеспечивает обработку WM_CREATE внутри оконной процедуры
};
После того, как я перевёл его всюду на static_cast, половина кода (см. далее) не скомпилировалась.
Это тоже тонкий момент: преобразование выполняется на стадии компиляции, но этот класс уже сам по себе имеет функцию OnCreate, а после преобразования к DerWndCls можно обратиться к OnCreate уже класса DerWndCls. В этом разница от описанного выше случая преобразования внутри WindowClassBase.
Таким образом, можно создать некий класс WindowClassDerived, в нём переопределить OnCreate и инстанцировать им описанный выше WindowClassTemplate, снова реализуя тот самый указанный в начале статьи исходный странно повторяющийся шаблон:
class WindowClassDerived : public WindowClassTemplate
{
static unsigned short int usiWndNum; //количество объектов класса
public:
WindowClassDerived(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr); //конструктор для инициализации класса по умолчанию
WindowClassDerived(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr); //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию
WindowClassDerived(WindowClassDerived&); //конструктор копирования
virtual ~WindowClassDerived() override; //виртуальный деструктор
virtual void OnCreate(HWND hWnd) override; //обеспечивает обработку WM_CREATE внутри оконной процедуры
virtual void OnPaint(HWND hWnd) override; //обеспечивает обработку WM_PAINT внутри оконной процедуры
virtual void OnDestroy(HWND hWnd) override; //обеспечивает обработку WM_DESTROY внутри оконной процедуры
};
И OnCreate этого WindowClassDerived будет вызываться внутри WindowClassBase, что и требовалось!
WindowClassDerived::WindowClassDerived(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle) : WindowClassTemplate(hInstance, szClassName, szWndTitle)
{
usiWndNum++; //увеличиваем количество объектов данного класса
}
Но это — однократное наследование. При многократном наследовании следует вместо WindowClassDerived, в свою очередь, объявить новый шаблон, потенциально принимающий класс уровнем выше в иерархии и передающий его в WindowClassTemplate. Конкретно выделю два ключевых момента:
- Потенциально принимающий класс уровнем выше в иерархии. Это означает, что может и не принимать никакого класса, то есть сам быть тем самым верхним классом иерархии так, чтобы из него можно было создать объект.© Habrahabr.ru