MSVC实施的例子:
#define offsetof(s,m) \
(size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))
// ^^^^^^^^^^^
可以看出,它取消引用了一个空指针,它通常会调用未定义的行为。这是规则的例外还是正在发生的事情?
答案 0 :(得分:25)
如果语言标准显示“未定义的行为”,则任何给定的编译器都可以定义行为。标准库中的实现代码通常依赖于此。所以有两个问题:
(1)代码UB是否与C ++标准有关?
这是一个非常难的问题,因为它是一个众所周知的几乎缺陷,C ++ 98/03标准从未在规范性文本中说明,一般来说,取消引用空指针是UB。 typeid
的例外是暗示,其中不是 UB。
你可以肯定地说,使用非{POD类型的offsetof
是UB。
(2)代码UB是针对它编写的编译器吗?
不,当然不是。
给定编译器的编译器供应商代码可以使用该编译器的任何功能。
干杯&amp;第h。,
答案 1 :(得分:15)
“未定义行为”的概念不适用于标准库的实现,无论它是宏,函数还是其他任何东西。
一般情况下,不应将标准库视为以C ++(或C)语言实现。这也适用于标准头文件。标准库应符合其外部规范,但其他所有内容都是实现细节,不受语言的所有和任何其他要求的约束。标准库应始终被认为是以某种“内部”语言实现的,这种语言可能与C ++或C非常相似,但仍然不是C ++或C.
换句话说,您引用的宏不会产生未定义的行为,只要它特别是标准库中定义的offsetof
宏。但是如果你在代码中做了完全相同的事情(比如以相同的方式定义自己的宏),它确实会导致未定义的行为。 “Quod licet Jovi,non licet bovi”。
答案 2 :(得分: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;
}
如果src
和dest
是不相关的指针,dest-src
的计算将产生未定义的行为。但是,在某些平台上,char*
和ptrdiff_t
之间的关系是任意char* p1, p2
,计算p1 + (p2-p1);
将始终等于p2
。在提供这种保证的平台上,strcpy
的上述实现将是合法的(并且在某些此类平台上可能比任何合理的替代方案更快)。但是,在某些其他平台上,除非两个字符串都是同一个已分配对象的一部分,否则这样的函数可能总是会失败。
同样的原则适用于offsetof
宏。没有要求编译器提供任何方式来获得等同于offsetof
的行为(除了实际使用该宏之外)如果编译器的指针算法模型可以通过使用获得所需的offsetof
行为空指针上的->
运算符,那么它的offsetof
宏可以做到这一点。如果编译器不支持对除了指向该类型实例的合法指针之外的其他内容使用->
的任何努力,那么它可能需要定义一个可以计算字段偏移并定义{{1使用它的宏。重要的不是标准定义了使用标准库宏和函数执行的操作的行为,而是实现确保这些宏和函数的行为符合要求。
答案 3 :(得分:1)
这基本上相当于询问这是否是UB:
s* p = 0;
volatile auto& r = p->m;
显然,r
的目标没有生成内存访问,因为它是volatile
,并且禁止编译器生成对volatile
变量的虚假访问。但*s
不易变,因此编译器可能会生成对它的访问。无论是address-of运算符还是强制转换为引用类型都不会根据标准创建未评估的上下文。
所以,我没有看到volatile
的任何理由,我同意其他人认为这是根据标准的未定义行为。当然,允许任何编译器定义标准使其实现指定或未定义的行为。
最后,[dcl.ref]
部分中的说明
特别是,在一个定义良好的程序中不能存在空引用,因为创建这样一个引用的唯一方法是将它绑定到通过解引用空指针获得的“对象”,这会导致未定义的行为。 / p>
答案 4 :(得分:0)
如果m
在结构s
中的偏移量为0,以及在某些其他情况下,这不是C ++中的未定义行为。根据{{3}}(强调我):
一元*运算符执行间接操作:应用该表达式的表达式应为指向对象类型的指针或为函数类型的指针,并且结果为指向表达式所指向的对象或函数的左值(如果有)。 如果指针是空指针值(7.11 [conv.ptr])或指向数组的最后一个元素对象(8.7 [expr.add]),结果为一个空的左值,并且不引用任何对象或函数。空的左值不可修改。
因此,仅当&((s *)0)->m
既不位于偏移量0,也不位于与数组对象的最后一个元素之后的地址对应的偏移量时,m
才是未定义的行为。请注意,允许Issue 232向null
添加0偏移量,但在C语言中则不允许。
正如其他人指出的那样,编译器被允许(并且极有可能)永远不会创建未定义的行为,并且可以与使用特定编译器的增强规范的库一起打包。
答案 5 :(得分:-3)
不,这不是未定义的行为。表达式在运行时解析。
请注意,它从空指针获取成员m
的地址。它不会取消引用空指针。