此用法中的指针算法是否会导致未定义的行为
Does the pointer arithmetic in this usage cause undefined behavior
这是 following question 的跟进。我假设我最初使用的指针算法会导致未定义的行为。然而,一位同事告诉我,用法实际上是明确定义的。以下是一个简化的例子:
typedef struct StructA {
int a;
} StructA ;
typedef struct StructB {
StructA a;
StructA* b;
} StructB;
int main() {
StructB* original = (StructB*)malloc(sizeof(StructB));
original->a.a = 5;
original->b = &original->a;
StructB* copy = (StructB*)malloc(sizeof(StructB));
memcpy(copy, original, sizeof(StructB));
free(original);
ptrdiff_t offset = (char*)copy - (char*)original;
StructA* a = (StructA*)((char*)(copy->b) + offset);
printf("%i\n", a->a);
free(copy)
}
根据 C++11 规范的 §5.7 ¶5:
If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.
我假设代码的以下部分:
ptrdiff_t offset = (char*)copy - (char*)original;
StructA* a = (StructA*)((char*)(copy->b) + offset);
导致未定义的行为,因为它:
- 减去两个指向不同数组的指针
- 偏移计算的结果指针不再指向同一个数组。
这会导致未定义的行为,还是我误解了 C++ 规范?这同样适用于 C 语言吗?
编辑:
根据评论,我假设以下修改仍然是未定义的行为,因为对象在生命周期结束后使用:
ptrdiff_t offset = (char*)(copy->b) - (char*)original;
StructA* a = (StructA*)((char*)copy + offset);
是否会在使用索引时定义:
typedef struct StructB {
StructA a;
ptrdiff_t b_offset;
} StructB;
int main() {
StructB* original = (StructB*)malloc(sizeof(StructB));
original->a.a = 5;
original->b_offset = (char*)&(original->a) - (char*)original
StructB* copy = (StructB*)malloc(sizeof(StructB));
memcpy(copy, original, sizeof(StructB));
free(original);
StructA* a = (StructA*)((char*)copy + copy->b_offset);
printf("%i\n", a->a);
free(copy);
}
这是未定义的行为,因为对指针运算可以做什么有严格的限制。您所做的修改和建议的修改无法解决此问题。
加法中的未定义行为
StructA* a = (StructA*)((char*)copy + offset);
首先,由于添加到 copy
:
,这是未定义的行为
When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.
- (4.1) If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.
- (4.2) Otherwise, if P points to an array element i of an array object x with n elements (
[dcl.array]
), the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) array element i+j of x if 0 ≤ i + j ≤ n and the expression P - J points to the (possibly-hypothetical) array element i − j of x if 0 ≤ i − j ≤ n.
- (4.3) Otherwise, the behavior is undefined.
见https://eel.is/c++draft/expr.add#4
简而言之,对非数组和非空指针执行指针运算始终是未定义的行为。即使 copy
或其成员是数组,添加到指针上使其变为:
- 数组末尾后两个或更多
- 第一个元素前至少一个
也是未定义的行为。
减法中的未定义行为
ptrdiff_t offset = (char*)original - (char*)(copy->b);
你的两个指针相减也是未定义的行为:
When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; [...]
- (5.1) If P and Q both evaluate to null pointer values, the result is 0.
- (5.2) Otherwise, if P and Q point to, respectively, array elements i and j of the same array object x, the expression P - Q has the value i − j.
- (5.3) Otherwise, the behavior is undefined.
见https://eel.is/c++draft/expr.add#5
因此,当指针不是都为空或指向同一数组元素的指针时,将指针彼此相减是未定义的行为。
C 中的未定义行为
C标准有类似的限制:
(8) [...] If the pointer operand points to an element of
an array object, and the array is large enough, the result points to an element offset from
the original element such that the difference of the subscripts of the resulting and original
array elements equals the integer expression.
(标准没有提到非数组指针相加会发生什么)
(9) When two pointers are subtracted, both shall point to elements of the same array object,
or one past the last element of the array object; [...]
请参阅 C11 standard (n1570) 中的 §6.5.6 加法运算符。
改用数据成员指针
C++ 中一个干净且类型安全的解决方案是使用数据成员指针。
typedef struct StructB {
StructA a;
StructA StructB::*b_offset;
} StructB;
int main() {
StructB* original = (StructB*) malloc(sizeof(StructB));
original->a.a = 5;
original->b_offset = &StructB::a;
StructB* copy = (StructB*) malloc(sizeof(StructB));
memcpy(copy, original, sizeof(StructB));
free(original);
printf("%i\n", (copy->*(copy->b_offset)).a);
free(copy);
}
备注
标准引用来自 C++ 草案。您引用的 C++11 似乎对指针运算没有任何宽松的限制,它只是格式不同。参见 C++11 standard (n3337)。
该标准明确规定,在它描述为未定义行为的情况下,实现可以“以环境的文档化时尚特征”表现。根据基本原理,这种表征的目的除其他外,是为了确定“一致的语言扩展”的途径;实现何时支持此类“流行扩展”的问题是最好留给市场的实现质量问题。
许多旨在 and/or 配置用于在普通平台上进行低级编程的实现通过指定以下等价关系来扩展语言,对于 p
和 q
类型的任何指针 q
=12=] 和整数表达式 i
:
p
、(uintptr_t)p
和 (intptr_t)p
的位模式相同。
p+i
等同于 (T*)((uintptr_t)p + (uintptr_t)i * sizeof (T))
p-i
等同于 (T*)((uintptr_t)p - (uintptr_t)i * sizeof (T))
在除法没有余数的所有情况下,p-q
等同于 ((uintptr_t)p - (uintptr_t)q) / sizeof (T)
。
p>q
等同于 (uintptr_t)p > (uintptr_t)q
并且对于所有其他关系运算符和比较运算符也是如此。
该标准不承认任何始终支持这些等价的实现类别,这与不支持这些等价的实现有所不同,部分原因是它们不希望将这种支持等价的不寻常平台描述为“劣等”实现不切实际。相反,它希望在有意义的实现上支持此类实现,并且程序员会知道他们何时以此类实现为目标。为 68000 或小型 8086(自然会保持这种等价性)编写内存管理代码的人可以编写内存管理代码,这些代码将 运行 在其他系统上可互换地使用这些等价物,但有人写内存- 大型号 8086 的管理代码需要针对该平台明确设计,因为这些等价性不成立(指针为 32 位,但单个对象限制为 65520 字节,并且大多数指针操作仅作用于 a 的底部 16 位指针)。
不幸的是,即使在通常持有此类等价物的平台上,某些类型的优化也可能会产生与这些等价物所暗示的行为不同的极端情况行为。商业编译器通常坚持 C 原则的精神“不要阻止程序员做需要做的事情”,并且即使在启用大多数优化时也可以配置为坚持等价性。然而,gcc 和 clang C 编译器不允许对语义进行这种控制。当禁用所有优化时,它们将在普通平台上维持这些等价性,但没有其他优化设置可以阻止它们做出与它们不一致的推论。
这是 following question 的跟进。我假设我最初使用的指针算法会导致未定义的行为。然而,一位同事告诉我,用法实际上是明确定义的。以下是一个简化的例子:
typedef struct StructA {
int a;
} StructA ;
typedef struct StructB {
StructA a;
StructA* b;
} StructB;
int main() {
StructB* original = (StructB*)malloc(sizeof(StructB));
original->a.a = 5;
original->b = &original->a;
StructB* copy = (StructB*)malloc(sizeof(StructB));
memcpy(copy, original, sizeof(StructB));
free(original);
ptrdiff_t offset = (char*)copy - (char*)original;
StructA* a = (StructA*)((char*)(copy->b) + offset);
printf("%i\n", a->a);
free(copy)
}
根据 C++11 规范的 §5.7 ¶5:
If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.
我假设代码的以下部分:
ptrdiff_t offset = (char*)copy - (char*)original;
StructA* a = (StructA*)((char*)(copy->b) + offset);
导致未定义的行为,因为它:
- 减去两个指向不同数组的指针
- 偏移计算的结果指针不再指向同一个数组。
这会导致未定义的行为,还是我误解了 C++ 规范?这同样适用于 C 语言吗?
编辑:
根据评论,我假设以下修改仍然是未定义的行为,因为对象在生命周期结束后使用:
ptrdiff_t offset = (char*)(copy->b) - (char*)original;
StructA* a = (StructA*)((char*)copy + offset);
是否会在使用索引时定义:
typedef struct StructB {
StructA a;
ptrdiff_t b_offset;
} StructB;
int main() {
StructB* original = (StructB*)malloc(sizeof(StructB));
original->a.a = 5;
original->b_offset = (char*)&(original->a) - (char*)original
StructB* copy = (StructB*)malloc(sizeof(StructB));
memcpy(copy, original, sizeof(StructB));
free(original);
StructA* a = (StructA*)((char*)copy + copy->b_offset);
printf("%i\n", a->a);
free(copy);
}
这是未定义的行为,因为对指针运算可以做什么有严格的限制。您所做的修改和建议的修改无法解决此问题。
加法中的未定义行为
StructA* a = (StructA*)((char*)copy + offset);
首先,由于添加到 copy
:
When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.
- (4.1) If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.
- (4.2) Otherwise, if P points to an array element i of an array object x with n elements (
[dcl.array]
), the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) array element i+j of x if 0 ≤ i + j ≤ n and the expression P - J points to the (possibly-hypothetical) array element i − j of x if 0 ≤ i − j ≤ n.- (4.3) Otherwise, the behavior is undefined.
见https://eel.is/c++draft/expr.add#4
简而言之,对非数组和非空指针执行指针运算始终是未定义的行为。即使 copy
或其成员是数组,添加到指针上使其变为:
- 数组末尾后两个或更多
- 第一个元素前至少一个
也是未定义的行为。
减法中的未定义行为
ptrdiff_t offset = (char*)original - (char*)(copy->b);
你的两个指针相减也是未定义的行为:
When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; [...]
- (5.1) If P and Q both evaluate to null pointer values, the result is 0.
- (5.2) Otherwise, if P and Q point to, respectively, array elements i and j of the same array object x, the expression P - Q has the value i − j.
- (5.3) Otherwise, the behavior is undefined.
见https://eel.is/c++draft/expr.add#5
因此,当指针不是都为空或指向同一数组元素的指针时,将指针彼此相减是未定义的行为。
C 中的未定义行为
C标准有类似的限制:
(8) [...] If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression.
(标准没有提到非数组指针相加会发生什么)
(9) When two pointers are subtracted, both shall point to elements of the same array object, or one past the last element of the array object; [...]
请参阅 C11 standard (n1570) 中的 §6.5.6 加法运算符。
改用数据成员指针
C++ 中一个干净且类型安全的解决方案是使用数据成员指针。
typedef struct StructB {
StructA a;
StructA StructB::*b_offset;
} StructB;
int main() {
StructB* original = (StructB*) malloc(sizeof(StructB));
original->a.a = 5;
original->b_offset = &StructB::a;
StructB* copy = (StructB*) malloc(sizeof(StructB));
memcpy(copy, original, sizeof(StructB));
free(original);
printf("%i\n", (copy->*(copy->b_offset)).a);
free(copy);
}
备注
标准引用来自 C++ 草案。您引用的 C++11 似乎对指针运算没有任何宽松的限制,它只是格式不同。参见 C++11 standard (n3337)。
该标准明确规定,在它描述为未定义行为的情况下,实现可以“以环境的文档化时尚特征”表现。根据基本原理,这种表征的目的除其他外,是为了确定“一致的语言扩展”的途径;实现何时支持此类“流行扩展”的问题是最好留给市场的实现质量问题。
许多旨在 and/or 配置用于在普通平台上进行低级编程的实现通过指定以下等价关系来扩展语言,对于 p
和 q
类型的任何指针 q
=12=] 和整数表达式 i
:
p
、(uintptr_t)p
和(intptr_t)p
的位模式相同。p+i
等同于(T*)((uintptr_t)p + (uintptr_t)i * sizeof (T))
p-i
等同于(T*)((uintptr_t)p - (uintptr_t)i * sizeof (T))
在除法没有余数的所有情况下,p-q
等同于((uintptr_t)p - (uintptr_t)q) / sizeof (T)
。p>q
等同于(uintptr_t)p > (uintptr_t)q
并且对于所有其他关系运算符和比较运算符也是如此。
该标准不承认任何始终支持这些等价的实现类别,这与不支持这些等价的实现有所不同,部分原因是它们不希望将这种支持等价的不寻常平台描述为“劣等”实现不切实际。相反,它希望在有意义的实现上支持此类实现,并且程序员会知道他们何时以此类实现为目标。为 68000 或小型 8086(自然会保持这种等价性)编写内存管理代码的人可以编写内存管理代码,这些代码将 运行 在其他系统上可互换地使用这些等价物,但有人写内存- 大型号 8086 的管理代码需要针对该平台明确设计,因为这些等价性不成立(指针为 32 位,但单个对象限制为 65520 字节,并且大多数指针操作仅作用于 a 的底部 16 位指针)。
不幸的是,即使在通常持有此类等价物的平台上,某些类型的优化也可能会产生与这些等价物所暗示的行为不同的极端情况行为。商业编译器通常坚持 C 原则的精神“不要阻止程序员做需要做的事情”,并且即使在启用大多数优化时也可以配置为坚持等价性。然而,gcc 和 clang C 编译器不允许对语义进行这种控制。当禁用所有优化时,它们将在普通平台上维持这些等价性,但没有其他优化设置可以阻止它们做出与它们不一致的推论。