52

每 7.5,

[errno] 扩展为具有 int 类型的可修改 lvalue175),其值由几个库函数设置为正错误数。未指定 errno 是宏还是使用外部链接声明的标识符。如果为了访问实际对象而禁止宏定义,或者程序定义了名称为 errno 的标识符,则行为未定义。

175) 宏 errno 不必是对象的标识符。它可能会扩展为函数调用产生的可修改左值(例如,*errno())。

我不清楚这是否足以要求&errno不违反约束。C 语言具有左值(例如寄存器存储类变量;但是这些只能是自动的,因此errno不能这样定义),对于这些左值,&运算符是违反约束的。

如果&errno是合法的C,它是否必须是常数?

4

5 回答 5

18

所以§6.5.3.2p1 指定

一元 & 运算符的操作数应为函数指示符、[] 或一元 * 运算符的结果,或指定不是位域且未使用寄存器存储类说明符声明的对象的左值.

认为这可以理解&lvalue为对于不在这两个类别中的任何左值都可以。正如你所提到的,errno不能用 register 存储类说明符声明,我认为(虽然我现在不追逐要检查的引用)你不能有一个类型为 plain 的位域int

所以我相信规范要求&(errno)是合法的 C.

如果 &errno 是合法的 C,它是否必须是常量?

据我了解,允许errno成为宏的部分原因(以及它在例如 glibc 中的原因)是允许它成为对线程本地存储的引用,在这种情况下,它肯定不会在线程之间保持不变. 而且我看不出有任何理由期望它必须保持不变。只要 的值errno保留指定的语义,我认为不正当的 C 库没有理由不能&errno在程序过程中更改为引用不同的内存地址——例如,每次设置 时释放和重新分配后备存储errno

您可以想象维护一个由库设置的最后 N errno 值的环形缓冲区,并且&errno始终指向最新的。我认为它不会特别有用,但我看不出它有任何违反规范的方式。

于 2012-10-18T00:59:22.363 回答
16

我很惊讶还没有人引用C11 规范。为长引用道歉,但我相信它是相关的。

7.5 错误

标头定义了几个宏...

...和

errno

它扩展为具有类型int和线程本地存储持续时间的可修改左值(201),其值由几个库函数设置为正错误数。如果为了访问实际对象而禁止宏定义,或者程序定义了名称为 的标识符errno,则行为未定义。

初始线程中的值errno在程序启动时为零(errno其他线程中的初始值为不确定值),但绝不会被任何库函数设置为零。(202) errno 的值可以通过以下方式设置为非零一个库函数调用,无论是否有错误,只要 errno在本国际标准的函数描述中没有记录使用。

(201) 宏errno不必是对象的标识符。它可能会扩展为函数调用产生的可修改左值(例如,*errno())。

(202) 因此,errno用于错误检查的程序应在库函数调用之前将其设置为零,然后在后续库函数调用之前对其进行检查。当然,库函数可以保存errnoon entry 的值,然后将其设置为零,只要errno在返回之前如果 ' 的值仍然为零,则恢复原始值。

“线程本地”的意思register是out。类型int表示位域已出(IMO)。所以&errno在我看来是合法的。

持续使用诸如“它”和“价值”之类的词表明该标准的作者并没有考虑&errno成为非常数。我想人们可以想象一个&errno在特定线程中不是恒定的实现,但要按照脚注所说的方式使用(设置为零,然后在调用库函数后检查),它必须是故意对抗的,并且可能需要专门的编译器支持只是为了对抗。

简而言之,如果规范确实允许非常量&errno,我认为这不是故意的。

[更新]

R. 在评论中提出了一个很好的问题。经过思考,我相信我现在知道了他问题的正确答案,以及原始问题的正确答案。让我看看我能不能说服你,亲爱的读者。

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 这样的东西。

现在,考虑引用的int 对象的声明。errno(注意:我不是指errno name的声明,因为如果是一个宏,当然可能没有这样的声明errno。我的意思是底层int对象的声明。)

上面的语言说你可以获取一个左值的地址,除非它指定一个位域或者它指定一个对象 "declared" register。底层对象的规范errno说它是一个int具有线程本地持续时间的可修改左值。

现在,规范确实没有说errno必须声明底层对象。也许它只是通过一些实现定义的编译器魔法出现。但同样,当规范说“使用寄存器存储类说明符声明”时,它使用了自己的术语。

因此,要么底层errno对象在标准意义上被“声明”,在这种情况下,它不能既是register线程本地的;或者它根本没有被声明在这种情况下它没有被声明register。不管怎样,因为它是一个左值,你可以取它的地址。

(除非它是位域,但我认为我们同意位域不是类型的对象int。)

于 2012-10-31T03:12:30.720 回答
4

最初的实现errno是一个全局 int 变量,各种标准 C 库组件在遇到错误时用来指示错误值。然而,即使在那些日子里,人们也必须小心可重入代码或库函数调用,这些函数调用可能会errno在您处理错误时设置为不同的值。errno通常,如果由于某些其他函数或代码段显式或通过库函数调用设置值的可能性而需要任何时间长度的错误代码,则通常会将值保存在临时变量中。

因此,对于全局 int 的原始实现,使用运算符的地址并根据地址保持不变几乎内置于库的结构中。

然而,对于多线程,不再有单个全局,因为拥有单个全局不是线程安全的。因此,拥有线程本地存储的想法可能是使用返回指向已分配区域的指针的函数。因此,您可能会看到类似于以下完全虚构示例的构造:

#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将被用作单个全局变量而不是线程安全的 int 变量,errno = 0;或者像检查它if (errno == 22) { // handle the error,甚至像int *pErrno = &errno;. 这一切都行得通,因为最终线程本地数据区域被分配并保持不变并且不会四处移动,并且errno看起来像的宏定义extern int隐藏了其实际实现的管道。

我们不想要的一件事是errno在我们访问值时使用某种动态分配、克隆、删除序列在线程的时间片之间突然移动地址。当你的时间片到了,它就到了,除非你有某种同步,或者在你的时间片过期后保持 CPU 的某种方式,让线程局部区域移动对我来说似乎是一个非常冒险的提议。

这反过来意味着您可以依赖运算符的地址为您提供特定线程的常量值,尽管线程之间的常量值会有所不同。我可以很好地看到库使用的地址,errno以减少每次调用库函数时进行某种线程本地查找的开销。

在线程中将地址errno作为常量还提供了与使用 errno.h 包含文件的旧源代码的向后兼容性,正如他们应该做的那样(请参阅linux 的此手册页以获取 errno,其中明确警告不要extern int errno;像在过去)。

我阅读标准的方式是允许这种线程本地存储,同时保持与旧时使用extern int errno;时相似的语义和语法,errno并允许旧用法以及用于不支持的嵌入式设备的某种交叉编译器多线程。然而,由于使用了宏定义,语法可能相似,因此不应使用旧式快捷方式声明,因为该声明不是实际的声明errno

于 2012-11-01T03:27:14.493 回答
0

我们可以找到一个反例:因为位域可以有 type interrno所以可以是位域。在那种情况下,&errno将是无效的。标准的行为在这里没有明确说你可以写&errno,所以未定义行为的定义在这里适用。

C11 (n1570), § 4. 一致性
未定义的行为在本国际标准中以“未定义的行为”一词或省略任何明确的行为定义来表示。

于 2012-10-27T15:00:42.680 回答
0

这似乎是一个有效的实现,其中&errno会违反约束:

struct __errno_struct {
    signed int __val:12;
} *__errno_location(void);

#define errno (__errno_location()->__val)

所以我认为答案可能是否定的......

于 2013-08-22T04:28:17.187 回答