18

MSVC 的实现示例:

#define offsetof(s,m) \
    (size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))
//                                                   ^^^^^^^^^^^

可以看出,它取消引用一个空指针,这通常会调用未定义的行为。这是规则的例外还是发生了什么?

4

6 回答 6

25

在语言标准说“未定义的行为”的地方,任何给定的编译器都可以定义行为。标准库中的实现代码通常依赖于此。所以有两个问题:

(1) 代码是否符合 C++ 标准?

这是一个非常困难的问题,因为这是一个众所周知的几乎缺陷,C++98/03 标准从未在规范文本中直接说明,通常它是取消引用空指针的 UB。它不是UB的例外情况所暗示的。typeid

您可以肯定地说,它是offsetof与非 POD 类型一起使用的 UB。

(2) 代码相对于它所针对的编译器而言是 UB 吗?

不,当然不是。

给定编译器的编译器供应商代码可以使用该编译器的任何功能。

干杯&hth.,

于 2011-06-22T00:54:21.840 回答
16

“未定义行为”的概念不适用于标准库的实现,无论它是宏、函数还是其他任何东西。

在一般情况下,不应将标准库视为用 C++(或 C)语言实现的。这也适用于标准头文件。标准库应该符合其外部规范,但其他一切都是实现细节,不受语言的所有和任何其他要求。标准库应该始终被认为是用某种“内部”语言实现的,它可能与 C++ 或 C 非常相似,但仍然不是 C++ 或 C。

换句话说,您引用的宏不会产生未定义的行为,只要它是offsetof标准库中专门定义的宏。但是如果你在你的代码中做同样的事情(比如以同样的方式定义你自己的宏),它确实会导致未定义的行为。“Quod licet Jovi,非 licet bovi”。

于 2011-06-22T01:06:08.773 回答
4

当 C 标准指定某些操作调用未定义行为时,这通常并不意味着此类操作被禁止,而是实现可以自由地指定后续行为或不按照他们认为合适的方式。因此,在标准要求定义行为的情况下,实现可以自由地执行此类操作,当且仅当实现可以保证这些操作的行为将与标准要求的行为一致。例如,考虑以下 strcpy 的实现:

char *strcpy(char *dest, char const *src)
{
  ptrdiff_t diff = dest-src-1;
  int ch;
  while((ch = *src++) != 0)
    src[diff] = ch;
  return dest;
}

如果srcdest是不相关的指针,计算dest-src将产生未定义的行为。char*然而,在某些平台上,和之间的关系ptrdiff_t使得给定 any char* p1, p2,计算p1 + (p2-p1);将始终等于p2。在做出该保证的平台上,上述实现strcpy将是合法的(并且在某些此类平台上可能比任何可能的替代方案更快)。然而,在其他一些平台上,这样的函数可能总是失败,除非两个字符串都是同一个分配对象的一部分。

同样的原则也适用于offsetof宏。不要求编译器提供任何方法来获得等同于(offsetof除了实际使用该宏)的offsetof行为可以做到这一点。如果编译器不支持使用除指向该类型实例的合法指针之外的任何东西,那么它可能需要定义一个可以计算字段偏移量并定义->offsetof->offsetof宏来使用它。重要的不是标准定义使用标准库宏和函数执行的操作的行为,而是实现确保这些宏和函数的行为符合要求。

于 2015-08-09T19:55:34.647 回答
1

这基本上相当于问这是不是UB:

s* p = 0;
volatile auto& r = p->m;

显然,不会对 的目标生成内存访问r,因为它volatile和编译器被禁止生成对volatile变量的虚假访问。But*s不是易失性的,因此编译器可能会生成对它的访问。根据标准,地址操作符和转换为引用类型都不会创建未评估的上下文

因此,我看不出有任何理由volatile,并且我同意其他人的观点,即根据标准这是未定义的行为。当然,任何编译器都可以定义标准使其实现指定或未定义的行为。

最后,部分中的注释[dcl.ref]

特别是,在定义良好的程序中不能存在空引用,因为创建此类引用的唯一方法是将其绑定到通过取消引用空指针获得的“对象”,这会导致未定义的行为。

于 2011-06-22T02:03:33.457 回答
0

m如果在结构中的偏移量 0 处s,以及在某些其他情况下,这不是 C++ 中未定义的行为。根据第 232 期(重点是我的):

一元 * 运算符执行间接:应用它的表达式应该是一个指向对象类型的指针,或者是一个指向函数类型的指针,结果是一个左值,指向表达式指向的对象或函数,如果有的话. 如果指针是空指针值(7.11 [conv.ptr])或指向数组对象的最后一个元素 (8.7 [expr.add]),则结果为空左值并且不引用任何对象或功能。空的左值是不可修改的。

因此,&((s *)0)->m只有当m它既不在偏移量 0 处,也不在与数组对象最后一个元素之后的地址相对应的偏移量处,才是未定义的行为。请注意,添加 0 偏移量在 C++null中是允许的,但在 C 中是不允许的。

正如其他人所指出的,编译器被允许(并且极有可能)永远不会创建未定义的行为,并且可能与使用特定编译器增强规范的库一起打包。

于 2019-03-01T06:40:36.073 回答
-3

不,这不是未定义的行为。表达式在运行时解析。

请注意,它m从空指针中获取成员的地址。它不是取消引用空指针。

于 2011-06-21T23:55:35.137 回答