以 C 語言打造 Object Model - 2

Type Informat
第一篇#define SHAPE_CLASS(c) ((ShapeClass *) (c))#define SHAPE(o) ((Shape *) (o))

它們的功用很直觀 - 將 (A *) 轉成 (B *). 要記著 - pointer 的 casting 只是改變 “看 object 的觀點”, 但並未改變 object 本身. 舉個簡單點的例子int v = 1234;int * p1 = & v;float * p2 = (float ) p1;printf(" p1 = %d, * p2 = %f, * p2 = %g\n", * p1, * p2, *p2);

執行的結果* p1 = 1234, * p2 = 0.000000, * p2 = 1.7292e-42

因為對 memory 資料的觀點不同, memory 中同一個值的意義就不同. 所以, cast 動作做得正確與否, 會深深的影響執行結果.

再以我們先前的 sample code 為例, 我能不能這麼做呢? 會發生什麼事?Circle * circle = CIRCLE(point_new());printf("%g\n", circle_get_radius(circle));

執行結果1.89367e-40

這個值哪來的? 怎麼會是這樣的執行結果? 我們可以用簡單的 code snippet 去瞭解原因printf("sizeof(Point) = %d, sizeof(Circle) = %d", sizeof(Point, sizeof(Circle)));

執行結果sizeof(Point) = 24, sizeof(Circle) = 32

再深入來看 Circle 的各 field 在 struct 中的 offsetPoint * point = POINT(point_new());printf("sizeof(point->x) = %lu, x offset = %li\n",        sizeof(point->x),        ((char *) & point->x) - (char *) point);printf("sizeof(point->y) = %lu, y offset = %li\n",        sizeof(point->y),        ((char *) & point->y) - (char *) point);printf("sizeof(circle->radius) = %lu, radius offset = %li\n",        sizeof(CIRCLE(point)->radius),        ((char *) & CIRCLE(point)->radius) - (char *) point);

執行結果sizeof(point->x) = 4, x offset = 16sizeof(point->y) = 4, y offset = 20sizeof(circle->radius) = 4, radius offset = 24

x 在 Point 中起始位置是第 16 byte, 長度為 4 byte, 所以佔用了第 16, 17, 18, 19 byte. y 則是第 20, 21, 22, 23 byte. 這己經是 Point 的最尾端了, 但我們確要求讀取 radius, 也就是第 24, 25, 26, 27 byte - 超出了 Point 所在的範圍, 雖然一般 malloc() 配置 memory 時的最小單位都大於 32 bytes 所以沒有當掉, 但這並不安全, 結果也不正確!

C++ 中, 提供了 dynamic_cast<> 供 type safty 的 casting. 它會檢查 object 所屬的 class hierarchy 跟目標 class 的相容性, OK 就傳回 object pointer, 否則傳回 NULL. 要示範這個功能, 我們再加上一個 Line classclass Line : public Shape{private:    float x1;    float y1;    float x2;    float y2;

public:    Line() : x1(0.0f), y1(0.0f), x2(0.0f), y2(0.0f) {}    float get_x1() { return x1; }    void set_x1(float x1) { this->x1 = x1; }    float get_y1() { return y1; }    void set_y1(float y1) { this->y1 = y1; }    float get_x2() { return x2; }    void set_x2(float x2) { this->x2 = x2; }    float get_y2() { return y2; }    void set_y2(float y2) { this->y2 = y2; }};

現在 class hierarchy 長成這樣 

試試 dynamic castCircle * circle = new Circle();Point * point = dynamic_cast(circle);Line * line = dynamic_cast(circle);printf("circle = %p, point = %p, line = %p", circle, point, line);執行結果circle = 0xbcd010, point = 0xbcd010, line = (nil)

因 為 Circle 是 Point 的 subclass, 可視為 Point 直接操作, 所以 dynamic cast 後得到了 address. Line 因為是 Shape 的另一分支, 不在 Circle 及 Point 這個階層上, 因此 dynamic cast 失敗.

