以 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 是 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
回 過頭來看我們 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 在此