最醜陋的C語言特性:tgmath.h

Hacker_YHJ發表於2013-10-10

<tgmath.h>是一個在C99引入的,標準C語言庫提供的標頭檔案。對於Fortran編寫的數值軟體,它向C語言提供更加簡潔的介面。

跟C語言不同,Fortran提供了編寫在該語言內部的“固有函式”,其表現得更像操作符一樣。固有函式接受不同型別的引數,並根據引數的型別返回對應型別的返回值。同時,Fortran中的普通函式(“外部函式”)的行為跟C語言中的函式類似,對型別要求嚴格(即函式引數的型別必須符合,返回值也是固定的)。舉個例子,Fortran77提供了一個名為INT的函式,它能夠接受Integer、Real、Double和Complex的引數,並總是返回Integer。另有一個名為SIN的函式,接受Real、Double和Complex的引數並返回相同型別的值。這兩個函式僅僅是固有函式的一小部分。

某種意義上,這個特性幫了程式設計師不少忙,因為即使變數型別改變了,函式呼叫也不需要更改。另一方面,使用者定義的函式不能像這樣工作,因此這些附加的便利性只有在不呼叫使用者定義函式的情況下才成立。

僅僅根據以上描述,就已經有一些C程式設計師認為這個特性是醜陋的了。同樣的理由,他們認為把printf整合到C中一樣醜陋。

這個功能和其他特性在C99被整合進C語言,包含在在之前提到的中,目的是更好的支援數值計算。其中提供了三角函式和對數函式,舍入相關的函式和少數其它函式。這個標頭檔案定義了一系列巨集,覆蓋了中已有的一些函式;例如,cos巨集在引數是double的時候表現得像cos函式一樣,引數是float時像cosf,引數是long double時像cosl,double _Complex時像ccos,引數是float _Complex時像ccosf,引數是long double _Complex時像ccosl。最終,如果引數是任何整形,巨集呼叫cos函式,就像引數被隱式的轉換為了double型別一樣。

這個特性醜陋的第二個理由在於它試圖模仿成函式,但是這個模仿不但不完美,甚至是非常危險的:如果你嘗試著將泛型巨集cos當成一個引數傳遞給函式,而事實上它總是被當做對應double的cos函式,因為cos後面不緊跟一個左括號的話巨集根本不會展開。

最後一個被認為醜陋的理由在於,這樣的巨集在嚴格意義上的C上根本不能實現,它們需要依靠某種編譯器支援——另外,某些經驗(例如,glibc實現中bug被發現的速度)表明,這個特性基本上沒有使用過,因此不應該被算作這個語言核心的一部分,尤其是它根本就不支援潛在的特性。(相比之下,<stdarg.h>對便攜性的支援就非常的好。)

說了這麼多,這個特性又醜陋有沒有實用價值,我幹嘛提到它?我寫這個文章的原因是我在考察glibc的時候,發現它是一個如此天才的實現。我認為它應該用一種更好的辦法被後人銘記,而不是像下面這樣的註釋一樣。

2000-08-01 Ulrich Drepper drepper@redhat.com

Joseph S. Myers jsm28@cam.ac.hk

* math/tgmah.h: Make standard compliant. Don’t ask how.

最直接模仿Fortran編譯器的方法是使用一個簡單的巨集:(我會用cos來舉大部分例子,其他巨集的語法是相似的。)

編譯器會將__tgmath_cos當做內部操作符,然後將其轉換成某一個前端的函式呼叫。

我見過的被推選出的最簡潔的解決方法,是在編譯器前段給基本函式加上了過載支援,這可以利用運營商擴充套件來實現。(否則,C語言標準會要求編譯器檢查某個標示符的不相容宣告。)

(簡單的習題: 為什麼在定義__tgmath_cos(double)時,cos兩旁有括號呢?)

當然,僅僅為了<tgmath.h>的這個目的而實現它是一件非常繁雜的工作。(雖然它有可能能在C++前端上工作。)沒人想在C語言中用這樣一個笨重的擴充套件,何況本就沒多少程式使用<tgmath.h>,所以似乎這樣擴充套件編譯器有些不值得。

glibc的實現必須依靠那些用已經成熟的gcc版本推出的擴充套件,因此要實現它更加複雜了。

首先,讓我們實現一個選擇正確函式型別的巨集吧。因為C語言不支援條件巨集擴充套件,因此條件判斷語句需要包含在擴充套件程式碼中。我們需要像下面這樣程式碼:

