xff.cz - mapa webu - novinky

Esej proti zapouzdřování v C

Klasické zapouzdřování je, když před uživatelem nějakého rozhraní skryjeme vnitřní reprezentaci datových typů, tak implementaci procedur.

V C to vypadá takto:

// obj.h:
#pragma once

struct obj*
     obj_new             (void);
void obj_do_something    (struct obj* obj, int some_input);
int  obj_get_some_result (struct obj* obj);
void obj_free            (struct obj* obj);

Je to hezké a funguje to celkem dobře. Podobný přístup používá velké množství knihoven v C (např. prakticky celý projekt GNOME) a má to své výhody. Ta největší spočívá v snadné realizaci binární zpětné kompatibility knihoven. Když se uživatelský kód nemůže vrtat v datech přímo, protože nevidí do datových struktur a ke všemu přistupuje přes volání funkcí, tak vše co musíte zajistit je, dodržet kontrakt na volání již publikovaných funkcí a nové vlastnosti do knihovny doplňujete přidáváním nových funkcí jazyka C. Mezitím můžete klidně přepsat celou implementaci v obj.c a vše bude fungovat bez problémů a bez nutnosti překompilovat uživatelské aplikace.

Nicméně, pokud nepíšete knihovnu, ale koncovou aplikaci a nemusíte se tedy starat o binární kompatibilitu, nebo píšete privátní knihovny pro privátní použití v rámci několika binárek vaší aplikace můžete si dovolit otevřenější přístup k programování knihovních funkcí. Ten vám pak může:

Výše naznačené API by se změnilo na:

// obj-open.h:
#pragma once

struct obj {
    int val;
};

void obj_init            (struct obj* obj);
void obj_do_something    (struct obj* obj, int some_input);
int  obj_get_some_result (struct obj* obj);
void obj_cleanup         (struct obj* obj);

Přitom, připravíte-li definici struktury struct Obj tak, aby výchozí hodnota byla ekvivalentní vynulované paměti, můžete klidně obj_init() zahodit. Pokud v rámci struct Obj nejsou alokované žádné další zdroje či paměť na haldě, tak nebudete potřebovat ani obj_cleanup() a rozhraní API se smrskne na:

// obj-open.h:
#pragma once

struct obj {
    int val;
};

void obj_do_something    (struct obj* obj, int some_input);
int  obj_get_some_result (struct obj* obj);

Překvapivě velké množství problémů lze řešit minimálně bez nutnosti explicitní inicializace pomocí obj_init(). Uvolnění prostředků se mnohdy vyhnout nedá.

Takto definované rozhraní pak lze použít ke kompozici datových struktur i funkcí:

// mux-obj.c:
#include "obj.h"

struct mux_obj {
    struct obj objs[4];
    int selected;
};

void mux_obj_do_something(struct mux_obj* mux, int some_input)
{
    obj_do_something(mux->objs[mux->selected], some_input);
}

int main()
{
  struct mux_obj mux = {};

  mux.selected = 2;
  mux_obj_do_something(&mux, 123);
  mux.selected = 1;
  mux_obj_do_something(&mux, 456);

  // ...
  return 0;
}

Kompozice je triviální a není třeba řešit manuálně správu paměti. Prostor pro mux za nás kompilátor vyhradí na zásobníku volání funkcí a následně při návratu z main() se prostor i automaticky „uvolní.“ Pomocí rozšíření jazyka C v kompilátoru gcc, můžeme vynulovat obsah mux použitím inicializátoru {}.

Ekvivalentní kód za pomoci rozhraní z úvodu člnáku by vyžadoval alokaci paměti na haldě a její manuální dealokaci pro každý struct obj v objs zvlášť, voláním funkcí obj_new() a obj_free(). Jednotlivé struct obj v poli objs by nebyly blízko u sebe v paměti, což by v určitých případech mohlo mít vliv na výkon.

Tohle je samozřejmě umělý příklad.

Nicméně tento otevřený přístup umožňuje implementovat i velmi složité programy v jazyce C. Jedním z nejvíce notorických příkladů je Linuxové jádro, které tento způsob programování v C používá velmi hojně.

Historie změn

15.7.2018 18:17První sestavení webu