--- tags: DYKC, CLANG, C LANGUAGE, preprocessor --- # [你所不知道的 C 語言](https://hackmd.io/@sysprog/c-prog/):前置處理器應用篇 *邁向專業程式設計必備技能* Copyright (**慣C**) 2016 [宅色夫](http://wiki.csie.ncku.edu.tw/User/jserv) ==[直播錄影](https://youtu.be/7gadhJcIq_o)== ## 概況 * 回顧 C99/C11 的 macro 特徵,探討 C11 新的關鍵字 **_Generic** 搭配 macro 來達到 C++ template 的作用 * 探討 C 語言程式的物件導向程式設計、抽象資料型態 (ADT) / 泛型程式設計 (Generics)、程式碼產生器、模仿其他程式語言,以及用 preprocessor 搭配多種工具程式的技巧 ## 不要小看 preprocessor 怎樣讓你的程式師朋友懷疑人生:把他代碼裡的分號換成希臘文的問號: * `;` [Greek Question Mark](http://unicode-table.com/en/037E/) * `;` [Semicolon](http://unicode-table.com/en/003B/) 把以下加進 Makefile 的 CFLAGS 中: ```shell `-D’;’=’;’` ``` 會看到以下悲劇訊息: ```shell <command-line>:0:1: error: macro names must be identifiers ``` C++,叫我如何接納你? <img style="display: block; margin: auto;" src="https://i.imgur.com/MVZVuDt.png"></img> source: [dlib](https://github.com/davisking/dlib/blob/9000d93789e5ee2a2ba86c21e4f0ddc16f2a9343/examples/dnn_introduction_ex.cpp#L71) 純 C 還是最美! ## Preprocessor 是後來才納入 C 語言的特徵 由 Dennis M. Ritchie (以下簡稱 dmr) 開發的[早期 C 語言編譯器](https://www.bell-labs.com/usr/dmr/www/primevalC.html) 沒有明確要求 function prototype 的順序。dmr 在 1972 年發展的早期 C 編譯器,原始程式碼後來被整理在名為 "last1120c" [磁帶](https://github.com/mortdeus/legacy-cc/)中,我們如果仔細看 [c00.c 這檔案](https://github.com/mortdeus/legacy-cc/blob/master/last1120c/c00.c),可發現位於第 [269 行的 mapch(c) 函式定義](https://github.com/mortdeus/legacy-cc/blob/master/last1120c/c00.c#L269),在沒有 [forward declaration](https://en.wikipedia.org/wiki/Forward_declaration) 的狀況下,就分別於第 246 行和第 261 行呼叫,奇怪吧? 而且只要瀏覽 last1120c 裡頭其他 C 語言程式後,就會發現根本沒有 `#include` 或 `#define` 這一類 C preprocessor 所支援的語法,那到底怎麼編譯呢?在回答這問題前,摘錄 Wikipedia 頁面的訊息: > As the C preprocessor can be invoked separately from the compiler with which it is supplied, it can be used separately, on different languages. > Notable examples include its use in the now-deprecated imake system and for preprocessing Fortran. 原來 C preprocessor 以獨立程式的形式存在,所以當我們用 gcc 或 cl (Microsoft 開發工具裡頭的 C 編譯器) 編譯給定的 C 程式時,會呼叫 cpp (伴隨在 gcc 專案的 C preprocessor) 一類的程式,先行展開巨集 (macro) 或施加條件編譯等操作,再來 才會出動真正的 C 語言編譯器 (在 gcc 中叫做 cc1)。值得注意的是,1972-1973 年間被稱為 "Very early C compilers" 的實作中,不存在 C preprocessor (!),當時 dmr 等人簡稱 C compiler 為 "cc",此慣例被沿用至今,而無論原始程式碼有幾個檔案,在編譯前,先用 [cat](https://www.unix.com/man-page/posix/1posix/cat/) 一類的工具,將檔案串接為單一檔案,再來執行 "cc" 以便輸出對應的組合語言,之後就可透過 assembler (組譯器,在 UNIX 稱為 "as") 轉換為目標碼,搭配 linker (在 UNIX 稱為 "ld") 則輸出執行擋。 所以說,在早期的 C 語言編譯器,強制規範 function prototype 及函式宣告的順序是完全沒有必要的,要到 1974 年 C preprocessor 才正式出現在世人面前,儘管當時的實作仍相當陽春,可參見 dmr 撰寫的〈[The Development of the C Language](https://www.bell-labs.com/usr/dmr/www/chist.html)〉。C 語言的標準化是另一段漫長的旅程,來自 Bell Labs 的火種,透過 UNIX 來到研究機構和公司行號,持續影響著你我所處的資訊社會。 你或許會好奇,function prototype 的規範有什麼好處呢?這就要從 "[Rationale for International Standard -- Programming Languages -- C](http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf)" 閱讀起,依據 C9X RATIONALE 的第 70 頁 (PDF 檔案對應於第 78 頁),提到以下的解說範例: ```cpp extern int compare(const char *string1, const char *string2); void func2(int x) { char *str1, *str2; // ... x = compare(str1, str2); // ... } ``` 編譯器裡頭的最佳化階段 (optimizer) 可從 function prototype 得知,傳遞給函式 compare 的兩個指標型態參數,由於明確標注了 "const" 修飾子,所以僅有記憶體地址的使用並讀取相對應的內容,但不會因為修改指標所指向的記憶體內容,從而沒有產生副作用 (side effect)。這樣編譯器可有更大的最佳化空間,可對照[你所不知道的 C 語言:編譯器和最佳化原理篇](https://hackmd.io/@sysprog/c-compiler-optimization),得知相關最佳化手法。 一如 C9X RATIONALE 提到,C 語言和其他受 Algol 影響的程式語言,都具備 function prototype 機制,這使得編譯時期,就能進行有效的錯誤分析和偵測。無論是 C 語言、B 語言,還是 Pascal 語言,都可追溯到 [ALGOL 60](https://en.wikipedia.org/wiki/ALGOL_60)。 ALGOL 是 Algorithmic Language (演算法使用的語言) 的縮寫,提出巢狀 (nested) 結構和一系列程式流程控制,今日我們熟知的 if-else 語法,就在 ALGOL 60 出現。ALGOL 60 和 COBOL 程式語言並列史上最早工業標準化的程式語言。 黑格爾在其 1820 年的著作「法哲學原理」提到: (德語原文) > Was vernünftig ist, das ist wirklich; > und was wirklich ist, das ist vernünftig. 英語可解讀為 "What is rational is actual and what is actual is rational.",像是 C 語言這樣的工業標準,至今仍活躍地演化,當我們回顧發展軌跡時,凡是合乎理性 (vernuftig),也就必然會出現、或成為現實 (wirklich),反過來說也成立。甚至我們可推敲 C9X RATIONALE 字裡行間,每個看似死板規則的背後,其實都可追溯出像是上方的討論。 ## 開發物件導向程式時,善用 preprocessor 可大幅簡化開發 延續「[物件導向程式設計篇](https://hackmd.io/@sysprog/c-oop)」的思考,我們可善用 preprocessor,讓程式碼更精簡、更容易維護,從而提昇程式設計思考的層面 * `#`: [Stringification/Stringizing (字串化)](https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html): **讓一個表示式變成字串**,在 [assert](http://man7.org/linux/man-pages/man3/assert.3.html) 巨集用到 * `##`: [concatenation (連結,接續)](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html) <img style="display: block; margin: auto;" src="https://i.imgur.com/JMOk7Mz.png"></img> 以 [raytracing](https://github.com/embedded2016/raytracing) (光影追蹤) 程式為例 [ [source](http://wiki.csie.ncku.edu.tw/embedded/2016q1h2) ],考慮以下 macro ([objects.h](https://github.com/embedded2016/raytracing/blob/master/objects.h)): ```cpp #define DECLARE_OBJECT(name) \ struct __##name##_node; \ typedef struct __##name##_node *name##_node; \ struct __##name##_node { \ name element; \ name##_node next; \ }; \ void append_##name(const name *X, name##_node *list); \ void delete_##name##_list(name##_node *list); DECLARE_OBJECT(light) DECLARE_OBJECT(rectangular) DECLARE_OBJECT(sphere) ``` light 在 `DECLARE_OBJECT(light)` 中會取代 name,因此會產生以下程式碼: ```cpp struct __light_node; typedef struct __light_node *light_node; struct __light_node { light element; light_node next; }; void append_light(const light *X, light_node *list); void delete_light_list(light_node *list); ``` :::info Macro 做的事情 => generate (產生) / (生成) ::: 可用 `gcc -E -P` 觀察輸出: - [ ] [_POSIX_SOURCE](https://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html) * 因為 Macro 定義的不同,會導致程式行為的不同。 * Feature Test Macros > The exact set of features available when you compile a source file is controlled by which feature test macros you define. 延伸閱讀: [圖解 JavaScript 的物件模型](https://www.facebook.com/JservFans/photos/pcb.886519028141099/886518218141180/) ```cpp typedef enum { NORTH, SOUTH, EAST, WEST} Direction; typedef struct { char *description; int (*init)(void *self); void (*describe)(void *self); void (*destroy)(void *self); void *(*move)(void *self, Direction direction); int (*attack)(void *self, int damage); } Object; int Object_init(void *self); void Object_destroy(void *self); void Object_describe(void *self); void *Object_move(void *self, Direction direction); int Object_attack(void *self, int damage); void *Object_new(size_t size, Object proto, char *description); #define NEW(T, N) Object_new(sizeof(T), T##Proto, N) #define _(N) proto.N ``` ## _Generic [C11] C11 沒有 C++ template,但有以 macro 為基礎的 type-generic functions,主要透過 **_Generic** 關鍵字。 - [ ] [C11 standard (ISO/IEC 9899:2011)](http://www.iso.org/iso/iso_catalogue/catalogue_tc/catalogue_detail.htm?csnumber=57853): 6.5.1.1 Generic selection (page: 78-79) 以下求開立方根 ([cube root](https://en.wikipedia.org/wiki/Cube_root)) 的程式 (generic.c) 展示了 _Generic 的使用: ```cpp #include <stdio.h> #include <math.h> #define cbrt(X) \ _Generic((X), \ long double: cbrtl, \ default: cbrt, \ const float: cbrtf, \ float: cbrtf \ )(X) int main(void) { double x = 8.0; const float y = 3.375; printf("cbrt(8.0) = %f\n", cbrt(x)); printf("cbrtf(3.375) = %f\n", cbrt(y)); } ``` 編譯並執行: ```shell $ gcc -std=c11 -o generic generic.c -lm $ ./generic cbrt(8.0) = 2.000000 cbrtf(3.375) = 1.500000 ``` `<math.h>` 的原型宣告: ```cpp double cbrt(double x); float cbrtf(float x); ``` * `x` (型態為 double) => cbrt(x) => selects the default cbrt * `y` (型態為 const float) => cbrt(y) => * gcc: converts const float to float, then selects cbrtf * clang: selects cbrtf for const float 注意 casting 的成本和正確性議題 C11 程式碼: ```cpp #include <stdio.h> void funci(int x) { printf("func value = %d\n", x); } void funcc(char c) { printf("func char = %c\n", c); } void funcdef(double v) { printf("Def func's value = %lf\n", v); } #define func(X) \ _Generic((X), \ int: funci, char: funcc, default: funcdef \ )(X) int main() { func(1); func('a'); func(1.3); return 0; } ``` 輸出結果是 ``` func value = 1 func value = 97 Def func's value = 1.300000 ``` 相似的 C++ 程式碼: ```cpp template <typename T> void func(T v) { cout << "Def func's value = " << v << endl; } template <> void func<int>(int x) { printf("func value = %d\n", x); } template <> void func<char>(char c) { printf("func char = %c\n", c); } ``` 對照輸出,對於 character 比對不一致,會轉型為 int。 ``` func value = 1 func char = a Def func's value = 1.3 ``` <[tgmath.h](http://pubs.opengroup.org/onlinepubs/009695399/basedefs/tgmath.h.html)> - type-generic macros [libm 的 \<tgmath.h\> 實做](http://gitweb.dragonflybsd.org/dragonfly.git/commitdiff/de03118d092c3b87f593a7c5ebd54ff2818d218c) 延伸閱讀: * [C11 - Generic Selections](http://www.robertgamble.net/2012/01/c11-generic-selections.html) * [Fun with C11 generic selection expression](https://speakerdeck.com/lichray/fun-with-c11-generic-selection-expression) * [Experimenting with _Generic() for parametric constness in C11](http://fanf.livejournal.com/144696.html) ## 應用: Unit Test Unity: Unit Test for C source: [unity](http://www.throwtheswitch.org/unity/) File: [unity/unity_fixture.h](https://github.com/ThrowTheSwitch/Unity/blob/master/extras/fixture/src/unity_fixture.h) [Google Test](https://github.com/google/googletest) > Google Mock:an extension to Google Test for writing and using C++ mock classes. File: [mock/gmock-generated-actions.h](https://github.com/google/googletest/blob/master/googlemock/include/gmock/gmock-generated-actions.h) > [metaclass](https://en.wikipedia.org/wiki/Metaclass):In object-oriented programming, a metaclass is a class whose instances are classes.產生模板的模板 > Mock Class: 給定一個表示式,產生對應的 class 和程式碼 ## 應用: Object Model [ObjectC](https://github.com/DaemonSnake/ObjectC): use as a superset of the C language adding a lot of modern concepts missing in C Files * [inc/ObjectC/std/Object.h](https://github.com/DaemonSnake/ObjectC/blob/master/inc/ObjectC/std/Object.h) * [inc/ObjectC/language/new_delete.h](https://github.com/DaemonSnake/ObjectC/blob/master/inc/ObjectC/language/new_delete.h) * [inc/ObjectC/language/type.h](https://github.com/DaemonSnake/ObjectC/blob/master/inc/ObjectC/language/type.h) C: exception jmp => setjmp + longjmp ## 應用: Exception Handling [ExtendedC](https://github.com/jspahrsummers/libextc) library extends the C programming language through complex macros and other tricks that increase productivity, safety, and code reuse without needing to use a higher-level language such as C++, Objective-C, or D. File: [include/exception.h](https://github.com/jspahrsummers/libextc/blob/master/include/exception.h) ## 應用: ADT [pearldb](https://github.com/willemt/pearldb): A Lightweight Durable HTTP Key-Value Pair Database in C * 內含 [Klib](https://github.com/willemt/pearldb/tree/master/deps/klib): a Generic Library in C * Ksort: 在標頭檔中直接展開,省去 function call 的成本,加速資料庫處理的效能。