在不同类型的结构体指针之间转换?其中包含指向不同类型的指针。

4

我有一个结构,定义如下:

struct vector
{
  (TYPE) *items;
  size_t nitems;
};

这里的"type"是任何类型,而我有一个类似的不受类型限制的结构。

struct _vector_generic
{
  void *items;
  size_t nitems;
};

第二种结构用于将任何类型的第一种结构传递给调整大小函数,例如像这样:
struct vector v;
vector_resize((_vector_generic*)&v, sizeof(*(v->items)), v->nitems + 1);

这里的vector_resize尝试为向量中给定数量的项目重新分配内存。

int
vector_resize (struct _vector_generic *v, size_t item_size, size_t length)
{
  void *new = realloc(v->items, item_size * length);
  if (!new)
    return -1;

  v->items = new;
  v->nitems = length;

  return 0;
}

然而,C标准规定不同类型的指针不要求具有相同的大小。

6.2.5.27:

一个指向void的指针应该具有与指向字符类型的指针相同的表示和对齐要求。39)类似地,指向兼容类型的限定或非限定版本的指针应具有相同的表示和对齐要求。所有指向结构类型的指针应具有彼此相同的表示和对齐要求。所有指向联合类型的指针应具有彼此相同的表示和对齐要求。指向其他类型的指针不需要具有相同的表示或对齐要求。
现在我的问题是,我应该担心这段代码在某些架构上可能会出错吗?
我可以通过重新排列结构体使指针类型位于末尾来解决这个问题吗?例如:
struct vector
{
  size_t nitems;
  (TYPE) *items;
};

如果不行,我该怎么办?
参考我要实现的内容,请查看:
https://github.com/andy-graprof/grapes/blob/master/grapes/vector.h 例如用法,请查看:
https://github.com/andy-graprof/grapes/blob/master/tests/grapes.tests/vector.exp

@Joey 这里的解释和引用来自于这个答案:https://dev59.com/_XM_5IYBdhLWcg3ww2AQ#1241314 - Andreas Grapentin
我相信你可能在用“苹果”和“橙子”来思考。提到“指针大小”的参考意味着在某些架构上,指针的大小可能不同(例如,“4位”与“8位”等)。这与指针所指向的内容无关。我可能没有完全理解你问题的核心,但如果这是问题所在,你可能在侧面思考它。 - David C. Rankin
@DavidC.Rankin 我确实在考虑指针本身的大小,而不是它所指向的内容。我的问题是,如果 (TYPE*) 的大小不等于 (void*) 的大小,那么我的代码将会出错。 - Andreas Grapentin
1
那么我道歉,必须承认很难想象出一种情况,即(TYPE*)类型的指针无法转换为(void*)类型以提供更有生产力的评论。从指针大小的角度来看,我唯一注意到的问题是某些愚蠢的寻址方案,其中较小的指针大小会阻止在不同硬件上寻址目标(例如,某些手动编写的内存管理器等)。对于造成干扰,我表示抱歉。 - David C. Rankin
@DavidC.Rankin 是的,直接转换指针也可以,但我选择将指针转换为包含指针的结构体指针,其中这些结构体是不兼容的。但是您的评论非常有价值,也许我应该直接转换成员,而不是使用通用中间结构体。 - Andreas Grapentin
3个回答

2

你的代码未定义。

使用不兼容类型的lvalue访问对象会导致未定义行为。

标准在以下地方定义:

6.5 p7:

一个对象只能通过具有以下类型之一的lvalue表达式来访问其存储值:

— 与对象有效类型兼容的类型,

— 与对象有效类型兼容的带限定符版本的类型,

— 与对象有效类型对应的有符号或无符号类型,

— 与对象有效类型对应的带限定符版本的有符号或无符号类型,

— 包括上述类型之一的聚合体或联合体类型(包括递归地子聚合体或包含联合体中的成员),或

— 字符类型。

struct vector和struct _vector_generic具有不兼容类型,并且不适合上述任何一类。它们的内部表示在这种情况下是不相关的。

例如:

struct vector v;
_vector_generic* g = &v;
g->size = 123 ;   //undefined!

同样的情况也适用于你的例子,你将结构体vector的地址传递给函数,并将其解释为_vector_generic指针。
结构体的大小和填充也可能不同,导致元素在不同的偏移位置上。
你可以使用通用结构体,在主代码中根据void指针所持有的类型进行转换。
struct gen
{
    void *items;
    size_t nitems;
    size_t nsize ;
};

struct gen* g = malloc( sizeof(*g) ) ;
g->nitems = 10 ;
g->nsize = sizeof( float ) ;
g->items = malloc( g->nsize * g->nitems ) ;
float* f = g->items ;
f[g->nitems-1] = 1.2345f ;
...

使用相同的结构定义,您可以为不同类型分配内存:
struct gen* g = malloc( sizeof(*g) ) ;
g->nitems = 10 ;
g->nsize = sizeof( int ) ;
g->items = malloc( g->nsize * g->nitems ) ;
int* i = g->items ;
...

由于您正在存储类型的大小和元素数量,因此很明显您的调整大小函数将是什么样子(请尝试一下)。

您必须小心记住哪个变量使用了哪种类型,因为编译器不会警告您,因为您正在使用void*。


1
我的结构体不兼容,我真的不应该在指针周围转换类型,你说得很对。 - Andreas Grapentin
我找到了一种去除强制转换的方法。感谢您的建议! - Andreas Grapentin

1
您的问题中的代码调用了未定义行为(UB),因为您解引用了一个可能无效的指针。强制转换如下:
(_vector_generic*)&v

被第6.3.2.3段第7款所覆盖:

指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针对于引用类型没有正确对齐,则其行为是未定义的。否则,当再次转换时,结果应与原始指针相等。

