通过定义明确的 offsetof 访问成员吗?

Is accessing members through offsetof well defined?

在使用 offsetof 进行指针运算时,获取结构的地址、向其添加成员的偏移量,然后取消引用该地址以获取底层成员是否是明确定义的行为?

考虑以下示例:

#include <stddef.h>
#include <stdio.h>

typedef struct {
    const char* a;
    const char* b;
} A;

int main() {
    A test[3] = {
        {.a = "Hello", .b = "there."},
        {.a = "How are", .b = "you?"},
        {.a = "I\'m", .b = "fine."}};

    for (size_t i = 0; i < 3; ++i) {
        char* ptr = (char*) &test[i];
        ptr += offsetof(A, b);
        printf("%s\n", *(char**)ptr);
    }
}

这应该在三个连续的行上打印 "there."、"you?" 和 "fine.",目前它对 clang 和 gcc 都这样做,因为您可以在 [=12= 上验证自己].但是,我不确定这些指针转换和算术中的任何一个是否违反了一些会导致行为变得未定义的规则。

据我所知,这是定义明确的行为。但只是因为您通过 char 类型访问数据。如果您使用其他一些指针类型来访问该结构,它将是 "strict aliasing violation".

严格来说,越界访问数组是不明确的,但是明确的使用字符类型指针来抓取任意字节在结构之外。通过使用 offsetof 你保证这个字节不是填充字节(这可能意味着你会得到一个不确定的值)。

但是请注意,放弃 const 限定符确实会导致定义不明确的行为。

编辑

同样,强制转换 (char**)ptr 是一个无效的指针转换——这本身就是未定义的行为,因为它违反了严格的别名。变量 ptr 本身被声明为 char*,所以你不能骗编译器说 "hey, this is actually a char**",因为它不是。这与 ptr 指向的内容无关。

我相信没有定义不当的行为的正确代码应该是这样的:

#include <stddef.h>
#include <stdio.h>
#include <string.h>

typedef struct {
    const char* a;
    const char* b;
} A;

int main() {
    A test[3] = {
        {.a = "Hello", .b = "there."},
        {.a = "How are", .b = "you?"},
        {.a = "I\'m", .b = "fine."}};

    for (size_t i = 0; i < 3; ++i) {
        const char* ptr = (const char*) &test[i];
        ptr += offsetof(A, b);

        /* Extract the const char* from the address that ptr points at,
           and store it inside ptr itself: */
        memmove(&ptr, ptr, sizeof(const char*)); 
        printf("%s\n", ptr);
    }
}

给定

struct foo {int x, y;} s;
void write_int(int *p, int value) { *p = value; }

标准中的任何内容都不会区分:

write_int(&s.y, 12); //Just to get 6 characters

write_int((int*)(((char*)&s)+offsetof(struct foo,y)), 12);

标准可以这样理解,即暗示上述两​​者都违反了左值类型规则,因为它没有指定可以使用成员类型的左值访问结构的存储值, 要求想作为结构成员访问的代码写成:

void write_int(int *p, int value) { memcpy(p, value, sizeof value); }

我个人认为这很荒谬;如果 &s.y 不能用于访问 int 类型的左值,为什么 & 运算符会产生 int*

另一方面,我也认为标准所说的并不重要。不能依赖 clang 和 gcc 来正确处理使用指针执行任何操作的代码 "interesting", 即使在标准 明确定义的情况下,除非使用 -fno-strict-aliasing。在至少根据标准的某些似是而非的读物定义的情况下,如果编译器做出任何善意的努力来避免任何不正确的别名 "optimizations",那么在所有访问的情况下处理使用 offsetof 的代码将不会有任何问题这将在下一次通过其他方式访问对象之前使用指针(或从它派生的其他指针)完成。