Linux/minmax. h中的__safe_cmp宏

wsxa1bj1  于 2023-03-22  发布在  Linux
关注(0)|答案(1)|浏览(91)

__safe_cmp定义为minmax. h

#define __safe_cmp(x, y) \
    (__typecheck(x, y) && __no_side_effects(x, y))
#define __no_side_effects(x, y) \
    (__is_constexpr(x) && __is_constexpr(y))
#define __typecheck(x, y) \
    (!!(sizeof((typeof(x) *)1 == (typeof(y) *)1)))
#define __is_constexpr(x) \
    (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

据我了解,

  • 此宏用于在编译过程中进行检查,以便在运行时不会出现不兼容的意外
  • 如果在编译过程中没有警告,这个宏的结果应该总是1(true)

我还查看了Linux Kernel's __is_constexpr Macro中关于__is_constexpr的讨论
还有一些项目不清楚

  • __typecheck的返回值被sizeof Package ,目的只是将结果保留为常量表达式?也就是说==操作符不作为常量表达式返回?在c11标准6.5.9中,它说返回的类型是int

结果的类型为int。对于任何一对操作数,只有一个关系为真

  • 雌兔(type(x))1 ==(typeof(y))1总是返回1?在6.5.9中,它还说两者都是指向同一个对象的指针(包括指向对象的指针和在其开头的子对象)或函数,这意味着如果指针的值相同,则指针应该相等??即使类型不同??我测试过(int,long),(int,struct),(int,void),(long,int*)等等用这个表达式,在linux下都返回1,这个条件有false的情况吗?

5否则,至少有一个操作数是指针。如果一个操作数是指针,另一个是空指针常量,则空指针常量转换为指针的类型。如果一个操作数是指向对象类型的指针,另一个是指向void的限定或非限定版本的指针,则前者转换为后者的类型。
6两个指针比较相等当且仅当两者都是空指针,两者都是指向同一对象的指针(包括指向对象的指针和在其开头的子对象)或函数,两者都是指向同一数组对象的最后一个元素之后的一个元素的指针或者一个是指向一个数组对象末尾的指针,另一个是指向另一个数组对象的开始的指针,该数组对象恰好紧跟在地址空间中第一个数组对象之后

  • 如果第1项和第2项是正确的,sizeof(equal expr)是将“equal expr”的结果转换为整数常量表达式,为什么需要这样做?我认为sizeof是将结果转换为true(即使为false),这样宏将始终返回true。我注意到评论说保持常量表达式避免VLA警告?如果没有常量表达式结果保留,是否有任何例子?
  • 在__is_constexpr,“(long)(x)* 0 l”中,当x是常量表达式时,它将返回null指针常量,表达式将返回(int )null(使用第三个操作数的类型),在www.example.com的Linux Kernel's __is_constexpr Macro中提到6.5.15.6,如果表达式类似于(长)(x++) 01,其也应该为空,但它不是一个空指针常量,所以结果是(void )null?然后sizeof((void *))== 1(sizeof(void))。如何从代码级别验证这种差异,因为没有sizeof %p的printf似乎是相同的
#include <stdio.h>

#define check(x)      ((void *)((long)(x) * 0l))
#define _check(x)     (8 ? check(x) : (int *)8)
#define __check(x)    sizeof(*(_check(x)))

int main(int args, char** argv)
{
  int x = 0;

  printf("%p\n", check(x++));    // (nil)
  printf("%p\n", check(1));      // (nil)

  printf("%p\n", _check(x++));   // (nil)
  printf("%p\n", _check(1));     // (nil)

  printf("%ld\n", __check(x++)); // 1
  printf("%ld\n", __check(1));   // 4

  return 0;
}
  • 另一个问题是为什么在c11中第二个操作数是空指针常量时使用第三个操作数的类型作为返回类型。NULL在linux中被定义为((void )0)并且不指向任何真实的对象,为什么不直接使用void 作为返回类型?
  • 在typecheck. h中还定义了一个名为typecheck的函数。这两个函数之间的区别是什么,实现也是为了比较x和y'类型之间的指针,在编译过程中生成警告/错误,并始终返回1,这是一个常量表达式。
#define typecheck(type,x) \
({    type __dummy; \
  typeof(x) __dummy2; \
  (void)(&__dummy == &__dummy2); \  // always false?
  1; \
})

顺便问一句,除了c11,有没有什么文档或书籍来解释这些问题?
感谢您的帮助!

更新:
关于__的问题是_constexpr

int main(int args, char** argv)
{

    int x = 1;
    int a = (long)x * 0l;
    int b = (long)1 * 0l;

    return 0;
}
0000000000001129 <main>:
    1129:   f3 0f 1e fa             endbr64 
    112d:   55                      push   %rbp
    112e:   48 89 e5                mov    %rsp,%rbp
    1131:   89 7d ec                mov    %edi,-0x14(%rbp)
    1134:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
    1138:   c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)
    113f:   c7 45 f8 00 00 00 00    movl   $0x0,-0x8(%rbp)
    1146:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
    114d:   b8 00 00 00 00          mov    $0x0,%eax
    1152:   5d                      pop    %rbp
    1153:   c3                      ret

