Adventi naptár

Reflektivitás

Van egy programunk, amely különféle típusú objektumok adatait tárolja. Ezeknek az objektumoknak az adatait ki kell írni fájlokba, és vissza kell olvasni őket onnan. Néha meg kell jeleníteni egy objektum adatait egy párbeszédablakban, hogy a felhasználó átírhassa azokat stb.

Legyen erre egy egyszerű példa egy téglalap:

struct Teglalap {
    int px, py;             /* pozicio a kepernyon */
    int szeles, magas;      /* meret */
};

És a fájlba írt változat, ahogyan ott egy téglalap kinéz:

PozicioX=5
PozicioY=10
MeretX=15
MeretY=20

A következő problémákkal szembesülünk. Ha a programot továbbfejlesztjük, a téglalapnak esetleg új tulajdonsága jelenik meg, pl. az új verzió a színét is eltárolja. A kódot egy csomó helyen módosítanunk kell:

  • A teglalap.h fájlban, ahol a struktúra definiálva van.
  • A teglalap_save() függvényben, amelyik egy téglalap adatait kiírja egy fájlba.
  • A teglalap_load() függvényben, amelyik beolvassa azokat.
  • A teglalap_edit() függvényben két helyen is; egyrészt ahol létrehozzunk a téglalap tulajdonságai párbeszédablakban a beviteli mezőket, másrészt pedig ahol visszaolvassuk azokat, és egy adott struktúrába elmentjük.
  • … és így tovább.

A másik probléma az, hogy esetleg többféle objektumot szeretnénk hasonló módon kezelni. A kör objektumokat is hasonló módon mentjük fájlba, jelenítünk meg hozzájuk ablakot. A kor_save() függvény szinte ugyanúgy néz ki, mint a teglalap_save() függvény, csak mások a tulajdonságok nevei. A hasonlóság pedig azt sejtteti, hogy valahogyan általánosítani lehetne. Vagy éppen kellene.

1. Reflektivitás

Ami itt hiányzik nekünk a C-ből, az a reflektivitás. Arra lenne szükségünk itt, hogy valahogyan fel tudjuk sorolni egy struktúra adattagjait – például úgy, hogy sztringként tudjuk megadni azt, hogy melyikre szeretnénk hivatkozni. Meg hogy egy for() ciklussal végig tudjunk menni rajtuk. Ha a struktúrában egy tömb lenne, akkor ez működne… De a különálló adattagokra ilyet nem lehet.

Azt viszont meg lehet csinálni, hogy felsoroljuk az adattagok neveit és melléjük pointereket egy adott példány adattagjaira. Ez könnyen általánosítható bármilyen típusú tagra, úgyhogy maradjunk az egészeknél:

#include <stdio.h>

typedef struct Teglalap {
    int px, py;         /* pozicio a kepernyon */
    int szeles, magas;  /* meret */
} Teglalap;

void teglalap_save(Teglalap *t) {
    struct {
        char const *nev;
        int *adattag;
    } t_adatai[] = {
        { "PozicioX", &t->px },
        { "PozicioY", &t->py },
        { "MeretX", &t->szeles },
        { "MeretY", &t->magas },
        { NULL }
    };

    for (int i = 0; t_adatai[i].nev != NULL; ++i)
        printf("%s=%d\n", t_adatai[i].nev, *t_adatai[i].adattag);
}

int main(void) {
    Teglalap t = {5, 10, 15, 20};
    
    teglalap_save(&t);
}

Itt a for() ciklus elég kényelmes már, és jól használható az adattagok táblázatos formája is. A probléma csak az, hogy az int* pointerek egy adott téglalap példányhoz kötődnek. Emiatt kellett a függvényen belül definiálni a tömböt; hogy kezdeti értékként a pointerek megkapják az egyes intekre mutató értékeket, a konkrét *t téglalap adattagjaira mutatva. Tehát a struktúra inicializálását ugyanígy szerepeltetni kell a többi helyen is, vagyis ha módosul a téglalap struktúra, akkor még mindig sok helyen kell javítgatni a programot.

