Разработка эмулируемых классов для совместимости и / или взаимозаменяемости - PullRequest
0 голосов
/ 02 апреля 2020

Я некоторое время возился с 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?

...