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

碼頭工人 11年前發布 | 13K 次閱讀 C 語言

<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來舉大部分例子,其他宏的語法是相似的。)

#define cos(X) __tgmath_cos(X)

編譯器會將__tgmath_cos當做內部操作符,然后將其轉換成某一個前端的函數調用。

我見過的被推選出的最簡潔的解決方法,是在編譯器前段給基本函數加上了重載支持,這可以利用運營商擴展來實現。(否則,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>的這個目的而實現它是一件非常繁雜的工作。(雖然它有可能能在C++前端上工作。)沒人想在C語言中用這樣一個笨重的擴展,何況本就沒多少程序使用<tgmath.h>,所以似乎這樣擴展編譯器有些不值得。

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

首先,讓我們實現一個選擇正確函數類型的宏吧。因為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)
....

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

  • “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擴展,聲明表達式:

#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; })

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

是嗎?

事實上并沒有。我們該怎么定義result_type?對于浮點數類型我們可以直接用__typeof__ (X),但我們又想用double作為整形參數,況且C語言并沒有一種對于類型的? :操作數,是吧。

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

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

1 ? (int *)0 : (void *)0

1 ? (int *)0 : (void *)1

以及為什么?

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

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

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

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

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

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

如果第二和第三操作數都是指針…,那么結果類型也會是一個指針…。更有,如果兩個操作數都是指向類型相兼容的指針的話…,結果類型會是一個…指向其合成類型的指針;如果一個操作數是一個空指針常量,結果類型跟另一個操作數的類型相同;否則,…結果類型是一個指向void…的指針。

因此,在下面表達式中

1 ? (int *)0 : (void *)0

第三個操作數是一個空指針常量,因此結果是(int *)0。而在

1 ? (int *)0 : (void *)1

中,第三個操作數不是一個空指針常量,因此結果是。這就是我們對于類型的條件操作符,我們只需再稍加修繕。

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

1 ? (__typeof__ (X) *)0 : (void *)(X has a integer type)

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

1 ? (int *)0 : (void *)(!(X has an integer type))

在X是整形的情況下結果是(int *)0,否則結果是(void *)0。注意到兩個情況中都有其中一個結果是(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))

這就對了。對于多于一個參數的宏來說會稍微復雜一點,不過基本概念和方法都和上面描述的一樣。

原文鏈接: Miloslav Trma?   翻譯: 伯樂在線 - Hacker_YHJ
譯文鏈接: http://blog.jobbole.com/49139/

 本文由用戶 碼頭工人 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!