4

在zwol的回答中,通过在一个结构之间转换指针来实现继承是否合法,该结构是另一个结构的子集而不是第一个成员?他举了一个例子,说明为什么相似结构之间的简单类型转换不安全,并且在评论中有一个示例环境,它的行为异常:在 -O2 上使用 gcc 编译以下内容会导致它打印“x=1.000000 some= 2.000000"

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

struct base
{
    double some;
    char space_for_subclasses[];
};
struct derived
{
    double some;
    int value;
};

double test(struct base *a, struct derived *b)
{
    a->some = 1.0;
    b->some = 2.0;
    return a->some;
}

int main(void)
{
    size_t bufsz = sizeof(struct base);
    if (bufsz < sizeof(struct derived)) bufsz = sizeof(struct derived);
    void *block = malloc(bufsz);

    double x = test(block, block);
    printf("x=%f some=%f\n", x, *(double *)block);
    return 0;
}

我在乱搞代码以更好地了解它的行为方式,因为我需要做类似的事情,并注意到标记avolatile足以防止它打印不同的值。这符合我对出了什么问题的预期——gcc 假设它a->some不受写入的影响b->some。但是,我原以为 gcc 只能假设ab标记为restrict

我是否误解了这里发生的事情和/或限制限定符的含义?如果不是,gcc 是否可以自由地做出这个假设,因为 a并且b属于不同类型?最后,是否同时标记和a标记使此代码符合标准,或者至少防止未定义的行为允许 gcc 做出上述假设?bvolatile

4

2 回答 2

3

如果一个存储区域仅使用volatile-qualified lvalues 访问,编译器将不得不竭尽全力不将每次写入处理为将写入的值转换为位模式并存储它,并且每次读取都作为读取内存中的位模式并将其转换为一个值。该标准实际上并没有强制要求这种行为,并且理论上给出了一个编译器:

long long volatile foo;
...
int test(void)
{
  return *((short volatile*)(&foo));
}

可以假设任何可以调用的代码分支test永远不会被执行,但我还不知道有任何编译器以这种极端的方式运行。

另一方面,给定如下函数:

void zero_aligned_pair_of_shorts(uint16_t *p)
{
  *((uint32_t void volatile*)&p) = 0;
}

编译器喜欢gcc并且clang不会可靠地认识到它可能会对使用非限定左值类型访问的对象的存储值产生一些影响uint16_t。像 icc 这样的一些编译器将volatile访问视为同步任何地址已被占用的寄存器缓存对象的指标,因为这样做是编译器维护标准章程和基本原理文档中描述的 C 精神原则的一种廉价而简单的方法“不要阻止程序员做需要做的事情”而不需要特殊的语法。然而,像 gcc 和 clang 这样的其他编译器要求程序员要么使用 gcc/clang 特定的内在函数,要么使用命令行选项来全局阻止大多数形式的寄存器缓存。

于 2018-12-10T17:59:18.393 回答
0

这个特定问题和zwol 的答案的问题在于它们将类型双关语和严格别名混为一谈。Zwol 的答案对于该特定用例是正确的,因为用于初始化结构的类型;但不是在一般情况下,也不是。struct sockaddrPOSIX 类型可能会读到暗示的答案。

对于具有公共初始成员的结构类型之间的类型双关语,您需要做的就是声明(而不是使用!)这些结构的联合,并且您可以通过任何结构类型的指针安全地访问公共成员。这是自 ANSI C 3.3.2.3 以来明确允许的行为,包括C11 6.5.2.3p6(链接到 n1570 草案)。

如果一个实现包含struct sockaddr_对用户空间应用程序可见的所有结构的联合,那么在我看来, zwol 的答案OP 链接到是具有误导性的,如果有人读它暗示struct sockaddr结构支持需要编译器提供非标准的东西。(如果您定义_GNU_SOURCE,glibc 将此类联合定义为struct __SOCKADDR_ARG包含所有此类类型的匿名联合。但是,glibc 设计为使用 GCC 编译,因此可能存在其他问题。)

严格的别名要求函数的参数不引用相同的存储(内存)。例如,如果您有

int   i = 0;
char *iptr = (char *)(&i);

int modify(int *iptr, char *cptr)
{
    *cptr = 1;
    return *iptr;
}

那么调用modify(&i, iptr)是一个严格的混叠违规。定义中的类型双关语iptr是偶然的,实际上是允许的(因为您可以使用该char类型来检查任何类型的存储表示;C11 6.2.6.1p4)。

这是类型双关语的正确示例,避免了严格的别名问题:

struct item {
    struct item *next;
    int          type;
};

struct item_int {
    struct item *next;
    int          type; /* == ITEMTYPE_INT */
    int          value;
};

struct item_double {
    struct item *next;
    int          type; /* == ITEMTYPE_DOUBLE */
    double       value;
};

struct item_string {
    struct item *next;
    int          type;    /* == ITEMTYPE_STRING */
    size_t       length;  /* Excluding the '\0' */
    char         value[]; /* Always has a terminating '\0' */
};

enum {
    ITEMTYPE_UNKNOWN = 0,
    ITEMTYPE_INT,
    ITEMTYPE_DOUBLE,
    ITEMTYPE_STRING,
};

现在,如果在同一范围内可以看到以下联合,我们可以在指向上述结构类型的指针之间进行类型双关,并完全安全地访问nextandtype成员:

union item_types {
    struct item         any;
    struct item_int     i;
    struct item_double  d;
    struct item_string  s;
};

对于其他(非常见)成员,我们必须使用用于初始化结构的相同结构类型。这就是该type领域存在的原因。

作为这种完全安全用法的示例,请考虑以下打印项目列表中的值的函数:

void print_items(const struct item *list, FILE *out)
{
    const char *separator = NULL;

    fputs("{", out);        

    while (list) {

        if (separator)
            fputs(separator, out);
        else
            separator = ",";

        if (list->type == ITEMTYPE_INT)
            fprintf(out, " %d", ((const struct item_int *)list)->value);
        else
        if (list->type == ITEMTYPE_DOUBLE)
            fprintf(out, " %f", ((const struct item_double *)list)->value);
        else
        if (list->type == ITEMTYPE_STRING)
            fprintf(out, " \"%s\"", ((const struct item_string *)list)->value);
        else
            fprintf(out, " (invalid)");

        list = list->next;
    }

    fputs(" }\n", out);
}

请注意,我value为 value 字段使用了相同的名称,只是因为我没有想到更好的名称;它们不需要相同。

类型双关语出现在fprintf()语句中,并且当且仅当 1) 结构是使用与type字段匹配的结构初始化的,并且 2)union item_types在当前范围内可见时才有效。

我尝试过的当前 C 编译器中没有一个与上述代码有任何问题,即使在破坏标准行为某些方面的极端优化级别也是如此。(我没有检查过 MSVC,但它确实是一个 C++ 编译器,它也可以编译大多数 C 代码。但是,如果上面的代码有任何问题,我会感到惊讶。)

于 2018-12-10T04:54:31.803 回答