Хабрахабр

Разноцветные окошки: виртуальный конструктор, CRTP и забористые шаблоны

С достаточно давних времён известен нетривиальный шаблон проектирования, когда производный класс передаётся в параметре базового:

template<class T> class Base{ …}; class Derived : public Base<Derived>{ …};

Этот шаблон имеет своё собственное название – CRTP: Curiously Recurring Template Pattern, что переводится как «странно повторяющийся шаблон». Я же к этой и без того странной конструкции добавил ещё больше странностей: обобщил её на целую цепочку наследований. Да, это действительно можно сделать, но ради этого придётся отдать душу заплатить большую цену. Чтобы узнать, как это у меня получилось и какую цену придётся заплатить, за подробностями приглашаю читать дальше эту статью. Здесь мы будем заниматься страшными извращениями различными странными методами и прочими нехорошими вещами.

Сразу хочу предупредить: не воспринимайте описываемый здесь материал как что-то серьёзное. Уверен, что в 95-99% случаев вам всё это ни разу не пригодится на практике. Это – нечто вроде занимательной математики, разминки для ума. На практике вряд ли пригодится, но уделить этому время интересно. Только в данном случае в качестве математики выступает язык С++ и его возможности. Предупреждаю заранее, т.к. если вы ищете здесь чего-то серьёзного и практически ориентированного, вы можете разочароваться.

Ещё сразу настраивайтесь на экзотику, будто вы внезапно попали в страну, где две луны, три солнца, листья растений синие или сиреневые, да и вообще многие привычные вещи здесь какие-то… странные и необычные… Если вы погрязли в серых буднях и давно не читали чего-нибудь этакого, то вы пришли по адресу…

Разноцветные окошки

Это было очень давно. Почти три года назад. Я тогда сидел на тяжёлой траве только постигал дзен основы С++11/14 по книге Мейерс С. – «Эффективный и современный С++». В ней тоже встречается упоминание этого шаблона. После этого, как я почувствовал, что достиг просветления освоил основы нового стандарта и готов смотреть на старые вещи по-новому, я стал освежать в памяти книгу по Windows API: Щупак Ю. – «Win32 API.
Эффективная разработка приложений». В самом начале в ней описывается минимальная программа на языке С для создания и вывода окна:

#include <Windows.h> 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<CREATESTRUCT *>(lParam); WindowClass *p_wndClass = reinterpret_cast<WindowClass *>(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<class WndCls> struct ListElement //узел списка{ //данные узла HWND hWnd; //дескриптор окна Windows WindowClass<WndCls> *p_wndClass; //указатель на объект класса WindowClass ListElement *pNext; //указатель на следующий элемент списка ListElement *pPrev; //указатель на предыдущий элемент списка}; template<class WndCls> 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<WndCls> 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<class T, typename = T::OnCreate> void LaunchOnCreate(HWND hWnd, T *p_wndClass) //ошибка! см. проект FirstWin32CPP_DerivedTemplate2 { //выполняет запуск OnCreate для класса WndCls, если OnCreate определена в нём T::OnCreate(hWnd); } template<class T> 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<WndCls>; 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<WindowClassDerived>{ 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<class WndCls> void LaunchOnCreate(HWND hWnd){ //выполняет запуск OnCreate для класса WndCls, если OnCreate определена в нём if (p_drvWndCls) (static_cast<WndCls *>(p_drvWndCls))->WndCls::OnCreate(hWnd);}