如果我们假设满足对齐要求,则强制转换不会调用UB。但是,并没有要求转换后的指针必须“相等”(即指向与原指针相同的对象),甚至没有要求它指向任何对象——也就是说,指针的值是未指定的。因此,在解引用此指针之前(未先确定它是否等于原始指针)将会引发未定义的行为。

(许多精通C的人发现这很奇怪。我认为这是因为他们知道指针强制转换通常编译成无操作——指针值仅保持不变——因此他们将指针转换视为纯粹的类型转换。然而,标准并不强制执行这种方式)。

即使转换后的指针与原始指针相等,6.5第7段(所谓的“严格别名规则”)也不允许您对其进行解引用。本质上,您不能通过两个具有不同类型的指针访问同一对象,除了一些有限的例外情况。
示例:
struct a { int n; };
struct b { int member; };

struct a a_object;
struct b * bp = (struct b *) &a_object; // bp takes an unspecified value

// Following would invoke UB, because bp may be an invalid pointer:
// int m = b->member;

// But what if we can ascertain that bp points at the original object?:
if (bp == &a_object) {
    // The comparison in the line above actually violates constraints
    // in 6.5.9p2, but it is accepted by many compilers.
    int m = b->member;   // UB if executed, due to 6.5p7.
}

然而,没有任何要求(被省略)指向任何对象。这是不正确的,因为:6.3.2.3,第7段:对象类型的指针可以转换为指向不同对象类型的指针。如果所得到的指针未对齐于所引用的类型,则行为是未定义的。否则,当再次转换时,结果应与原始指针相等。6.2.5,第28页:所有结构类型的指针都应具有相同的表示和对齐要求。因此,只要不进行解引用,就可以在两个结构之间进行转换。 - 2501
@2501 "6.2.5, p 28: 所有指向结构类型的指针应具有相同的表示和对齐要求" - 这与指针转换的结果无关。 "因此,只要不解除引用,您就可以在两个结构之间进行转换" - 我的答案已经说过,只要不解除引用,您就可以在两个指针类型之间进行转换。 - davmac
否则,当再次转换时,结果应与原指针相等。因此,您可以分配到不同结构类型的指针,并返回,这使得指针分配被定义(但当然不是解引用)。 - 2501
@2501 将赋值返回到原始类型是明确定义的。中间指针值是未指定的。(这是“未指定行为”的示例,但不是“未定义行为”)。然后对未指定的指针进行解引用是未定义行为。 - davmac
@2501做得很好,虽然我认为注释是有价值的,如果您留下对我的回答的评论而不是删除它们,我会更喜欢。 - davmac

0

为了讨论的方便,我们忽略C标准正式声明这是未定义行为。因为未定义行为仅意味着某些事情超出了语言标准的范围:任何事情都可能发生,而C标准不提供任何保证。然而,在您使用的特定系统上,制造系统的人可能会有“外部”保证。

在现实世界中,存在硬件,确实存在这样的保证。在实践中,这里只有两件事可能出错:

  • TYPE*void*具有不同的表示或大小。
  • 由于对齐要求,每个结构类型中的不同结构填充。

这两者似乎都不太可能,并且可以通过静态断言来避免:

static void ct_assert (void) // dummy function never linked or called by anyone
{
  struct vector v1;
  struct _vector_generic v2;

  static_assert(sizeof(v1.items) == sizeof(v2.items), 
                "Err: unexpected pointer format.");
  static_assert(sizeof(v1) == sizeof(v2), 
                "Err: unexpected padding.");
}

现在唯一可能出错的事情就是,如果“指向x的指针”在您特定的系统上与“指向y的指针”具有相同的大小但不同的表示方式。我从未听说过在现实世界中存在这样的系统。但是当然,不能保证:这种晦涩难懂、非正统的系统可能存在。在这种情况下,由您决定是否要支持它们,或者仅需要支持99.99%的全球现有计算机的可移植性。

实际上,在系统上拥有多个指针格式的唯一时间是当您正在寻址超出CPU标准地址宽度的内存时,通常通过非标准扩展(例如far指针)处理。在所有这些情况下,指针将具有不同的大小,并且您将使用上面的静态断言检测到这些情况。


1
现代编译器中的严格别名规则非常严格,如果你违反了它,就会出问题。静态断言对此没有帮助。 - 2501
1
这取决于您如何定义“现实世界中的问题”:https://dev59.com/-HA85IYBdhLWcg3wD_O8 https://www.google.com/search?hl=en&safe=off&q=strict+aliasing+bug http://davmac.wordpress.com/2009/10/ 在我看来,建议违反标准的代码是不负责任的。 - 2501
1
@Lundin 这是错误的。是的,您可以知道所有指针都是具有相同宽度的“地址”,但编译器也可以“知道”如果解引用某些指针类型不允许别名。结果可能是灾难性的。请参见http://davmac.wordpress.com/2009/10/25/mysql-and-c99-aliasing-rules-a-detective-story/以获取一个真实世界的例子(这是我的博客)。 - davmac
1
@Lundin...这使得它完全不适合作为一般答案。程序员通常不会深入了解他们使用的编译器。此外,您引用的内容在答案的评论中,而不是答案本身中。您至少应该在答案中明确表明编译器必须具有定义良好的行为,超出语言规范所要求的范围。 - davmac
1
@Lundin 此外,这些假设不仅适用于指针的大小和表示。您看过我链接的文章吗?许多编译器在调用未定义行为时实际上没有定义行为,这并不奇怪。您在回答中的声明 - “在实践中只有两件事可能出错” - 完全是错误的。编译器可能会重新排序读取和写入数据,因为它假定由于别名规则而不能别名。 - davmac
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接