&errno 合法吗?
Posted
技术标签:
【中文标题】&errno 合法吗?【英文标题】:Is &errno legal C? 【发布时间】:2012-10-08 08:53:06 【问题描述】:每 7.5,
[errno] 扩展为具有 int 类型的可修改 lvalue175),其值由几个库函数设置为正错误数。未指定 errno 是宏还是使用外部链接声明的标识符。如果为了访问实际对象而禁止宏定义,或者程序定义了名称为 errno 的标识符,则行为未定义。
175) 宏 errno 不必是对象的标识符。它可能会扩展为函数调用产生的可修改左值(例如,*errno())。
我不清楚这是否足以要求&errno
不是违反约束的。 C 语言有左值(例如寄存器存储类变量;但是这些只能是自动的,因此不能这样定义 errno
),&
运算符是违反约束的。
如果&errno
是合法的C,是否必须是常量?
【问题讨论】:
不一定。它是一个左值,因此例如您可以分配给它。能够获取地址并不是那么遥不可及。 也许这是相关的:左值(union signed int x:32;)0.x
是什么类型? (假设 32 是 int
的宽度;根据需要替换它)。
如果它是可修改的并且类型为int
,那么获取它的地址一定是合法的吧?现在该地址以后是否有效还有待商榷;不知何故,实现可能使其仅在使用它的表达式或其他内容中有效。
一元 & 运算符的操作数应为函数指示符、[] 或一元 * 运算符的结果,或指定非位域对象的左值并且未使用寄存器存储类说明符声明。 (6.5.3.2)
@SethCarnegie:我不这么认为。明确禁止使用register
存储类获取变量的地址,但它们仍然可以用作左值。
【参考方案1】:
所以§6.5.3.2p1 指定
一元 & 运算符的操作数应该是一个函数指示符、一个 [] 或一元 * 运算符的结果,或者一个左值,它指定一个不是位域且未使用寄存器存储声明的对象-类说明符。
我认为可以理解为&lvalue
适用于不在这两个类别中的任何左值。正如你所提到的,errno
不能用寄存器存储类说明符声明,我认为(虽然我现在不追逐要检查的引用)你不能有一个类型为普通 int
的位域。
所以我相信规范要求 &(errno)
是合法的 C。
如果 &errno 是合法的 C,它是否必须是常量?
据我了解,允许 errno
成为宏(以及它在例如 glibc 中的原因)的部分原因是允许它成为对线程本地存储的引用,在这种情况下它将跨线程肯定不是恒定的。而且我看不出有任何理由期望它必须保持不变。只要 errno
的值保留指定的语义,我认为不正当的 C 库没有理由不能更改 &errno
以在程序过程中引用不同的内存地址——例如每次设置 errno
时释放并重新分配后备存储。
您可以想象维护由库设置的最后 N 个 errno 值的环形缓冲区,并让 &errno
始终指向最新的。我认为它不会特别有用,但我看不出它有任何违反规范的方式。
【讨论】:
关于地址是否恒定的话题,我知道线程之间可能会有所不同;我的想法是您是否可以依靠通过其地址存储到errno
和从errno
检索。例如,如果你有一个函数void print_int_at(int *p);
,调用print_int_at(&errno)
来打印errno
的值是否有效,或者errno
在函数读取它之前移动到不同的地址(其中如果您需要将其存储在临时文件中,或者执行类似print_int_at((int[])errno);
) 的操作
位域可以是普通的int
类型;它只是没有由语言定义是有符号还是无符号类型(当应用于位字段时)。因此,给定struct b int b0 : 1;
,不清楚该位字段的可接受值是什么。【参考方案2】:
我很惊讶还没有人引用C11 spec。为长引用道歉,但我相信它是相关的。
7.5 错误
头部定义了几个宏...
...和
errno
扩展为具有
int
类型和线程本地的可修改左值(201) 存储持续时间,其值设置为正错误数 几个库函数。如果一个 禁止宏定义以访问实际对象,或 一个程序定义了一个名为errno
的标识符,行为是 未定义。
errno
在初始线程中的值为零 程序启动(errno
在其他线程中的初始值为 不确定值),但任何库都不会将其设置为零 function.(202) errno 的值可以由库设置为非零 函数调用是否有错误,前提是使用errno
未记录在此函数的描述中 国际标准。(201) 宏
errno
不必是对象的标识符。它可能会扩展为 函数调用产生的可修改左值(例如,*errno()
)。(202) 因此,使用
errno
进行错误检查的程序应在 库函数调用,然后在随后的库函数调用之前对其进行检查。的 当然,库函数可以在输入时保存errno
的值,然后将其设置为零, 只要在errno
的值在 返回。
“线程本地”表示register
已退出。类型 int
表示位域已出 (IMO)。所以&errno
在我看来是合法的。
持续使用“它”和“ 值”等词表明该标准的作者没有考虑到 &errno
是非常量的。我想可以想象一个实现,其中&errno
在特定线程中不是恒定的,但要按照脚注所说的方式使用(设置为零,然后在调用库函数后检查),它必须是故意对抗的,并且可能需要专门的编译器支持才能成为对抗性。
简而言之,如果规范确实允许非常量 &errno
,我认为这不是故意的。
[更新]
R。在 cmets 中提出了一个很好的问题。经过思考,我相信我现在知道了他问题的正确答案,以及原始问题的正确答案。亲爱的读者,让我看看能不能说服你。
R。指出 GCC 在顶层允许这样的事情:
register int errno asm ("r37"); // line R
这会将errno
声明为寄存器r37
中保存的全局值。显然,这将是一个线程局部可修改的左值。那么,一个符合标准的 C 实现可以像这样声明errno
吗?
答案是否。当您或我使用“声明”一词时,我们通常会想到一个口语化和直观的概念。但是标准并没有口语化或直观地表达;它准确地说,并且只旨在使用定义明确的术语。在“声明”的情况下,标准本身定义了该术语;当它使用这个术语时,它使用的是它自己的定义。
通过阅读规范,您可以准确地了解“声明”是什么以及它是什么不是。换句话说,该标准描述了语言“C”。它没有描述“某种不是 C 的语言”。就标准而言,“带有扩展的 C”只是“某种不是 C 的语言”。
因此,从标准的角度来看,第 R 行 根本不是声明。它甚至不解析!不妨这样读:
long long long __Foo_e!r!r!n!o()blurfl??/**
就规范而言,这与第 R 行一样是“声明”;也就是说,根本没有。
所以,当 C11 规范说,在第 6.5.3.2 节中:
一元
&
运算符的操作数应为函数 指示符,[]
或一元*
运算符的结果,或左值 指定一个不是位字段且未声明为的对象 寄存器存储类说明符。
...它的意思是非常精确的东西,不指的是像 Line R 这样的东西。
现在,考虑errno
所指的int
对象 的声明。 (注意:我不是指errno
name 的声明,因为如果errno
是一个宏,那么当然可能没有这样的声明。我的意思是底层的声明int
对象。)
上面的语言说你可以获取一个左值的地址,除非它指定一个位域或者它指定一个“声明的”对象register
。底层 errno
对象的规范说它是一个可修改的 int
左值,具有线程本地持续时间。
现在,规范确实没有说必须声明底层的errno
对象。也许它只是通过一些实现定义的编译器魔法出现。但同样,当规范说“使用寄存器存储类说明符声明”时,它使用了自己的术语。
因此,在标准意义上“声明”了底层errno
对象,在这种情况下,它不能既是register
又是线程本地的;或者它根本没有声明,在这种情况下它没有声明register
。不管怎样,因为它是一个左值,你可以取它的地址。
(除非它是位域,但我认为我们同意位域不是int
类型的对象。)
【讨论】:
好吧,标准定义的register
无论如何都已经过时了,因为它只能用于自动存储持续时间,但 GCC 有 register
全局变量作为扩展,它们总是(固有地)线程-当地的。这会是errno
的合法实施吗?
无论如何,我仍然认为这是迄今为止信息量最大的答案之一。
@R:哇,这是一个很好的问题。一方面,全球“注册”声明在技术上在标准下格式不正确。另一方面,标准并没有说errno
本身必须以标准描述的语言实现。在扣人心弦的手上,register
豁免的地址必须指的是格式良好的程序......不是吗?整个问题都很好。
通常errno
根本不会被声明。规范实现将宏扩展为表达式,该表达式通过在函数返回的地址上使用*
运算符来计算为左值。所以我看不到声明是如何涉及的。
@R: “...并且没有使用寄存器存储类说明符声明”是可能阻止您获取左值地址的措辞。 (这里相关的不是errno
的声明,而是errno object 的声明。)“declared”一词意味着“declaration”。我将重新表述我的更新以明确这一点。【参考方案3】:
errno
的原始实现是一个全局 int 变量,各种标准 C 库组件在遇到错误时用来指示错误值。然而,即使在那些日子里,人们也必须小心reentrant code 或在处理错误时可能将errno
设置为不同值的库函数调用。通常,如果由于某些其他函数或代码段可能显式或通过库函数调用设置errno
的值而需要任何时间长度的错误代码,则通常会将值保存在临时变量中。
因此,对于全局 int 的原始实现,使用运算符的地址并根据地址保持不变几乎内置于库的结构中。
但是对于多线程,不再有单个全局,因为拥有单个全局不是线程安全的。因此,拥有thread local storage 的想法可能是使用一个返回指向分配区域的指针的函数。因此,您可能会看到类似于以下完全虚构示例的构造:
#define errno (*myErrno())
typedef struct
// various memory areas for thread local stuff
int myErrNo;
// more memory areas for thread local stuff
ThreadLocalData;
ThreadLocalData *getMyThreadData ()
ThreadLocalData *pThreadData = 0; // placeholder for the real thing
// locate the thread local data for the current thread through some means
// then return a pointer to this thread's local data for the C run time
return pThreadData;
int *myErrno ()
return &(getMyThreadData()->myErrNo);
然后 errno
将被用作单个全局变量,而不是 errno = 0;
的线程安全 int 变量,或者像 if (errno == 22) // handle the error
甚至像 int *pErrno = &errno;
这样的检查它。这一切都奏效了,因为最终线程本地数据区域被分配并保持不变,并且使errno
看起来像extern int
的宏定义隐藏了其实际实现的管道。
我们不希望的一件事是在我们访问该值时,使用某种动态分配、克隆、删除序列的线程的时间片之间突然转换errno
的地址。当你的时间片用完时,它就启动了,除非你有某种同步或某种方式在你的时间片过期后保持 CPU,否则让线程局部区域移动对我来说似乎是一个非常冒险的提议。
这反过来意味着您可以依赖运算符的地址为您提供特定线程的常量值,尽管线程之间的常量值会有所不同。我可以使用errno
的地址很好地查看库,以减少每次调用库函数时进行某种线程本地查找的开销。
在线程中将errno
的地址作为常量还提供了与使用errno.h 包含文件的旧源代码的向后兼容性,因为它们应该这样做(请参阅此man page from linux for errno,它明确警告不要使用@987654337 @ 在过去很常见)。
我阅读标准的方式是允许这种线程本地存储,同时在使用 errno
时保持与旧 extern int errno;
类似的语义和语法,并允许旧用法以及某种交叉不支持多线程的嵌入式设备的编译器。但是,由于使用了宏定义,语法可能相似,因此不应使用旧样式的快捷方式声明,因为该声明不是实际的 errno
真正的样子。
【讨论】:
【参考方案4】:我们可以找到一个反例:因为位域可以有int
类型,errno
可以是位域。在这种情况下,&errno
将无效。标准的行为在这里并没有明确说你可以写&errno
,所以未定义的行为的定义在这里适用。
C11 (n1570), § 4. 一致性 未定义的行为在本国际中另有说明 以“未定义的行为”一词或省略任何 行为的明确定义。
【讨论】:
我不清楚位域的类型是什么。是int
,还是“int
位域宽度n
”?【参考方案5】:
这似乎是一个有效的实现,其中&errno
将违反约束:
struct __errno_struct
signed int __val:12;
*__errno_location(void);
#define errno (__errno_location()->__val)
所以我认为答案可能是否定的......
【讨论】:
errno
必须有 int
类型;你的没有。
它有什么类型?据我所知,int
,当然这在直觉上似乎是错误的。这就是问题的重点。
这是一个 12 位宽的有符号位域。以上是关于&errno 合法吗?的主要内容,如果未能解决你的问题,请参考以下文章