Я некоторое время возился с C++
и начал эмулировать некоторые стандартные классы в C
. Я хотел сделать так, чтобы объекты с той же целью можно было поменять местами. На бумаге все выглядело хорошо, но когда я применил свой подход на практике, у меня оказалось слишком много кода и слишком много определений макросов.
Как я планировал свои "классы"
Вместо создания множества функций для каждого объекта (например, CreateX()
, DestroyX()
et c), я попытался обернуть это в одну большую процедуру, которая на самом деле просто очень большая switch
. Он работает аналогично win32
оконным процедурам, и я имею в виду, что когда создается объект, вы не вызываете CreateX
, вы вызываете процедуру объекта с помощью MSG_CREATE
(где MSG_CREATE
- это макрос с некоторыми числовое значение).
Итак, я сделал обертку для каждого экземпляра объекта, например так:
// !NOTE: This has nothing to do with types in <windows.h>. HANDLE was previously declared.
typedef unsigned long __stdcall (*OBJPROC) (HANDLE, unsigned long, unsigned long, unsigned long);
struct tagCLASS_WRAPPER
{
OBJPROC fnObjProc;
void* lpVariables;
};
typedef struct tagCLASS_WRAPPER CLASS_WRAPPER;
Чтобы обсудить, что обычно содержит ObjectProcedure
, вот один (не полный) простой пример для Allocator
, который подсчитывает, сколько блоков он выделил:
unsigned long __stdcall AllocatorProcedure (HANDLE hAllocator, unsigned long uMsg, unsigned long uData, unsigned long uMetaData)
{
switch (uMsg)
{
/** This immitates the constructor. */
case MSG_CREATE:
{
/** There is no object yet, hAllocator contains nothing usefull... */
/** Allocate the class wrapper */
CLASS_WRAPPER* lpObject = malloc(sizeof(CLASS_WRAPPER));
/** Set this function as the object procedure. */
lpObject->fnObjProc = AllocatorProcedure;
/** We only need to count something, so one unsigned long variable should do. */
lpObject->lpVariables = malloc(sizeof(unsigned long));
/** Return the class wrapper. */
return (unsigned long) lpObject;
}
break;
/** This immitates the destructor. */
case MSG_DESTROY:
#define This ( (CLASS_WRAPPER*)hAllocator )
free(This->lpVariables);
free(This);
#undef This
break;
case MSG_ALLOC:
#define This ( (CLASS_WRAPPER*)hAllocator )
*((unsigned long*)This->lpVariables) ++; // add one to the blocks allocated.
return malloc(uData); // the size of the object is passes using the data parameter;
#undef This
break;
case MSG_FREE:
#define This ( (CLASS_WRAPPER*)hAllocator )
*((unsigned long*)This->lpVariables) --; // substract from to the blocks allocated.
free((void*)uData); // the address is passes using the data parameter;
#undef This
break;
}
}
И когда вы хотите использовать этот недавно созданный Allocator
, это будет выглядеть так:
#define IGNORE 0
int main()
{
/** Create the allocator. */
HANDLE hAllocator = (HANDLE)CreateObject(AllocatorProcedure, IGNORE, IGNORE);
/* This translates to: (HANDLE)AllocatorProcedure(NULL, MSG_CREATE, IGNORE, IGNORE); */
/** Allocate 10 ints. */
#define SIZE 10
int* SomeArray = (int*) CommandObject(hAllocator, MSG_ALLOC, sizeof(int)*SIZE, IGNORE);
/* This translates to: (int*) AllocatorProcedure(hAllocator, MSG_ALLOC, sizeof(int)*SIZE, IGNORE); */
// do something with the array...
/** Free 10 ints. */
#undef SIZE
CommandObject(hAllocator, MSG_FREE, (unsigned long)SomeArray, IGNORE);
/* This translates to: AllocatorProcedure(hAllocator, MSG_FREE, (unsigned long)SomeArray, IGNORE); */
/** Destroy the allocator. */
CommandObject(hAllocator, MSG_DESTROY, IGNORE, IGNORE);
/* This translates to: AllocatorProcedure(hAllocator, MSG_DESTROY, IGNORE, IGNORE); */
return 0;
}
CreateObject
, CommandObject
и DestroyObject
- это макрофункции, которые будут расширены в том, что я прокомментировал. Я говорю неполный пример, потому что я ничего не делаю с подсчитанными блоками, и в коде абсолютно нет проверки ошибок. Кроме того, я говорю просто, потому что вы не можете стать меньше, чем это ...
Теперь, то, что я имел в виду в начале с objects with the same purpose can be swapped
, это root того, почему я решил реализовать свои классы так: путь.
Давайте представим, что в одном проекте я использую уже созданный класс List
, для которого требуется объект Allocator
при создании. Объект List
может принимать любой объект Allocator
, если сообщения (например, MSG_ALLOC
и MSG_FREE
из приведенного выше примера) не перепутаны.
Предположим, что я создал проект с использованием переносного распределителя с процедурой PortableAllocProc
, и теперь я хочу переключить его на другой, который использует HeapAlloc
с <windows.h>
и определяется с помощью процедуры WinAllocatorProc
. Все, что мне нужно изменить, - это строка, в которой создается распределитель:
// so from something like this
HANDLE hAllocator = (HANDLE) CreateObject(PortableAllocProc, IGNORE, IGNORE);
// I will switch to something like this
HANDLE hAllocator = (HANDLE) CreateObject(WinAllocProc, GetProcessHeap(), IGNORE);
И тогда в коде ничего не должно измениться, потому что вызовы List
объектов, которые требуют выделения, отправляют сообщения типа MSG_ALLOCATE
и MSG_FREE
указанному распределителю.
Переходя к реальным ситуациям, происходит проверка ошибок, несколько классов входят в один и тот же проект и, возможно, некоторые из них выполняют одну и ту же роль (например, WinAllocator
и PortableAllocator
сверху). Код становится невероятно объемным, а количество определений макросов настолько велико, что никто не может их запомнить. Очевидно, что Error Codes
и Object Messages
- это имена, использующие макросы, и в некоторых случаях они похожи, поэтому вы не знаете, какой из них вам нужен (это WAM_ALLOC
, PAM_ALLOC
или WAM_VALLOC
?)
Как я могу уменьшить количество макросов, используемых классом? Есть ли лучшее соглашение об именах для них? Или у кого-то есть другие идеи о том, как достичь эмуляции класса в C
, чтобы объекты могли обмениваться, как я описал выше на примере List
и Allocator
?