再 來看一個常碰到的問題. 在設計 class 的 interface 時通常要愈 generic 愈好, 所以, 可能拿到手的 pointer 是 Shape 的, 但實際上是指向任何它 subclass 的 object. 假設我們沒有 Shape, Circle, Line, Point 的 source code, 但想寫個 rotate() 處理所有 shape 的旋轉操作, functon 的 prototype 可能會長成這樣void rotate(Shape * shape){    ….}

內 容呢? 嗯~ 有點 tricky, 只能用是否 dynamic cast 成功與否來確認 shape 指向的 object 確切的 class, 這己經是 C++ RTTI (RunTime Type Information) 的極限了 (因此一些如 QT 這樣的 framework 都會對 type information 進行強化)void rotate(Shape * shape){    if(dynamic_cast) {        …    }    else if(dynamic_cast) {        return;    }    else if(dynamic_cast) {        return;    }    assert(0);}

回 過頭來看我們 C 版的 Object. Object 有 ObjectClass 可供辨識所屬的 class, 裡面有記錄著 class name, superclass 是誰, instance 大小, 其實就是具體而微的 type information 了. 加個 function 用來檢查傳入的 a class 與 b class 是否相容int object_class_is(const ObjectClass * self, const ObjectClass * other){    assert(NULL != self);    assert(NULL != other);

do {        if(self == other) {            return 1;        }

self = self->parent;    }    while(self);

return 0;}

順便加個取 class name 的 functionconst char * object_class_get_name(const ObjectClass * self){    assert(self);

return self->name;}

測試程式. 我們 new 出一個 Point object, 以 shape 指向它, 然後用 object_class_is() 確認 shape 的類型. 因為有了 class, 所以能叫得出 object 的 class 名了 ObjectClass * classes[] = {        OBJECT_CLASS(& shape_class),        OBJECT_CLASS(& point_class),        OBJECT_CLASS(& circle_class),        OBJECT_CLASS(& line_class),};Shape * shape = point_new();const ObjectClass * clazz = object_get_class(shape);int i;for(i = 0; i < sizeof(classes) / sizeof(classes[0]); i ++) {    printf("%s %s %s\n",           object_class_get_name(clazz),           object_class_is(clazz, classes[i]) ? "is" : "is not",           object_class_get_name(classes[i]));}

執行結果Point is ShapePoint is PointPoint is not CirclePoint is not Line

Point 是 Shape 也是 Point, 但不是 Circle or Line, 行為正確. Source code 在此

有了這個 object_class_is(), 就可以強化如 SHAPE(), SHAPE_CLASS() 這類 macro 在轉型時的型態檢查以增加安全性. 先加入通用的部份#define TYPE_CHECK_INSTANCE_TYPE(o, c)            (object_class_is(object_get_class(o), (c)))#define TYPE_CHECK_INSTANCE_CAST(o, c, ctype)    (TYPE_CHECK_INSTANCE_TYPE(o, c) ? (ctype *) (o) : NULL)#define TYPE_INSTANCE_GET_CLASS(o, c, ctype)    (TYPE_CHECK_CLASS_CAST(object_get_class(o), c, ctype))#define TYPE_CHECK_CLASS_TYPE(c1, c2)            (object_class_is((c1), (c2)))#define TYPE_CHECK_CLASS_CAST(c1, c2, ctype)    ((const ctype *) (c1))

TYPE_CHECK_INSTANCE_TYPE(o, c) 用來檢查 o 的 class 是不是 c, 例如Shape * shape = load_shape_from_file(“shapes”);if(TYPE_CHECK_INSTANCE_TYPE(shape, & point_class)) {    printf(“Point[x=%f, y=%f]\n”,           point_get_x(POINT(shape)),           point_get_y(POINT(shape)));}

TYPE_CHECK_INSTANCE_CAST(o, c, ctype) 先進行型態檢查, 如果 o 的 class 是 c 的話, 就將 cast 成 ctype, 否則傳回 NULL. 例如Point * point = point_new();Line * line = TYPE_CHECK_INSTANCE_CAST(point, & line_class, Line);