上面的代码显示,2个表达式在编译过程中被翻译为0,似乎是一样的。
在gcc(版本11.3.0)中,下面的代码可以在c-typeck. c中找到,

static bool
null_pointer_constant_p (const_tree expr)
{
  /* This should really operate on c_expr structures, but they aren't
     yet available everywhere required.  */
  tree type = TREE_TYPE (expr);
  return (TREE_CODE (expr) == INTEGER_CST
      && !TREE_OVERFLOW (expr)
      && integer_zerop (expr)
      && (INTEGRAL_TYPE_P (type)
          || (TREE_CODE (type) == POINTER_TYPE
          && VOID_TYPE_P (TREE_TYPE (type))
          && TYPE_QUALS (TREE_TYPE (type)) == TYPE_UNQUALIFIED)));
}

根据cc 1的调试结果,

  • 当检查TREE_CODE(expr)时,int a =(long)x * 0 l被视为MULT_EXPR,false
  • 当检查TREE_CODE(expr)时,int B =(long)1 * 0 l被视为INTEGER_CST,true

第二个表达式在编译时是NULL指针常量,然后似乎与三元条件运算符的规则相匹配。
我不熟悉海合会内部,不确定分析是否正确。

根据Nate Eldredge的回答更新。非常感谢Nate Eldredge的病人在细节中回答所有问题。

  • 使用sizeof的目的是避免计算expr

1.我认为两个表达式在linux平台上用gcc编译后的汇编结果是一样的,然后假设在那之后应该没有区别
1.根据答案。“==”表达式的求值结果是实现定义的,可以由编译器本身优化,而不是强制性的。在linux平台上使用gcc的结果可能看起来不错,可能与其他编译器和平台不一样

1.这里的规则是,如果结果不是决定性的,更重要的是如果结果不需要,只要保持expr不求值即可。例如,使用sizeof等。typecheck中的第3行为false,在某些情况下甚至在运行时也可能运行

  • for __is_constexpr
  1. sizeof与上面提到的相同,也区分指针类型,sizeof((void))和sizeof((int))
    1.我认为((long)(x)* 0l)应该被编译器优化,计算为0l,首先转换为(void*)0,然后expr也应该是空指针常量,根据gcc中的调试,expr不是常量表达式,因为x不是行列式,即使结果似乎是0l,然后表达式可以用来区分常量表达式或不是常量表达式
  • 当第二个操作数返回空指针常量时,为什么使用第三个操作数的类型而不是void*

1.根据答案,当类型不兼容时也发出警告
1.我认为我需要更多地考虑6.5.15p8,原始描述是如果一个操作数是空指针常量,则将使用另一个操作数的类型。(void*)((长)(x++)* 0l)",似乎返回“我想,如果空指针是常量,直接返回它(只是不使用另一个操作数的类型),如果为false,则直接返回(int*)
1.还需要更仔细地阅读6.2.7
再次感谢Nate Eldredge的耐心和友好的回复。

qhhrdooz

qhhrdooz1#