(Первоначально в качестве второго параметра применялся std::true_type или std::false_type для выбора нужного варианта переопределения функции. Используя метод SFINAE, выяснялось на этапе компиляции, имеет ли класс WndCls функцию-член OnCreate. Если имеет, то вызывается вышеприведённый вариант функции. Если не имеет, то обращение к OnCreate производилось в виде:

(static_cast<WndCls *>(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<class WndCls> void LaunchOnCreate(HWND hWnd) { //выполняет запуск OnCreate для класса WndCls if (p_drvWndCls) (static_cast<WndCls *>(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<class WndCls> friend LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); //оконная процедура public: //функции template<class WndCls> WindowClassBase(WndCls *p_wndClass, HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr); //конструктор для инициализации класса по умолчанию template<class WndCls> WindowClassBase(WndCls *p_wndClass, WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr); //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию WindowClassBase(WindowClassBase&); //конструктор копирования virtual ~WindowClassBase(); //виртуальный деструктор};

Ну и приведу код самого короткого конструктора:

template<class WndCls> WindowClassBase::WindowClassBase(WndCls *p_wndClass, WNDCLASSEX& wc, const TCHAR *szWndTitle){ //создаём окно, инициализируя его параметрами, переданными через wc //на вход: p_wndClass - указатель на производный класс, по типу которого будет выводиться тип шаблонного конструктора, wc - ссылка на структуру класса //окна для регистрации внутри Windows, szWndTitle - строка заголовка окна WindowClassBase::wc = wc; WindowClassBase::wc.lpfnWndProc = WndProc<WndCls>; WindowClassBase::szWndTitle = szWndTitle; p_drvWndCls = p_wndClass; //сохраняем указатель на производный класс, чтобы вызывать OnCreate() этого класса при обработке сообщения WM_CREATE //создаём окно CreateWnd(WindowClassBase::wc, false, szWndTitle);}

Внутри же оконной процедуры обращение к LaunchOnCreate происходит так:

p_wndClass->LaunchOnCreate<WndCls>(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 DerWndCls> class WindowClassTemplate : public WindowClassBase{public: //конструктор для инициализации класса по умолчанию WindowClassTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerWndCls *>(this), hInstance, szClassName, szWndTitle) {} //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию WindowClassTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerWndCls *>(this), wc, szWndTitle) {} virtual void OnCreate(HWND hWnd) {} //обеспечивает обработку WM_CREATE внутри оконной процедуры};

Этот класс принимает параметр типа DerWndCls и, преобразуя к нему указатель this, передаёт в WindowClassBase.

Обратите внимание на static_cast. Это важно, потому что первоначально у меня преобразование было написано в стиле С так:

template<class DerWndCls> 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<WindowClassDerived>{ 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. Конкретно выделю два ключевых момента:

  1. Потенциально принимающий класс уровнем выше в иерархии. Это означает, что может и не принимать никакого класса, то есть сам быть тем самым верхним классом иерархии так, чтобы из него можно было создать объект.
  2. Передающий параметр в WindowClassTemplate. Это означает, что принятый аргумент шаблона необходимо передать дальше от класса к классу через всю цепочку наследований в самый низ, в WindowClassTemplate и оттуда в WindowClassBase.

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

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

Как реализовать эту функциональность?

Для решения проблемы автоматизации и интеллектуального принятия решения само собой напрашивается вариант аргумента по умолчанию для шаблона: если текущий создаваемый класс – самый верхний, и ему не передаётся параметр шаблона, то мы должны этот параметр ему назначить. Выполняется это с помощью аргумента по умолчанию. Тогда возникают следующие вопросы: каким его выбрать и как соотнести с ситуацией переданного явно параметра, а также передачи себя в случае, если параметр не передан?

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

template<class DerWndCls = WindowClassDerived<>> class WindowClassDerived : public WindowClassTemplate<DerWndCls>

Он сообщает, что рекурсивная зависимость типа слишком сложна.

Зайдём с другой стороны. Введём некий фиктивный класс, ничего функционально не выполняющий и ничего не хранящий, играющий роль лишь пустышки-затычки и сигнализирующий компилятору, что в случае его появления не происходит передача ничего «сверху»:

class thisclass {}; //класс-пустышка, используемый для аргумента по умолчанию

И в аргументе по умолчанию вместо себя подставим эту затычку:

template<class DerWndCls = thisclass> class WindowClassDerived : public WindowClassTemplate<DerWndCls>

При таком варианте в ситуации с аргументом по умолчанию происходит передача thisclass в WindowClassTemplate. Класс thisclass не имеет функции-члена OnCreate, так что такой вариант просто не скомпилируется.

Попробуем тогда ввести второй, вспомогательный контрольный параметр, на основании которого будем принимать решение, какой тип передавать дальше. Для этого, разумеется, нужно изменить WindowClassTemplate, например, так:

template<class DerWndCls, class ControlType> class WindowClassControlBaseTemplate : public WindowClassBase{ //если передаётся ControlType == thisclass, то тогда нужно использовать сам DerWndCls, в котором передаётся класс, передаваемый напрямую WindowClassBase //если же ControlType != thisclass, тогда следует использовать ControlType, эквивалентный классу в вершине иерархии наследования класса (при правильно //соблюдённых соглашениях о передаче ControlType вершинным и нижележащими базовыми классами) using DerivedWndClassType = std::conditional_t<std::is_same<ControlType, thisclass>::value, DerWndCls, ControlType>; public: //конструктор для инициализации класса по умолчанию WindowClassControlBaseTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerivedWndClassType *>(this), hInstance, szClassName, szWndTitle) {} //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию WindowClassControlBaseTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerivedWndClassType *>(this), wc, szWndTitle) {} virtual void OnCreate(HWND hWnd) {} //обеспечивает обработку WM_CREATE внутри оконной процедуры};

В него передаётся не один тип, а два. На основании комбинации этих двух типов определяется конечный тип с помощью средств : std::conditional_t и std::is_same. Именно этот тип и передаётся дальше в WindowClassBase. Логика выбора описывается в комментариях: если передаётся в ControlType thisclass, то тогда мы выбираем DerWndCls, в противном случае выбирается сам ControlType.

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

template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WndClsDerivedTemplateClass : public WindowClassControlBaseTemplate<WndClsDerivedTemplateClass<DerWndCls>, ControlType> //строим новый класс на основе предыдущего производного{protected: static unsigned short int usiWndNum; //количество объектов класса public: //конструктор для инициализации класса по умолчанию WndClsDerivedTemplateClass(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr); //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию WndClsDerivedTemplateClass(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr); WndClsDerivedTemplateClass(WndClsDerivedTemplateClass&); //конструктор копирования virtual ~WndClsDerivedTemplateClass() 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 внутри оконной процедуры};

Первый параметр по умолчанию инициализируется через thisclass, а ControlType вычисляется на основе самого DerWndCls: если DerWndCls = thisclass, то ControlType := thisclass, иначе ControlType := DerWndCls (специально указал присваивание в стиле Pascal, чтобы отличить от сравнения).

Дальше же передаваться будет сам же класс WndClsDerivedTemplateClass, параметризированный DerWndCls, вместе с вычисленным (на этапе компиляции) контрольным типом.

Если мы создаём объект этого класса, то есть WndClsDerivedTemplateClass сам является вершиной иерархии, то тогда DerWndCls = ControlType = thisclass, и дальше происходит передача <WndClsDerivedTemplateClass, thisclass>. Тот факт, что WndClsDerivedTemplateClass параметризуется пустышкой, не имеет никакого значения – этот тип, да и вообще любой переданный на месте DerWndCls, никак не используется внутри класса: из него не создаётся никакого объекта и не вызывается через него никакая функция. Потому формально WndClsDerivedTemplateClass можно инстанцировать буквально чем угодно – тип-параметр служит лишь для передачи дальше по линии наследования. Но вот то, что вместо DerWndCls дальше был передан WndClsDerivedTemplateClass< thisclass или любой другой тип>, имеет значение: WndClsDerivedTemplateClass имеет функцию OnCreate, которая будет вызываться внутри WindowClassBase.

При таком варианте в WindowClassControlBaseTemplate на месте ControlType приходит thisclass, и конечный тип выводится как DerWndCls = WndClsDerivedTemplateClass, имеющий нужную функцию OnCreate. Что нам и нужно.

Рассмотрим теперь вариант, когда строится новый класс на базе WindowClassControlBaseTemplate (дальнейшее наследование):

template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WindowClassDerivedTemplateNext : public WndClsDerivedTemplateClass<WindowClassDerivedTemplateNext<DerWndCls>>

В этом случае в WndClsDerivedTemplateClass на место DerWndCls приходит нечто, отличное от thisclass, и ControlType, увидев это отличие, принимает значение переданного DerWndCls.
Тогда в WindowClassControlBaseTemplate идёт следующий вариант параметризации: , WindowClassDerivedTemplateNext>.

В WindowClassControlBaseTemplate, в свою очередь, поскольку ControlType != thisclass, то используется сам ControlType, равный WindowClassDerivedTemplateNext, который как раз и является нужным классом для выбора OnCreate.

На первый взгляд при такой схеме всё вроде бы хорошо. Но это не так. Построим ещё один класс на основе последнего:

template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WindowClassDerivedTemplateNext2 : public WindowClassDerivedTemplateNext<WindowClassDerivedTemplateNext2<DerWndCls>>

В WindowClassDerivedTemplateNext на место DerWndCls придёт WindowClassDerivedTemplateNext2. ControlType выведется также как WindowClassDerivedTemplateNext2. Затем в WndClsDerivedTemplateClass будет передано WindowClassDerivedTemplateNext, и в нём ControlType выведется как этот же WindowClassDerivedTemplateNext. Далее в WindowClassControlBaseTemplate будут переданы эти же значения, и там вместо правильного WindowClassDerivedTemplateNext2 будет использован WindowClassDerivedTemplateNext, и будет вызвана функция OnCreate именно класса WindowClassDerivedTemplateNext, а не WindowClassDerivedTemplateNext2.

Напоминаю, что при такой схеме наследования и передачи параметров важен тип самого класса, который пришёл в итоге в WindowClassControlBaseTemplate, а не то, чем он там параметризован.

Следовательно, чтобы тип, для которого будет вызываться OnCreate, выводился правильно, нужно изменить определение класса WindowClassDerivedTemplateNext:

template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WindowClassDerivedTemplateNext : public WndClsDerivedTemplateClass<WindowClassDerivedTemplateNext<DerWndCls>, ControlType>

В таком случае дальше в WndClsDerivedTemplateClass на место ControlType будет передаваться верное значение, равное WindowClassDerivedTemplateNext2 вместо того, чтобы он выводился там в неверное значение.

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

Если мы забыли это сделать и нарушили это правило, то при использовании static_cast получим ошибку компиляции, а в случае преобразования указателей в стиле С внутри WindowClassControlBaseTemplate получим неверно работающую программу. Например, если мы попробуем создать объект для класса

template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WindowClassDerivedTemplateNext : public WndClsDerivedTemplateClass<WindowClassDerivedTemplateNext<DerWndCls>, ControlType>

то компилятор выдаст ошибку: он не сможет преобразовать типы указателей внутри WindowClassControlBaseTemplate за счёт того, что тип был передан неверный, для которого нельзя выполнить такое преобразование (поскольку мы собираемся создавать объект класса WindowClassDerivedTemplateNext, то для него считаем, что сам класс WindowClassDerivedTemplateNext находится в вершине иерархии, а в этом случае, как было показано выше, ControlType передавать не следует). Без static_cast код скомпилируется и просто будет вызвана OnCreate не того класса. Однако удаление передачи ControlType делает программу снова компилируемой.

В конечном итоге, всё это слишком сложно, ненадёжно и требует наличия исходных текстов всех классов. Кроме того, мы можем создавать объекты только последнего производного класса, а какого-либо из его базовых – нельзя из-за передачи ControlType (либо можем в случае передачи указателя в стиле С, но эти объекты будут неверно инициализироваться). Нужно другое решение, более простое и надёжное.

Вариативный шаблон

Тем не менее, вышеприведённый вариант шаблонного наследования и передачи типа создаваемого объекта в класс WindowClassBase, где происходит создание окна и вызов OnCreate, имеет серьёзные недостатки. Нужен какой-либо иной, более надёжный и работоспособный вариант.

С++11 представляет новый тип шаблона: шаблон с переменным числом аргументов, или вариативный шаблон. Его параметры представляют собой последовательность из типов заранее неизвестной длины. Вместо рискованных манипуляций с контрольным типом в предыдущем примере я решил пойти другим путём: чтобы избежать ситуаций, когда промежуточный в иерархии класс замещает собой вышестоящий класс по иерархии через неверную параметризацию (в примере выше это был WindowClassDerivedTemplateNext<WindowClassDerivedTemplateNext2>), можно вообще обойтись от подобного типа параметризации, просто ставя эти классы в последовательности рядом. Например, при трёх последовательных наследованиях в параметрах шаблона в конечном итоге сформируется такой список:

<WndCls3<>, WndCls2<>, WndCls1<>>

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

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

//реализация, когда нужный нам класс расположен последнимtemplate<class... Classes> class WindowClassVariadicTemplate; //общее объявление класса //специализация, при которой первый в списке параметров класс отделяется от остальныхtemplate<class DerWndCls, class... OtherWindowClasses> class WindowClassVariadicTemplate<DerWndCls, OtherWindowClasses...> : public WindowClassBase{ //просто извлекаем самый первый класс в списке: нужный нам класс - DerWndCls using DerivedWndClassType = DerWndCls; public: //конструктор для инициализации класса по умолчанию WindowClassVariadicTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerivedWndClassType *>(this), hInstance, szClassName, szWndTitle) {} //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию WindowClassVariadicTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerivedWndClassType *>(this), wc, szWndTitle) {} virtual void OnCreate(HWND hWnd) {} //обеспечивает обработку WM_CREATE внутри оконной процедуры};

Сначала объявляется общее описание шаблона класса без тела. Затем определяется его специализация, в которой первый тип отделён от остальных. Именно он нам и интересен. Это справедливо для случая, когда при движении по цепочке иерархии вниз к WindowClassBase каждый очередной класс помещает себя в конец списка параметров. Тогда нужный нам класс будет в начале, и его очень просто отделить от остальных. Можно поступить по-другому: каждый новый класс будет помещать себя в начало списка параметров шаблона. Тогда класс в вершине иерархии будет самым последним в списке, и извлечь его оттуда намного сложнее. В данном конкретном случае эти два подхода совершенно идентичны, но первый намного проще в реализации (в том числе во время компиляции – не придётся обрабатывать весь список, извлекая последний элемент из него), и именно он приведён выше.

Первый элемент, являющий самым высшим классом в иерархии, извлекается из списка и передаётся в WindowClassBase. Если для него определена OnCreate, она и будет вызвана. В противном случае будет вызвана OnCreate ближайшего базового класса по отношению к нему. Если вариативный список параметров оказался пустым (мы пытаемся создать объект из WindowClassVariadicTemplate), то компиляция завершится неудачей, требуя наличия хотя бы одного типа в списке параметров.

Первый класс на основе WindowClassVariadicTemplate будет таким:

template<class... PrevWndClasses> class WindowClassVariadic1 : public WindowClassVariadicTemplate<PrevWndClasses..., WindowClassVariadic1<>>{protected: static unsigned short int usiWndNum; //количество объектов класса public: //конструктор для инициализации класса по умолчанию WindowClassVariadic1(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassVariadicTemplate(hInstance, szClassName, szWndTitle) { usiWndNum++; //увеличиваем количество объектов данного класса } //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию WindowClassVariadic1(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassVariadicTemplate(wc, szWndTitle) { usiWndNum++; //увеличиваем количество объектов данного класса } WindowClassVariadic1(WindowClassVariadic1& wcObj) : WindowClassVariadicTemplate(wcObj) //конструктор копирования { usiWndNum++; //увеличиваем количество объектов данного класса } virtual ~WindowClassVariadic1() override //виртуальный деструктор { if (hWnd) this->OnClose(hWnd); //закрываем окно, используя механизм виртуальных функций } virtual void OnCreate(HWND hWnd) override //обеспечивает обработку WM_CREATE внутри оконной процедуры { //обеспечивает обработку WM_CREATE внутри оконной процедуры SetClassLongPtr(hWnd, GCL_HBRBACKGROUND, (LONG)CreateSolidBrush(RGB(200, 160, 255))); } virtual void OnPaint(HWND hWnd) override //обеспечивает обработку WM_PAINT внутри оконной процедуры { ... } virtual void OnDestroy(HWND hWnd) override //обеспечивает обработку WM_DESTROY внутри оконной процедуры { ... }};

Этот класс, приняв неопределённый список параметров PrevWndClasses, передаёт его дальше базовому классу, вставив себя перед ним в качестве первого элемента с пустым списком параметров. Поскольку сам этот класс WindowClassVariadic1 является вариативным, то WindowClassVariadic1 также будет вариативным, хоть и без параметров, и вся эта последовательность классов фактически есть вариативный шаблон, каждый элемент которого является также вариативным шаблоном.

Следующий производный класс имеет вид:

template<class... PrevWndClasses> class WindowClassVariadic2 : public WindowClassVariadic1<PrevWndClasses..., WindowClassVariadic2<>>{ ...};

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

template<class... PrevWndClasses> class WindowClassVariadic3 : public WindowClassVariadic2<PrevWndClasses..., WindowClassVariadic3<>>{ ...};

В этом и заключается смысл полиморфного многократного наследования: объявив класс таким образом, мы гарантируем не только создание объектов данного типа, но и всех объектов всех остальных классов, производных от него, сколько бы и какими бы они ни были в будущем. При этом в WindowClassBase всегда будет вызвана правильная OnCreate.

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

Забегая вперёд, где в конечном итоге был найден более лучший в данной ситуации метод, реализация наследования через вариативный шаблон позволяет реализовать более сложную логику компиляции в WindowClassBase: имея доступ ко всем типам, по которых произошло наследование, можно гибко выбирать среди них нужный по каким-либо критериям и вызывать определённую в нём функцию-член. Но это – всё же уже несколько другой случай.

Класс инициализации

Ещё не подозревая о реакции static_cast на производные типы, я продолжал искать другие способы реализации передачи вершинного класса иерархии в WindowClassBase. В какой-то момент подумал о том, чтобы вывести реализацию OnCreate в отдельный класс, специально для неё созданного:

class WindowClassInit1{public: void OnCreate(HWND hWnd) //обеспечивает обработку WM_CREATE внутри оконной процедуры { //обеспечивает обработку WM_CREATE внутри оконной процедуры SetClassLongPtr(hWnd, GCL_HBRBACKGROUND, (LONG)CreateSolidBrush(RGB(200, 160, 255))); }};

Этим классом параметризуется другой класс, реализующий все остальные переопределения для виртуальных функций. Он является производным от уже описанной WindowClassTemplate:

template<class WndClsInit = WindowClassInit1> class WindowClassDerivedI1 : public WindowClassTemplate<WndClsInit>{ ...};

Таким образом:

  • наследование классов происходит как обычно для виртуальных функций;
  • происходит передача от класса к классу по цепочке наследования только класса инициализации, специально определённого для реализации OnCreate.

Если данный класс расположен в вершине иерархии, то параметр WndClsInit станет равным WindowClassInit1 – определённого для этого класса классу инициализации, и произойдёт передача его дальше по цепочке иерархии. Если же этот класс – промежуточный в цепочке, то он просто примет переданный ему класс и передаст его дальше. Затем, что такой вариант выгодно отличается от предыдущих тем, что в шаблонах происходит не передача себя, а передача некоторого стороннего класса, что реализуется (и выглядит) намного проще. Шаблон в такой форме также подходит без изменений для реализации всей цепочки наследования: будет происходить только смена названий классов.

Тем не менее, static_cast, в отличие от преобразования в стиле С, внутри WindowClassTemplate не пропустит такую форму наследования: он просто не сможет преобразовать при передаче this от (WindowClassTemplate *) к (WindowClassInit1 *). И это логично: WindowClassInit1 – фактически посторонний класс, просто переданный как тип в эту точку, он никак не связан с WindowClassTemplate и всей цепочкой производных от него, потому преобразование указателя к нему недопустимо.

Цепочечная передача типа производного класса в WindowClassBase, условная передача

Ну и наконец был найден самый лучший для данной ситуации способ передачи типа производного класса в корневой базовый WindowClassBase через всю цепочку наследования, лишённый недостатков предыдущих и при этом проще, чем вариативный шаблон. Определим следующий шаблонный класс на основе WindowClassTemplate:

template<class DerWndCls = thisclass> class WindowClassDerivedAlternative1 : public WindowClassTemplate<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative1<>, DerWndCls>>{public: //конструктор для инициализации класса по умолчанию WindowClassDerivedAlternative1(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassTemplate(hInstance, szClassName, szWndTitle) {} //конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчанию WindowClassDerivedAlternative1(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassTemplate(wc, szWndTitle) {} virtual ~WindowClassDerivedAlternative1() override //виртуальный деструктор { if (hWnd) this->OnClose(hWnd); //закрываем окно, используя механизм виртуальных функций } virtual void OnCreate(HWND hWnd) override //обеспечивает обработку WM_CREATE внутри оконной процедуры { //обеспечивает обработку WM_CREATE внутри оконной процедуры SetClassLongPtr(hWnd, GCL_HBRBACKGROUND, (LONG)CreateSolidBrush(RGB(200, 160, 255))); } virtual void OnPaint(HWND hWnd) override //обеспечивает обработку WM_PAINT внутри оконной процедуры { //обеспечивает обработку WM_PAINT внутри оконной процедуры HDC hDC; PAINTSTRUCT ps; RECT rect; hDC = BeginPaint(hWnd, &ps); GetClientRect(hWnd, &rect); DrawText(hDC, TEXT("Шаблонный переопределённый класс с условной передачей параметра (ПЕРВОЕ наследование)."), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); EndPaint(hWnd, &ps); }};

Этот класс принимает в качестве параметра DerWndCls, который по умолчанию приравнивается к thisclass. При передаче происходит сравнение DerWndCls с thisclass: в случае равенства (значение по умолчанию, то есть данный класс находится в вершине иерархии) происходит передача себя с пустым списком параметров. В противном случае дальше передаётся принятый DerWndCls.

Это решение я считаю наилучшим в данной ситуации по всем параметрам:

  • единая форма определения класса для всей цепочки наследования;
  • простая и прозрачная логика передачи класса по всей цепочке наследования;
  • нет накладных расходов из-за вариативного шаблона (в тех случаях, как в данном, когда это и не требуется).

Страшное возмездие

Что всё это означает? Это значит, что если вы хотите использовать такую нетрадиционную форму наследования, вы все свои классы должны оформлять строго определённым образом, чтобы они допускали передачу через себя возможного нового производного. Это – весьма нетрудное требование, и при желании его просто соблюдать.

Но есть другой, гораздо более нетривиальный вопрос: соотношение типов и указателей. Писали же умные люди: не надо играться с такими вещами в конструкторе и идти против принципов языка и логики работы компилятора. А я не послушался и всё равно это сделал. Теперь наступает закономерное возмездие.

Итак, у нас есть 4 класса:

template<class DerWndCls = thisclass> class WindowClassDerivedAlternative1 : public WindowClassTemplate<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative1<>, DerWndCls>>{…}; template<class DerWndCls = thisclass> class WindowClassDerivedAlternative2 : public WindowClassDerivedAlternative1<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative2<>, DerWndCls>>{ …}; template<class DerWndCls = thisclass> class WindowClassDerivedAlternative3 : public WindowClassDerivedAlternative2<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative3<>, DerWndCls>>{ …}; template<class DerWndCls = thisclass> class WindowClassDerivedAlternative4 : public WindowClassDerivedAlternative3<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative4<>, DerWndCls>>{ …};

Как я писал выше, конкретное их содержимое и логика работы совершенно неважны. Важно лишь то, что в заголовке определения класса. На основании этих классов мы создаём 4 объекта:

WindowClassDerivedAlternative1<> w1(hInstance, TEXT("WindowClassDerivedAlternative1"), TEXT("WindowClassDerivedAlternative1"));WindowClassDerivedAlternative2<> w2(hInstance, TEXT("WindowClassDerivedAlternative2"), TEXT("WindowClassDerivedAlternative2"));WindowClassDerivedAlternative3<> w3(hInstance, TEXT("WindowClassDerivedAlternative3"), TEXT("WindowClassDerivedAlternative3"));WindowClassDerivedAlternative4<> w4(hInstance, TEXT("WindowClassDerivedAlternative4"), TEXT("WindowClassDerivedAlternative4"));

Развернём определения их типов, скрытые за пустыми скобками с помощью аргументов по умолчанию. Тип w1 является WindowClassDerivedAlternative1. Тип w2 равен WindowClassDerivedAlternative2, а его базовый класс – WindowClassDerivedAlternative1. Тип w3 есть WindowClassDerivedAlternative3, его базовый класс – WindowClassDerivedAlternative2, а базовый класс того – WindowClassDerivedAlternative1. Аналогично – для четвёртого объекта. Посмотрите на следующую схему:

Создавая каждый новый производный класс на основе некоторого таким образом определённого базового, вы определяете не просто новый класс, а заодно и всю цепочку его базовых заново и целиком. Она будет параллельна к цепочке его же собственного базового класса. У вашего класса будут свои собственные базовые классы, и ни один из них не удастся привести ни к одному из исходных базовых несмотря на то, что код генерации для всех этих классов единый! Это кажется настоящей фантастикой, но это действительно так! Это означает, что все привычные способы манипуляции наследуемых классов и указателей работать не будут! В данной конкретной архитектуре только базовый WindowClassBase спасает положение, в противном случае даже создать массив из базовых классов (например, на основе WindowClassTemplate) также было бы нельзя, потому что у всех таких классов разные типы.
Таким образом, всем известное и понятное определение вида:

WindowClassDerivedAlternative1<> *p2 = &w2;

…перестанет компилироваться, потому что вы пытаетесь создать указатель типа, несовместимый с типом объекта w2 несмотря на то, что полчаса назад сами же написали класс, производный от класса WindowClassDerivedAlternative1 и на основании которого был создан объект w2.

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

Эксперименты с кодом

Чтобы упростить всем интересующимся эксперименты и сэкономить им время на набор кода, я разместил на GitHub все проекты, которые послужили основой этой статьи:
github.com/SkyCloud555/ECRTP
Только выбирайте один проект по очереди в качестве стартового, иначе утонете в море разноцветных окон.

Заключение

«Если всё это такая шутка, то, учитывая затраченные усилия, выглядит слишком серьёзно и натурально. А если не шутка, то ни один нормальный разработчик в здравом уме в реальности ничего подобного использовать не будет. И вообще, чувак, не закончить ли тебе страдать ерундой и не пойти бы заняться чем-нибудь, что приносит деньги, полезным».

Вы правы, если так думаете. В этой статье я всего лишь показал, что С++ и так тоже может. Вопрос о практическом применении этих конструкций остаётся открытым. И вообще, это всё скорее уже относится к обобщённому и метапрограммированию. Вам может не потребоваться вообще создавать никаких объектов этих классов, но сами классы могут быть зачем-то нужны. Да и мало ли какие полезные решения можно придумать на этой основе… Исходный CRTP-то используется! Причём используется даже не где-нибудь, а прямо в стандартной библиотеке! Кто не верит или не помнит, погуглите std:: еnаblе_shared_from_this.

Возвращаясь же к исходной задаче с окнами… Особенно сейчас, трезво и без травы оглядываясь на всё это спустя три года… Даже если отбросить тот факт, что я затронул банальную уже миллион раз изъезженную тему, и это давно никому не интересно, потому что для реальных пацанов нормальных людей есть MFC Qt, я бы просто обеспечил передачу в класс окна какой-нибудь функциональный объект. Через цепочку наследований обеспечить его передачу совсем несложно, но зато он всё сделает просто, понятно и без извращений, и получится совершенно нормальный предсказуемый класс без каких-либо побочных эффектов, сопровождать и развивать который можно отдавать абсолютно любому.
То, что получилось в этой статье, – это всего лишь интересная нетривиальная задача, которую мне всё-таки удалось решить. Надеюсь, вам это тоже было интересно.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»