而且,我們發現寫上面那樣的條件判斷語句非常簡單。

  • “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,一種嵌入式的內部gcc,而在上述情況使用了另一種相似的替代。)
  • “x has type double/long double/float“也能被sizeof區分。但在有些硬體結構下,一些C型別被對映成相同的硬體型別,這時區分的結果可能那麼精確,不過在這些硬體結構下這些不同型別的運算都沒有差別,而且外部的C語言也不能識別出差別了。就C語言的”as-if”原則來說,這算是相當不錯的了。

好的,這樣一來我們的cos巨集就能選擇正確的函式來呼叫了。不過不幸的是,它總是返回long double _Complex型別的結果。原因在於,? :操作符的返回值的型別會是第二和第三運算元型別的“常用算術轉換”。

我們能夠避免這些型別轉換來使用我們自己選擇的型別,這需要另一個gcc擴充套件,宣告表示式:

現在,這個巨集的結果永遠會是result_type,問題引刃而解。

是嗎?

事實上並沒有。我們該怎麼定義result_type?對於浮點數型別我們可以直接用__typeof__ (X),但我們又想用double作為整形引數,況且C語言並沒有一種對於型別的? :運算元,是吧。

前兩個練習放在那兒,並不是因為我是個老師,想檢查一下你的進度。它們是為了最後最有難度的習題準備的——或者是為了在你到這裡之前就把你嚇跑。(好吧,我想我已經把大家都無聊死了,沒人能讀到這兒了。)雖然這個習題的上下文提示的已經夠多了,也可能仍然不足以解答,來看看吧:

困難的習題:以下兩個結果有何不同?

以及為什麼?

不像之前的兩個習題稍作研究和思考就能解決,這個習題(尤其是為什麼的部分)有可能要求你閱讀C語言標準,因此我在這裡做出解釋。

首先,解釋一下概念是必要的:

  • 從編譯器的角度來說,一個整形常數表示式就是一個整形表示式有一個常數值:編譯器能夠計算這個常數而不用任何除了常數合併以外的優化。尤其是這個表示式不會用到任何其他變數的值。
  • 空指標就是一個值等於整數值0的指標。空指標能夠是任何型別的指標。
  • 空指標常量是一種句法結構。空指標常量的值在轉換成一個指標型別時,是一個空指標(“空指標”和“它的值”都在上文說過了)。空巨集展開成空指標。

因為空指標常量是一種句法結構,它就有一個句法定義,它要不是一個等於零的整型常量,要不一個轉換成void *的表示式。舉個例子,0, 0L, 1 - 1, (((1 - 1))), (void *)(void *)(1 - 1)都是空指標常量,但(int *)0(void *)1就不是。

(其實,當其定義為一個表示式的值時,它就不是一個句法結構了。不過最好就這樣假裝它是個句法結構,因為大部分情況下,“值為零的整型常量表示式”其實就是字面上的0。)

現在我們來看看C語言標準的6.5.15部分的第六段,這部分講到了條件操作符? :,有以下內容:

如果第二和第三運算元都是指標…,那麼結果型別也會是一個指標…。更有,如果兩個運算元都是指向型別相相容的指標的話…,結果型別會是一個…指向其合成型別的指標;如果一個運算元是一個空指標常量,結果型別跟另一個運算元的型別相同;否則,…結果型別是一個指向void…的指標。

因此,在下面表示式中

第三個運算元是一個空指標常量,因此結果是(int *)0。而在

中,第三個運算元不是一個空指標常量,因此結果是。這就是我們對於型別的條件操作符,我們只需再稍加修繕。

注意到這個表示式(其中X是個整形)是一個整形常量表示式。

因此,如果X是一個整形變數,結果就是(void *)0,否則就是。而下面這個式子。

在X是整形的情況下結果是(int *)0,否則結果是(void *)0。注意到兩個情況中都有其中一個結果是(void *)0

我們定義上面兩個表示式分別為E1和E2,那麼,以下表示式:

在x是整形的時候為(int *)0,否則為(__typeof__ (X) *)0。同上,我們注意到有一個表示式總是空指標常量。

最後,我們定義result_type為:

這就對了。對於多於一個引數的巨集來說會稍微複雜一點,不過基本概念和方法都和上面描述的一樣。

相關文章