最丑陋的C語言特性:tgmath.h
<tgmath.h>
是一個在C99引入的,標準C語言庫提供的頭文件。對于Fortran編寫的數(shù)值軟件,它向C語言提供更加簡潔的接口。
跟C語言不同,F(xiàn)ortran提供了編寫在該語言內(nèi)部的“固有函數(shù)”,其表現(xiàn)得更像操作符一樣。固有函數(shù)接受不同類型的參數(shù),并根據(jù)參數(shù)的類型返回 對應(yīng)類型的返回值。同時,F(xiàn)ortran中的普通函數(shù)(“外部函數(shù)”)的行為跟C語言中的函數(shù)類似,對類型要求嚴格(即函數(shù)參數(shù)的類型必須符合,返回值也 是固定的)。舉個例子,F(xiàn)ortran77提供了一個名為INT的函數(shù),它能夠接受Integer、Real、Double和Complex的參數(shù),并總 是返回Integer。另有一個名為SIN的函數(shù),接受Real、Double和Complex的參數(shù)并返回相同類型的值。這兩個函數(shù)僅僅是固有函數(shù)的一 小部分。
某種意義上,這個特性幫了程序員不少忙,因為即使變量類型改變了,函數(shù)調(diào)用也不需要更改。另一方面,用戶定義的函數(shù)不能像這樣工作,因此這些附加的便利性只有在不調(diào)用用戶定義函數(shù)的情況下才成立。
僅僅根據(jù)以上描述,就已經(jīng)有一些C程序員認為這個特性是丑陋的了。同樣的理由,他們認為把printf整合到C中一樣丑陋。
這個功能和其他特性在C99被整合進C語言,包含在在之前提到的中,目的是更好的支持數(shù)值計算。其中提供了三角函數(shù)和對數(shù)函數(shù),舍入相關(guān)的函數(shù)和少 數(shù)其它函數(shù)。這個頭文件定義了一系列宏,覆蓋了中已有的一些函數(shù);例如,cos宏在參數(shù)是double的時候表現(xiàn)得像cos函數(shù)一樣,參數(shù)是float時 像cosf,參數(shù)是long double時像cosl,double _Complex時像ccos,參數(shù)是float _Complex時像ccosf,參數(shù)是long double _Complex時像ccosl。最終,如果參數(shù)是任何整形,宏調(diào)用cos函數(shù),就像參數(shù)被隱式的轉(zhuǎn)換為了double類型一樣。
這個特性丑陋的第二個理由在于它試圖模仿成函數(shù),但是這個模仿不但不完美,甚至是非常危險的:如果你嘗試著將泛型宏cos當成一個參數(shù)傳遞給函數(shù),而事實上它總是被當做對應(yīng)double的cos函數(shù),因為cos后面不緊跟一個左括號的話宏根本不會展開。
最后一個被認為丑陋的理由在于,這樣的宏在嚴格意義上的C上根本不能實現(xiàn),它們需要依靠某種編譯器支持——另外,某些經(jīng)驗(例如,glibc實現(xiàn)中 bug被發(fā)現(xiàn)的速度)表明,這個特性基本上沒有使用過,因此不應(yīng)該被算作這個語言核心的一部分,尤其是它根本就不支持潛在的特性。(相比之 下,<stdarg.h>對便攜性的支持就非常的好。)
說了這么多,這個特性又丑陋有沒有實用價值,我干嘛提到它?我寫這個文章的原因是我在考察glibc的時候,發(fā)現(xiàn)它是一個如此天才的實現(xiàn)。我認為它應(yīng)該用一種更好的辦法被后人銘記,而不是像下面這樣的注釋一樣。
2000-08-01 Ulrich Drepper drepper@redhat.com
|
最直接模仿Fortran編譯器的方法是使用一個簡單的宏:(我會用cos
來舉大部分例子,其他宏的語法是相似的。)
- #define cos(X) __tgmath_cos(X)
編譯器會將__tgmath_cos
當做內(nèi)部操作符,然后將其轉(zhuǎn)換成某一個前端的函數(shù)調(diào)用。
我見過的被推選出的最簡潔的解決方法,是在編譯器前段給基本函數(shù)加上了重載支持,這可以利用運營商擴展來實現(xiàn)。(否則,C語言標準會要求編譯器檢查某個標示符的不兼容聲明。)
- #define cos(X) __tgmath_cos(X)
- #praga compiler_vendor overload __tgmath_cos
- double __tgmath_cos (double x)
- {return (cos) (x); }
- float __tgmath_cos (float x)
- {return cosf (x); }
- long double __tgmath_cos (long double x)
- {return cosl (x); }
- ...
(簡單的習題: 為什么在定義__tgmath_cos(double
)
時,cos
兩旁有括號呢?)
當然,僅僅為了<tgmath.h>
的這個目的而實現(xiàn)它是一件非常繁雜的工作。(雖然它有可能能在C++前端上工作。)沒人想在C語言中用這樣一個笨重的擴展,何況本就沒多少程序使用<tgmath.h>
,所以似乎這樣擴展編譯器有些不值得。
glibc
的實現(xiàn)必須依靠那些用已經(jīng)成熟的gcc版本推出的擴展,因此要實現(xiàn)它更加復雜了。
首先,讓我們實現(xiàn)一個選擇正確函數(shù)類型的宏吧。因為C語言不支持條件宏擴展,因此條件判斷語句需要包含在擴展代碼中。我們需要像下面這樣代碼:
- #define cos(X) \
- (X is real) ? ( \
- (X has type double \
- || X has an integer type) \
- ? (cos) (X) \
- : (X has type long double) \
- ? cosl(X) \
- : cosf (X) \
- ) : (
- (X has type double _Complex) \
- ? ccos (X)
- ....
而且,我們發(fā)現(xiàn)寫上面那樣的條件判斷語句非常簡單。
- “x is real”就是
sizeof (X) == sizeof (__real__ (X))
- “x has an integer type”就是
(typeof (X))1.1 == 1
(中等的習題:(__typeof__ (X))0.1 == 0
不正確。這是為什么呢?) (事實上,glibc
在某些情況使用了__builtin_classify_type
,一種嵌入式的內(nèi)部gcc
,而在上述情況使用了另一種相似的替代。) - “x has type
double/long double/float
“也能被sizeof
區(qū)分。但在有些硬件結(jié)構(gòu)下,一些C類型被映射成相同的硬件類型,這時區(qū)分的結(jié)果可能那么精確,不過在這些硬件結(jié)構(gòu)下這些不同類型的運算都沒有差別,而且外部的C語言也不能識別出差別了。就C語言的”as-if”原則來說,這算是相當不錯的了。
好的,這樣一來我們的cos
宏就能選擇正確的函數(shù)來調(diào)用了。不過不幸的是,它總是返回long double _Complex
類型的結(jié)果。原因在于,? :
操作符的返回值的類型會是第二和第三操作數(shù)類型的“常用算術(shù)轉(zhuǎn)換”。
我們能夠避免這些類型轉(zhuǎn)換來使用我們自己選擇的類型,這需要另一個gcc擴展,聲明表達式:
- #define cos(X) ({ result_type __var; \
- if (X is real) { \
- if ((X has type double) \
- || (X has an integer type)) { \
- __var = (cos) (X); \
- else if (X has type long double) \
- __var = cosl (X); \
- ...
- __var; })
#p#
現(xiàn)在,這個宏的結(jié)果永遠會是result_type
,問題引刃而解。
是嗎?
事實上并沒有。我們該怎么定義result_type
?對于浮點數(shù)類型我們可以直接用__typeof__ (X)
,但我們又想用double
作為整形參數(shù),況且C語言并沒有一種對于類型的? :
操作數(shù),是吧。
前兩個練習放在那兒,并不是因為我是個老師,想檢查一下你的進度。它們是為了最后最有難度的習題準備的——或者是為了在你到這里之前就把你嚇跑。 (好吧,我想我已經(jīng)把大家都無聊死了,沒人能讀到這兒了。)雖然這個習題的上下文提示的已經(jīng)夠多了,也可能仍然不足以解答,來看看吧:
困難的習題:以下兩個結(jié)果有何不同?
- 1 ? (int *)0 : (void *)0
和
- 1 ? (int *)0 : (void *)1
以及為什么?
不像之前的兩個習題稍作研究和思考就能解決,這個習題(尤其是為什么的部分)有可能要求你閱讀C語言標準,因此我在這里做出解釋。
首先,解釋一下概念是必要的:
- 從編譯器的角度來說,一個整形常數(shù)表達式就是一個整形表達式有一個常數(shù)值:編譯器能夠計算這個常數(shù)而不用任何除了常數(shù)合并以外的優(yōu)化。尤其是這個表達式不會用到任何其他變量的值。
- 空指針就是一個值等于整數(shù)值0的指針??罩羔樐軌蚴侨魏晤愋偷闹羔?。
- 空指針常量是一種句法結(jié)構(gòu)??罩羔槼A康闹翟谵D(zhuǎn)換成一個指針類型時,是一個空指針(“空指針”和“它的值”都在上文說過了)??蘸暾归_成空指針。
因為空指針常量是一種句法結(jié)構(gòu),它就有一個句法定義,它要不是一個等于零的整型常量,要不一個轉(zhuǎn)換成void *
的表達式。舉個例子,0, 0L, 1 - 1, (((1 - 1))),
(void *)
和(void *)(1 - 1)
都是空指針常量,但(int *)0
和(void *)1
就不是。
(其實,當其定義為一個表達式的值時,它就不是一個句法結(jié)構(gòu)了。不過最好就這樣假裝它是個句法結(jié)構(gòu),因為大部分情況下,“值為零的整型常量表達式”其實就是字面上的0。)
現(xiàn)在我們來看看C語言標準的6.5.15部分的第六段,這部分講到了條件操作符? :
,有以下內(nèi)容:
如果第二和第三操作數(shù)都是指針…,那么結(jié)果類型也會是一個指針…。更有,如果兩個操作數(shù)都是指向類型相兼容的指針的話…,結(jié)果類型會是一個…指向其合成類型的指針;如果一個操作數(shù)是一個空指針常量,結(jié)果類型跟另一個操作數(shù)的類型相同;否則,…結(jié)果類型是一個指向void
…的指針。
因此,在下面表達式中
- 1 ? (int *)0 : (void *)0
第三個操作數(shù)是一個空指針常量,因此結(jié)果是(int *)0
。而在
- 1 ? (int *)0 : (void *)1
中,第三個操作數(shù)不是一個空指針常量,因此結(jié)果是。這就是我們對于類型的條件操作符,我們只需再稍加修繕。
注意到這個表達式(其中X是個整形)是一個整形常量表達式。
- 1 ? (__typeof__ (X) *)0 : (void *)(X has a integer type)
因此,如果X是一個整形變量,結(jié)果就是(void *)0
,否則就是。而下面這個式子。
- 1 ? (int *)0 : (void *)(!(X has an integer type))
在X是整形的情況下結(jié)果是(int *)0
,否則結(jié)果是(void *)0
。注意到兩個情況中都有其中一個結(jié)果是(void *)0
。
我們定義上面兩個表達式分別為E1和E2,那么,以下表達式:
- 1 ? (__typeof__ (E1))0 : (__typeof__ (E2))0
在x是整形的時候為(int *)0
,否則為(__typeof__ (X) *)0
。同上,我們注意到有一個表達式總是空指針常量。
最后,我們定義result_type
為:
- __typeof__ (*(1 ? (__typeof__ (E1))0 : (__typeof__ (E2))0))
原文鏈接:http://carolina.mff.cuni.cz/%7Etrmac/blog/2005/the-ugliest-c-feature-tgmathh/?foo