Ha több téglalap példányunk van, akkor bár az int* pointerek eltérőek, viszont minden téglalap memóriaképe megegyezik. Egy téglalap memóriaterületének elejétől számítva egy bizonyos adattag mindig ugyanannyi bájtnyira van. Ha az adattagokat leíró struktúrában ezt tárolnánk a cím helyett, akkor azt bármelyik téglalapra használhatnánk. Az offszetet úgy kapjuk, hogy kivonjuk az adott adattag memóriacíméből a téglalap elejének memóriacímét; persze mindkét pointert char*-gá konvertáljuk, hogy az eredményt bájtokban kapjuk:

printf("%d\n", (int)((char*)&t.px-(char*)&t));
printf("%d\n", (int)((char*)&t.py-(char*)&t));

Ezt visszafelé is meg lehet csinálni. Ha egy char* típusú mutatóhoz – amelyik egy téglalap memóriaképének az elejére mutat – hozzáadjuk az így létrehozott offszeteket, akkor a szóban forgó adattagra kapunk egy char* típusú pointert, amit aztán a megfelelő típusúra cast-olhatunk:

int pxo = (int)((char*)&t.px-(char*)&t);  // ofszet
int pyo = (int)((char*)&t.py-(char*)&t);

printf("%d", *(int*)((char*)&t + pxo)); // ofszetbol pointer
printf("%d", *(int*)((char*)&t + pyo));

Ezek az offszetek az adott téglalap példányoktól függetlenek; így már akkor is meghatározhatóak, amikor még nem tudjuk, melyik téglalappal kell majd dolgoznia egy függvénynek. Ez egyben azt is jelenti, hogy a téglalapokat leíró tömböt (amely a neveket és az offszeteket tartalmazza) csak egyszer kell definiálnunk, nem pedig minden függvényben.

2. A végeredmény

Még egy apró trükk. Az offszetek meghatározásához még arra sincsen szükség, hogy akár egyetlen egy téglalap is létezzen. A NULL pointert ugyanis castolhatjuk Teglalap* típusúvá. Ennek ugyan egy adott elemére nem hivatkozhatunk ((Teglalap*)0)->px, de egy adott elem memóriacímére igen: &(((Teglalap*)0)->px). Ebből kivonva a NULL pointert kapjuk az offszetet. Így született a lenti OFFSETOF makró.

#include <stdio.h>

#define OFFSETOF(STRUCT,MEMBER) ((char*)(&((STRUCT *)0)->MEMBER)-(char*)0)
#define MEMBER(MEMBERTYPE,STRUCTP,OFFSET) (*(MEMBERTYPE *)((char*)STRUCTP+OFFSET))

typedef struct Teglalap {
    int px, py;
    int szeles, magas;
} Teglalap;

struct {
    char const *nev;
    int offset;
} teglalap_leiro[] = {
    { "PozicioX", OFFSETOF(Teglalap, px) },
    { "PozicioY", OFFSETOF(Teglalap, py) },
    { "MeretX", OFFSETOF(Teglalap, szeles) },
    { "MeretY", OFFSETOF(Teglalap, magas) },
    { NULL }
};

int main(void) {
    Teglalap t = {5, 10, 15, 20};
    
    for (int i = 0; teglalap_leiro[i].nev != NULL; ++i)
        printf("%s=%d\n", teglalap_leiro[i].nev,
                          MEMBER(int, &t, teglalap_leiro[i].offset));
    
    return 0;
}

Némely C fordítók megengedik azt, hogy void* mutatókat vonjunk ki egymásból. Ilyenkor az eredményt ugyanúgy bájt egységekben kapjuk, mint char* esetén. Ez azonban nem szabványos, ezért kellenek a char*-ok.