调用 free() 包装器:取消引用 type-punned 指针将破坏 strict-aliasing 规则

Calling a free() wrapper: dereferencing type-punned pointer will break strict-aliasing rules

我已经尝试在 SO 上阅读具有类似标题的其他问题,但它们都有点太复杂了,我无法将解决方案(甚至解释)应用于我自己的问题,这似乎是一个更简单的性质。

在我的例子中,我有一个围绕 free() 的包装器,它在释放它后将指针设置为 NULL

void myfree(void **ptr)
{
    free(*ptr);
    *ptr = NULL;
}

在我正在做的项目中,它是这样调用的:

myfree((void **)&a);

如果我将优化级别提高到 -O3 并添加 -Wall(否则不会),这会使 gcc(OpenBSD 上的 4.2.1)发出警告 "dereferencing type-punned pointer will break strict-aliasing rules" ).

以下列方式调用 myfree() 不会使编译器发出该警告:

myfree((void *)&a);

所以我想知道我们是否应该改变我们调用 myfree() 的方式。

我相信我用第一种调用方式调用了未定义的行为 myfree(),但我一直无法理解为什么。此外,在我可以访问的所有编译器上(clanggcc),在所有系统上(OpenBSD、Mac OS X 和 Linux),这个是唯一真正给我警告的编译器和系统(我知道发出警告是一个不错的选择)。

在调用 myfree() 之前、内部和之后打印指针的值,两种调用方式都会给我相同的结果(但如果它是未定义的行为,那可能没有任何意义):

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

void myfree(void **ptr)
{
    printf("(in myfree) ptr = %p\n", *ptr);
    free(*ptr);
    *ptr = NULL;
}

int main(void)
{
    int *a, *b;

    a = malloc(100 * sizeof *a);
    b = malloc(100 * sizeof *b);

    printf("(before myfree) a = %p\n", (void *)a);
    printf("(before myfree) b = %p\n", (void *)b);

    myfree((void **)&a);  /* line 21 */
    myfree((void *)&b);

    printf("(after myfree) a = %p\n", (void *)a);
    printf("(after myfree) b = %p\n", (void *)b);

    return EXIT_SUCCESS;
}

编译并运行它:

$ cc -O3 -Wall free-test.c
free-test.c: In function 'main':
free-test.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

$ ./a.out
(before myfree) a = 0x15f8fcf1d600
(before myfree) b = 0x15f876b27200
(in myfree) ptr = 0x15f8fcf1d600
(in myfree) ptr = 0x15f876b27200
(after myfree) a = 0x0
(after myfree) b = 0x0

我想了解第一次调用 myfree() 有什么问题,我想知道第二次调用是否正确。谢谢。

由于 aint* 而不是 void*,因此 &a 无法转换为指向 void* 的指针。 (假设 void* 比指向整数的指针更宽,这是 C 标准所允许的。)因此,您的备选方案 - myfree((void**)a)myfree((void*)a) - 都不正确。 (转换为 void* 不是严格的别名问题。但它仍然会导致未定义的行为。)

更好的解决方案(恕我直言)是强制用户插入一个可见的赋值:

void* myfree(void* p) {
    free(p);
    return 0;
}

a = myfree(a);

用clang和gcc,可以用一个属性来表示必须使用my_free的return值,这样如果忘记赋值,编译器会警告你。或者您可以使用宏:

#define myfree(a) (a = myfree(a))

这里有一个建议:

  1. 不违反严格的别名规则。
  2. 让通话更自然。
void* myfree(void *ptr)
{
    free(ptr);
    return NULL;
}

#define MYFREE(ptr) ptr = myfree(ptr);

您可以简单地使用宏:

int* a = malloc(sizeof(int)*10);

...

MYFREE(a);

基本上有几种方法可以使函数与指针一起工作并mod以与指针的目标类型无关的方式确定指针:

  1. 将指针作为 void* 传递给函数,return 将其作为 void*,在调用站点的两个方向上应用适当的转换。这种方法的缺点是占用了函数的 return 值,无法将其用于其他目的,并且还排除了在锁内执行指针更新的可能性。

  2. 将指针传递给接受两个 void* 的函数,将其中一个转换为适当类型的指针,将另一个转换为该类型的双重间接指针,可能还有第二个函数可以将传入的指针读取为 void*,并使用这些函数读取和写入有问题的指针。这应该是 100% 可移植的,但可能非常低效。

  3. 在其他地方使用void*类型的指针变量和字段,并在实际使用时将它们转换为真正的指针类型,从而允许将void**类型的指针用于mod定义指针变量。

  4. 使用 memcpy 读取或 mod 化未知类型的指针,给出识别它们的双间接指针。

  5. 文档是用 1990 年代流行的 C 方言编写的代码,它将 "void**" 视为指向任何类型的双重间接指针,并使用编译器 and/or 支持该方言的设置。 C 标准允许实现对指向不同类型的事物的指针使用不同的表示,并且因为这些实现不能支持通用的双间接指针类型,并且因为可以轻松允许使用 void** 的实现在编写标准之前已经这样做了,因此没有必要让标准描述该行为。

拥有通用双间接指针类型的能力过去是并且对 90% 以上可以(并且确实)很容易支持它的实现非常有用,标准的作者当然知道这一点,但是作者对描述明智的编译器编写者无论如何都会支持的行为的兴趣远不如对强制行为的兴趣,这些行为总体上是有益的即使在无法廉价支持它们的平台上(例如强制即使在其无符号数学指令换行 mod 65535 的平台上,编译器也必须生成使计算换行 mod 65536 所需的任何代码)。我不确定为什么 modern 编译器作者无法识别这一点。

也许如果程序员开始公开地为 C 的正常方言编写代码,标准的维护者可能会认识到这些方言的价值。 [请注意,从别名的角度来看,将 void** 视为通用双间接指针的性能成本远低于强迫程序员使用上述 2-4 中的任何替代方案;因此,编译器作者声称将 void** 视为通用双间接指针会降低性能的任何说法都应持怀疑态度。