C标准是否规定平台不能定义超出标准所给定的行为?

7

C标准明确规定,编译器/库组合可以随意处理以下代码:

int doubleFree(char *p)
{
  int temp = *p;
  free(p);
  free(p);
  return temp;
}

如果编译器不需要使用特定的捆绑库,那么C标准中是否有任何禁止库定义有意义行为的规定呢?以一个简单的例子来说,假设代码是为一个具有引用计数指针的平台编写的,这样,在执行p = malloc(1234); __addref(p); __addref(p);之后,前两个对free(p)的调用将会减少计数器但不会释放内存。 任何为使用这种库编写的代码自然只能与此类库一起使用(在大多数其他库上,__addref()调用可能会失败),但在许多情况下这种功能可能很有帮助,例如当需要重复将字符串传递给期望接收使用strdup生成并随后调用free的字符串的方法时。
如果某个库定义了双重释放指针等操作的有用行为,那么在C标准中是否有任何授权编译器单方面打破它的规定呢?

5
答案不是根据未定义行为的定义得出的吗?我有什么遗漏吗? - Shafik Yaghmour
7
未定义行为是未定义的。 - Billy ONeal
1
“Undefined” 就是未定义。曾经有一段时间,GCC 通过启动游戏“rogue”来实现 #pragma 指令。 - Lee Daniel Crocker
1
我不知道你在说什么。"未定义"一直意味着“此文本不是格式良好的C程序。C编译器可以自由地处理它”。如果您愿意阅读特定编译器的文档并使用一些非标准功能,那很好,但要知道自己在做什么。 - Lee Daniel Crocker
2
那么显然Linux内核和Python语言就不存在了,因为这种松散定义的编译器行为使得系统编程变得不可能。 - Lee Daniel Crocker
显示剩余16条评论
3个回答

3
这里实际上有两个问题,一个是您正式提出的问题,另一个则是您在回答其他人提出问题时所概述的更广泛的问题。
您正式提出的问题可以通过未定义行为的定义和第4节关于一致性的内容来回答。该定义如下(强调我的):
对于使用不可移植或错误的程序结构或错误数据而言,本国际标准不会强制要求的行为。
重点在于不可移植没有强制要求。这就是说,编译器可以自由地进行不愉快的优化,也可以选择使行为得到记录并明确定义,当然这意味着程序不再是严格符合的,这就带我们来到了第4节:
严格符合的程序应仅使用本国际标准中指定的语言和库功能。它不应产生依赖于任何未指定、未定义或实现定义行为的输出,并且不应超过任何最小实现限制。
但符合实现允许扩展,只要它们不会破坏符合程序即可。
一个符合标准的实现可能会有扩展(包括额外的库函数),只要它们不改变任何严格符合标准的程序的行为。如C FAQ所述:“实际上,很少有现实的、有用的、严格符合标准的程序。另一方面,仅符合标准的程序可以使用任何编译器特定的扩展。”您的非正式问题涉及编译器对未定义行为采取更积极的优化机会,从长远来看,这将使真实世界的系统编程变得不可能。虽然我理解这种相对较新的激进立场对许多程序员来说似乎很不友好,但如果人们无法使用它构建有用的程序,那么编译器将很快过时。John Regehr的相关博客文章:C的友好方言提案

有人可能会持相反的观点,认为编译器已经做出了很多努力来构建扩展以支持标准不支持的各种需求。我认为文章GCC hacks in the Linux kernel 很好地展示了这一点。它介绍了许多Linux内核依赖的gcc扩展,而clang通常也尽可能支持许多gcc扩展。

至于编译器是否删除了对未定义行为的有用处理,从而妨碍了有效的系统编程,我并不清楚。我认为针对已在系统编程中被利用且不再起作用的个别未定义行为的替代方案的具体问题将对社区有用且有趣。


