欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C语言编程笔记丨最丑陋的C语言特性:tgmath.h

程序员文章站 2022-05-18 22:37:52
是一个在C99引入的标准C语言库提供的头文件。对于Fortran编写的数值软件,它向C语言提供更加简洁的接口。 跟C语言不同,Fortran提供了编写在该语言内部的“固有函数”,其表现得更像操作符一样。固有函数接受不同类型的参数,并根据参数的类型返回对应类型的返回值。同时,For ......

<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的时候,发现它是一个如此天才的实现。我认为它应该用一种更好的办法被后人铭记,而不是像下面这样的注释一样。

ulrich drepper

joseph s. myers

* 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 typedouble/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语言并没有一种对于类型的? :操作数,是吧。

前两个练习放在那儿,并不是因为我是个老师,想检查一下你的进度。它们是为了最后最有难度的习题准备的——或者是为了在你到这里之前就把你吓跑。(好吧,我想我已经把大家都无聊死了,没人能读到这儿了。)虽然这个习题的上下文提示的已经够多了,也可能仍然不足以解答,来看看吧:

困难的习题:以下两个结果有何不同?

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

 ? (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…的指针。

因此,在下面表达式中

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

第三个操作数是一个空指针常量,因此结果是(int *)0。而在

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

中,第三个操作数不是一个空指针常量,因此结果是。这就是我们对于类型的条件操作符,我们只需再稍加修缮。

注意到这个表达式(其中x是个整形)是一个整形常量表达式。

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

因此,如果x是一个整形变量,结果就是(void *)0,否则就是。而下面这个式子。

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

在x是整形的情况下结果是(int *)0,否则结果是(void *)0。注意到两个情况中都有其中一个结果是(void *)0。

我们定义上面两个表达式分别为e1和e2,那么,以下表达式:

 ? (__typeof__ (e1))0 : (__typeof__ (e2))0

在x是整形的时候为(int *)0,否则为(__typeof__ (x) *)0。同上,我们注意到有一个表达式总是空指针常量。

最后,我们定义result_type为:

__typeof__ (*(1 ? (__typeof__ (e1))0 : (__typeof__ (e2))0))

这就对了。对于多于一个参数的宏来说会稍微复杂一点,不过基本概念和方法都和上面描述的一样。

博主是一个有着7年工作经验的架构师,对于c++,自己有做资料的整合,一个完整学习c语言c++的路线,学习资料和工具。可以进我的q群7418,18652领取,免费送给大家。希望你也能凭自己的努力,成为下一个优秀的程序员!另外博主的微信公众号是:c语言编程学习基地,欢迎关注!