【问题标题】:Casting structure pointers between structs containing pointers to different types?在包含指向不同类型的指针的结构之间转换结构指针?
【发布时间】:2015-04-27 23:17:56
【问题描述】:

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

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 尝试将realloc 内存用于向量中给定数量的项目。

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 的解释和引用来自这个答案:stackoverflow.com/a/1241314/885605
  • 我相信你可能会想到 applesoranges。对pointer size 的引用意味着在某些架构上指针的大小 可能不同(例如4-bit8-bit 等)。这与指针所指向的无关。我可能不完全理解你问题的症结所在,但这就是问题所在,你可能会侧身考虑。
  • @DavidC.Rankin 我确实在考虑实际指针的大小,而不是它指向的大小。我的问题是,如果 (TYPE*​​) 的大小不等于 (void*) 的大小,那么我的代码将会中断。
  • 那么我很抱歉,并且必须承认难以设想无法将 (TYPE*​​) 的指针转换为 (void*) 以提供更有效的评论的情况。从指针大小的角度来看,我看到的唯一问题是一些愚蠢的寻址方案,其中较小的指针大小会阻止在不同硬件上寻址目标(例如,一些手动内存管理器等)。抱歉打扰了。
  • @DavidC.Rankin 是的,直接转换指针会起作用,但我将指针转换为包含指针的结构,其中结构不兼容。但是您的评论非常有效,也许我应该直接转换成员而不是使用通用中间结构。

标签: c pointers struct casting size


【解决方案1】:

您的代码未定义。

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

标准对此进行了定义:

6.5 p7:

一个对象的存储值只能由具有以下之一的左值表达式访问 以下类型:

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

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

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

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

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

——一种字符类型。

struct vector 和 struct _vector_generic 的类型不兼容,不属于上述任何类别。在这种情况下,它们的内部表示是无关紧要的。

例如:

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

这同样适用于您将结构向量的地址传递给函数并将其解释为 _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 ;
...

由于您要存储类型的大小和元素的数量,因此您的 resize 函数的外观很明显(试试看)。

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

【讨论】:

  • 我的结构不兼容,我真的不应该在这里使用指针,你说得很好。
  • 我找到了去除石膏的方法。感谢您的意见!
【解决方案2】:

您问题中的代码会调用未定义的行为 (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,p7:指向对象类型可以转换为指向不同对象类型的指针。如果结果指针未正确对齐引用的类型,则行为未定义。否则,当再次转换回来时,结果应与原始指针比较。6.2.5,第 28 页:所有指向结构类型的指针应具有彼此相同的表示和对齐要求。 因此,只要不取消引用,就可以在两个结构之间进行转换。
  • @2501 "6.2.5, p 28: 所有指向结构类型的指针都应具有相同的表示和对齐要求" - 这与指针转换的结果无关。 “所以只要你不取消引用,你就可以在两个结构之间进行转换”——我的回答已经说过,只要你不取消引用,你就可以在两种指针类型之间进行转换。
  • 否则,当再次转换回来时,结果将与原始指针比较。因此您可以分配给不同结构类型的指针,然后返回,这使得定义的指针分配(但当然不是取消引用)。
  • @2501 对原始类型的赋值定义明确。中间指针值未指定。 (将此作为未指定行为而非未定义行为的示例)。然后取消引用未指定的指针是 UB。
  • @2501 很好,虽然我觉得 cmets 可能很有价值,我更希望您将 cmets 留在我的回答中而不是删除它们。
【解决方案3】:

为了讨论,让我们忽略 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.");
}

现在唯一可能出错的是,与您的特定系统上的“指向 y 的指针”相比,“指向 x 的指针”具有相同的大小但表示不同。我从未在现实世界的任何地方听说过这样的系统。但当然,不能保证:这种晦涩的、非正统的系统可能存在。在这种情况下,您可以自行决定是否要支持它们,或者是否只需对世界上 99.99% 的现有计算机具有可移植性就足够了。

在实践中,只有当您对超出 CPU 标准地址宽度的内存寻址时,才会在系统上使用多种指针格式,这通常由非标准扩展处理,例如 far 指针。在所有这些情况下,指针将具有不同的大小,您将使用上面的静态断言检测这些情况。

【讨论】:

  • 严格的别名对于现代编译器来说是非常真实的,但你会破坏它。静态断言对此无济于事。
  • 取决于你如何定义现实世界中的问题stackoverflow.com/questions/2958633/… google.com/search?hl=en&safe=off&q=strict+aliasing+bug davmac.wordpress.com/2009/10 在我看来,建议违反标准的代码是不负责任的。
  • @Lundin 这是错误的。是的,您可以知道所有指针都是具有相同宽度的“只是地址”,但是编译器也可以“知道”某些指针类型如果被取消引用则不允许别名。结果可能是灾难性的。请参阅davmac.wordpress.com/2009/10/25/… 了解真实示例(这是我的博客)。
  • @Lundin ... 这使得它完全不适合作为一般答案。程序员对他们使用的编译器有深入的了解并不常见。此外,您引用的内容是对答案的评论,而不是答案本身。您至少应该在答案中明确说明编译器必须定义的行为远远超出语言规范的实际要求。
  • @Lundin 此外,这些假设不仅仅与指针的大小和表示有关。你读过我链接的文章吗?毫不奇怪,许多编译器在调用未定义行为时实际上并没有定义行为。您在答案中的陈述-“在实践中只有两件事可能出错”-完全错误。编译器可能会对由于别名规则而假定无法别名的数据的读取和写入重新排序。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-12-13
  • 1970-01-01
  • 1970-01-01
  • 2019-03-18
  • 2016-05-05
  • 1970-01-01
相关资源
最近更新 更多