你的帖子里有很多问题。我会尽我所能解决它们。标准参考是C17 N2176,因为这是我手边的版本,但我认为C11在这些方面应该是一样的。
首先,对于__typecheck宏,需要记住的关键点是,根据语言中的定义,sizeof的操作数不被求值(6.5.3.4p2)。因此,(typeof(x) *)1 == (typeof(y) *)1的求值结果是 * 无关紧要的 *,因为它根本不被求值。
拥有此表达式的唯一意义在于,如果它们指向不兼容的类型,则违反了约束(6.5.9p2)在这种情况下,编译器必须发出诊断,这适用于表达式是否求值。换句话说,我们不关心这个表达式的 * 语义 *,只关心它的 * 语法 *。我们不希望它实际 * 执行 *,我们只希望它被 * 解析 *。
这就是使用sizeof的原因。如果我们忽略它,那么(int *)1实际上会被求值,结果是实现定义的,所以我们不想依赖它。此外,它不指向任何对象,也不是空指针,而标准(6.5.9p6)对将==应用于这样的指针时会发生什么有点不清楚。
所以我们不关心表达式(typeof(x) *)1 == (typeof(y) *)1value;typeof宏的行为只取决于它的 type,它肯定是int。因此,假设xy实际上具有兼容的类型,那么__typecheck(x,y)的值就是!!sizeof(int),因为sizeof(int)不可能是零,所以肯定是1。我假设!!的唯一原因是可预测性,以确保无论我们在什么平台上,表达式总是具有特定的值1。
关于(typeof(x) *)1 == (typeof(y) *)1是否是一个常量表达式(在6.6的意义上):不,它不是。指针表达式只能出现在常量表达式中,如果它是一个 * 地址常量 ,它必须是“一个空指针,一个指向左值的指针,指定一个静态存储时间的对象,或者一个指向函数指示符的指针”。指针(typeof(x) *)1不是这些中的任何一个。然而,如上所述,这个问题与理解__typecheck是如何工作的无关。
使用sizeof避免了替代typecheck的潜在问题。这里实际上计算了(void)(&__dummy == &__dummy2);。现在在实践中编译器优化了它,但它没有义务这样做。一个愚蠢的编译器可能实际上为变量__dummy__dummy2分配堆栈,执行指令来比较它们的地址,然后忽略结果。这是无害的,但效率低下。通过将它放在sizeof中,我们避免了这种可能性。
请注意,在您的typecheck中,(&__dummy == &__dummy2)实际上的值为0,假设x实际上具有与type兼容的类型(如果不是,那么程序是病态的,所有的赌注都被取消)。对象__dummy__dummy2显然不是同一个对象,因此,在6.5.9p6下,指向它们的指针必须比较不相等。然而,这与宏的行为无关,因为(&__dummy == &__dummy2)的值不用于确定typecheck(x,type)的值。根据gcc的规则,其语句表达式扩展(不是标准C!的一部分),({ ... })表达式的值是表达式1;在末尾的值,它只是常量1
对于__is_constexpr,我们利用(6.5.15p6):
如果第二个和第三个操作数都是指针,或者一个是空指针常量而另一个是指针,则结果类型是指向由两个操作数引用的类型的所有类型限定符限定的类型的指针。此外,如果两个操作数都是指向兼容类型或兼容类型的不同限定版本的指针,则结果类型是指向由两个操作数引用的类型的所有类型限定符限定的类型的指针。结果类型是指向复合类型的适当限定版本的指针;(
)如果一个操作数是空指针常量,则结果具有另一个操作数的类型;(**)否则,一个操作数是指向void或void限定版本的指针,在这种情况下,结果类型是指向void适当限定版本的指针。
如果x是一个整数常量表达式,那么到6.6p6,(long)x * 0l也是一个整数常量表达式,并且它的值为零。因此,它是6.3.2.3p3下的 * 空指针常量 ,因此((void *)((long)(x) * 0l))也是如此。因此,通过子句I标记(),8 ? ((void *)((long)(x) * 0l)) : (int *)8的类型为int *。因此sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))的值为sizeof(int),因此__is_constexpr(x)的值为1。

如果x不是整数常量表达式,则((void *)((long)(x) * 0l))不是空指针常量。因此第二个操作数的类型为(void *),第三个操作数的类型为(int *),并且两者都不是空指针常量。因此,under子句(**),结果的类型为void *。因此__is_constexpr(x)简化为sizeof(int) == sizeof(void)。现在标准C中没有定义sizeof(void),但gcc作为扩展将其定义为1。因此,假设sizeof(int) != 1,这在Linux支持的每个系统上都是正确的,__is_constexpr(x)的结果是0。
根据您的测试,当您执行check(1)时,结果是如上所述的空指针常量。当您执行check(x++)时,您得到的结果是将 * 非常量 * 值0转换为(void *)。这不是 * 空指针常量,因此6.3.2.3p3不适用,并且我们剩下6.3.2.3p5,它表示结果是实现定义的。GCC在这种情况下将行为定义为所有位都为零的指针,在这个平台上,它恰好是一个空指针。所以在这个实现中,check(1)check(x++)都是空指针,但原因非常不同。
同样,在__is_constexpr宏中,我们关心的是语法而不是语义。我们不关心表达式((void *)((long)(x) * 0l))的运行时值,而且它不会被求值,因为它在sizeof内部。我们关心的是 * 语法上 * 它是否是一个空指针常量。
有人可能会问为什么?:具有6.5.15p6中定义的行为。您建议由于NULL在Linux中定义为(void *)0,那么flag ? p : q具有void *类型是有意义的。我不认为这实际上在实践中有用。例如,考虑以下错误代码:

int i;
double *pd;
pd = flag ? NULL : &i;

在实际的C规则下,flag ? NULL : &i的类型为int *。因此,我们将int *赋值给double *,并获得诊断,因为它们不兼容。这很好,编译器发现了我们的错误。
在你的规则下,flag ? NULL : &i的类型是void *,代码是格式良好的,所以它编译时没有错误。当flag为true时,我们得到pd = (void *)0的效果,所以pd成为空指针:很好。但是当flag为false时,我们会得到pd = (void *)&i的效果,如果我们稍后取消引用pd,我们可能会得到崩溃或其他UB。

相关问题