TYPE_INSTANCE_GET_CLASS(o, c, ctype) 用來取得 o 的 class, 一樣會先進行 type checking 看是否 o 的 class 是 c 然後轉型成 ctype, 例如Point * point = point_new();const ShapeClass * clazz = TYPE_INSTANCE_GET_CLASS(point, & shape_class, ShapeClass));

TYPE_CHECK_CLASS_TYPE() 及 TYPE_CHECK_CLASS_CAST() 也是用來檢查及轉型用的 macro, 只是對像是 class.

我們可再為各 class 包裝過上面的 macro 讓使用更簡便#define OBJECT(o)            (TYPE_CHECK_INSTANCE_CAST(o, & object_class, Object))#define IS_OBJECT(o)        (TYPE_CHECK_INSTANCE_TYPE(o, & object_class))#define OBJECT_GET_CLASS(o)    (TYPE_INSTANCE_GET_CLASS(o, & object_class, ObjectClass))#define OBJECT_CLASS(c)        (TYPE_CHECK_CLASS_CAST(c, & object_class, ObjectClass))#define IS_OBJECT_CLASS(c)    (TYPE_CHECK_CLASS_TYPE(c, & object_class))

供 Shape 及 Point 用的 macro#define SHAPE(o)                (TYPE_CHECK_INSTANCE_CAST(o, & shape_class, Shape))#define IS_SHAPE(o)            (TYPE_CHECK_INSTANCE_TYPE(o, & shape_class))#define SHAPE_GET_CLASS(o)        (TYPE_INSTANCE_GET_CLASS(o, & shape_class, ShapeClass))#define SHAPE_CLASS(c)            (TYPE_CHECK_CLASS_CAST(c, & shape_class, ShapeClass))#define IS_SHAPE_CLASS(c)        (TYPE_CHECK_CLASS_TYPE(c, & shape_class))

define POINT(o)                (TYPE_CHECK_INSTANCE_CAST(o, & point_class, Point))#define IS_POINT(o)            (TYPE_CHECK_INSTANCE_TYPE(o, & point_class))#define POINT_GET_CLASS(o)        (TYPE_INSTANCE_GET_CLASS(o, & point_class, PointClass))#define POINT_CLASS(c)            (TYPE_CHECK_CLASS_CAST(c, & point_class, PointClass))#define IS_POINT_CLASS(c)        (TYPE_CHECK_CLASS_TYPE(c, & point_class))

使用起來就會像這樣Object * object = OBJECT(point_new());assert(IS_OBJECT(object));            // object 是個 Object 嗎? Yesassert(IS_SHAPE(object));            // object 是個 Shape 嗎? Yesassert(IS_POINT(object));            // object 是個 Point 嗎? Yesassert(! IS_LINE(object));            // object 是個 Line 嗎? No

Shape * shape = SHAPE(object);assert(NULL != shape);

Point * point = POINT(object);assert(NULL != point);

Line * line = LINE(object);assert(NULL == line);

Source code 在此

Type System

用 靜態的 struct 填充做為 type information 有它的好處 - class 的關係在 build time 就完成了, 沒有太多 runtime 的初始動作 (有的話也是 OS 的 loader 做掉了). 但這樣的做法有些問題沒法解決, 其中之一是 class 中 virtual function pointer 的初始.

Virtual function pointer 的初始跟 object 的初始一樣以 top-down 的方式完成. 例如 CircleClass, 會由 Object, Shape, Point 最後是 Circle 的順序完成, 以下是 draw 這個 function pointer 在 class 初始時的變化Object => NAShape => NULLPoint => point_draw()Circle => circle_draw()

最 終 CircleClass 的 draw 會指向 circle_draw(). 為什麼要這麼做? 因為當 superclass 有多個 virtual function, 在 subclass 中不一定會全部 override, 這些沒有在 subclass 中 override 的 virtual function 就會指向 suerclass 實現的版本, 被 override 的版本則指向了 subclass 實現的版本, 達成改變行為的目的. 可以看到, 同一個 virtual function pointer 會在初始的過程中被多次修改, 如果只是靜態填充就不好做到這點了.

趕工中, 之後再接著完成第二篇, source code 在此