UB所提供的优化机会与程序员可以自由提供给编译器的机会相比,相形见绌。尽管编译器可能无法对那些对大小和执行速度不满意而没有进行优化的程序员的代码进行积极优化,但又怎样呢?如果一个程序员更喜欢一个比实际需要大20%的程序,但他对其能够正确运行充满信心,那么编译器的��者为什么要反对呢? - supercat
我从答案中得到的信息很有道理。在编程世界中,编译器要有意义,必须执行一些最低标准并拒绝会改变或破坏这些标准的扩展。换句话说,我们都必须遵守这个最低规则集,否则编译器就从可移植性或可靠性的角度变得毫无意义。在我看来,这就是标准的作用。每隔几年,所有新奇的想法都有机会被考虑纳入标准。有些被保留,有些被拒绝,但标准仍然存在,提供了一致性。 - David C. Rankin
@DavidC.Rankin:我认为 C 标准的一个根本性弱点在于它纯粹是从需求的角度而非规范的角度编写的。考虑以下两个假设的负数左移定义:(1) 实现必须实现 x<<y,使得在后者不会溢出或越过符号位的所有情况下都产生 x*(1<<y),或者记录它可能执行其他操作;它应该但不需要记录其他操作是什么;(2) 负数左移是未定义行为。 - supercat
@DavidC.Rankin:任何符合后者的实现都可以通过在其文档中最多添加一句话来轻松地符合前者。因此,从需求的角度来看,第二个实际上是简洁地表达第一个的方式。另一方面,第一个将允许语言朝着有用结构的一致实现方向发展,而第二个则给出了当前的情况。 - supercat
@supercat,您提出了一个非常好的观点,我认为它指向了另一个问题。首先是标准本身,其所服务的目的以及对程序员的强制要求。您的例子涉及的第二个问题是,标准在当前编程环境中的使用是否足够。我从未关注起草委员会的争端,但我怀疑辩论中的一方试图阻止以任何方式扩大标准,以遏制语言的创新使用。 - David C. Rankin
显示剩余7条评论

0
如果一个库为某些操作定义了有用的行为,例如双重释放指针,那么C标准中是否有任何内容授权编译器单方面打破它?
编译器和标准库(即定义了free的库)都是实现的一部分 - 谈论其中之一单方面做出某些行动并不真正连贯。
如果编译器“不需要使用特定的捆绑库”,那么除了作为独立实现之外,它本身就不是一个实现,因此标准根本不适用于它。库和编译器的组合行为由选择将它们组合在一起的人(可能是任何一个组件的作者或其他人)负责,并将此组合标记为实现。当然,明智的做法是不要将库实现的扩展文档化为此实现的功能,而未经确认编译器不会破坏它们。同样,您还需要确保编译器不会破坏库内部使用的任何内容。
回答你的主要问题:不,它不会。如果将库、编译器(以及内核、动态加载程序等)组合的结果是符合要求的托管环境,则即使某些库作者希望提供的扩展不受组合结果的支持,它仍然是一个符合实现标准的实现,但也不需要这些扩展来工作。相反,如果结果不符合要求-例如如果编译器破坏了库的内部结构,从而导致某些库函数不符合规范-则它就不是一个符合要求的实现。任何调用同一指针两次free或使用以两个下划线开头的任何保留标识符的程序都会导致未定义行为,因此不是严格符合规范的程序。

如果一个非托管实现不包括malloc/free/realloc(我认为在这种情况下它们不是必需的),那么这是否意味着程序可以合法地任意使用这些标识符,并且编译器将不能将两个连续的free()调用视为与两个连续的fnord()调用有任何区别? - supercat
@supercat 没错,但并没有说编译器在托管实现的一部分时必须与作为独立实现的同一编译器行为相同。这样的限制是不连贯的,因为“编译器”在C标准中不存在,并且无论是由两个编译器还是由一个编译器以两种不同的模式实现,它们都是两个不同的实现。 - Random832
经过进一步思考,我认为标准实际上没有任何东西严格改变在自由实现上运行的程序保留标识符的规则。因此,“free”仍然保留用作具有外部链接的标识符。 - Random832

0
C标准是否规定平台不能定义超出标准规定的行为?
简单来说,不是这样的。标准规定如下:
“实现应该附带一份文档,定义所有实现定义和特定于区域设置的特性以及所有扩展。”
标准中没有任何限制禁止实现提供任何其他文档。如果您愿意,可以阅读ISO C标准的最新免费草案N1570,并确认没有任何此类禁止。
如果库定义了某些操作的有用行为(例如双重释放指针),那么C标准中是否有任何授权编译器单方面打破它的内容?
C实现包括编译器和标准库。free()是标准库的一部分。标准没有定义将相同的指针值传递给free()两次的行为,但是实现可以自由定义该行为。任何此类文档都不是必需的,并且超出了C标准的范围。
如果一个C实现文档说明,例如,在相同的指针值上第二次调用free()没有效果,但是这样做实际上会导致程序崩溃,那么这将违反实现自己的文档,但不会违反C标准。 C标准中没有特定的要求,规定实现必须符合其自身的文档,除了标准所要求的文档之外。实现对自己文档的一致性是由市场和常识来强制执行的,而不是由C标准来强制执行的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接