你能将一个双精度数组重新解释为包含双精度的结构体吗?

12
可以将一个双重数组转换为由双精度数构成的结构体吗?
struct A
{
   double x;
   double y;
   double z;
};

int main (int argc , char ** argv)
{
   double arr[3] = {1.0,2.0,3.0};
   A* a = static_cast<A*>(static_cast<void*>(arr));
   std::cout << a->x << " " << a->y << " " << a->z << "\n";
}

这将打印出"1 2 3"。但是它能保证在任何编译器上都能正常工作吗?
编辑:根据9.2.21:指向标准布局结构体对象的指针,经过适当的reinterpret_cast转换,指向其初始成员(...),反之亦然。
如果我用下面的代码替换我的代码:
struct A
{
  double & x() { return data[0]; }
  double & y() { return data[1]; }
  double & z() { return data[2]; }
private:
   double data[3];
};

int main (int, char **)
{
   double arr[3] = {1.0,2.0,3.0};
   A* a = reinterpret_cast<A*>(arr);
   std::cout << a->x() << " " << a->y() << " " << a->z() << "\n";
}

那么它就能保证正常工作了,对吗?我知道很多人可能不觉得这样的代码美观,但是使用结构体来处理数据有其优势,不需要复制输入数组的数据。我可以在结构体中定义成员函数来计算标量和向量乘积、距离等,这样会使我的代码比使用数组更易于理解。
那么呢?
int main (int, char **)
{
   double arr[6] = {1.0,2.0,3.0,4.0,5.0,6.0};
   A* a = reinterpret_cast<A*>(arr);
   std::cout << a[0].x() << " " << a[0].y() << " " << a[0].z() << "\n";
   std::cout << a[1].x() << " " << a[1].y() << " " << a[1].z() << "\n";
}

这个也保证能工作吗?或者编译器会在数据成员之后添加一些内容,使得 `sizeof(A) > 3*sizeof(double)`?有没有一种可移植的方法来防止编译器这样做?

任何人都可以编写编译器,所以这是一个棘手的问题。为什么不指定您要使用的编译器呢?我猜测所有主要的编译器都处理得一样,它确实有效。 - Reut Sharabani
1
由于数据结构对齐的原因,很可能不行(在您想要使用不仅限于双精度的情况下)。 - Alexandru Barbarosie
1
你违反了严格别名规则。 - Jarod42
你可以假设它会起作用,然后添加一个 static_assert 测试来验证它确实起作用,这样至少如果它不起作用,你会知道。 - undefined
7个回答

11

不,这并不是保证的。

唯一防止任何编译器在xy之间或yz之间插入填充的是常识。没有任何语言标准的规定会禁止这种情况。

即使没有填充,即使A的表示与double[3]完全相同,它仍然无效。该语言不允许您将一个类型假装成另一个类型。您甚至不能将struct A { int i; };的实例视为struct B { int i; }


在这种情况下,A标准布局。理论上可能会有填充(虽然使用double不太可能,但使用long double则很可能),但无论如何,结构体和数组将具有相同的填充。这一切都假设没有#pragma来覆盖对齐规则。此外,AB 与彼此具有相同的布局兼容性(尽管与数组不兼容)。 - finnw
1
@finnw "但无论如何,结构体和数组将具有相同的填充": 标准中没有要求这样做。数组元素之间不能有填充,但类成员之间可以有填充。类是标准布局只意味着在第一个成员之前不会有任何填充,仅此而已。 - undefined

5
该标准对对象的内存布局提供了很少的保证。
对于类/结构体:
9.2./15:具有相同访问控制权限的类的非静态数据成员分配,以使后面的成员在类对象中具有更高的地址。未指定具有不同访问控制的非静态数据成员的分配顺序。实现对齐要求可能导致两个相邻成员不会立即分配在一起;对于管理虚函数和虚基类的空间的要求也可能是如此。
对于数组,元素是连续的。没有关于对齐的说明,因此它可能使用与结构体相同的对齐规则,也可能不使用:
8.3.4:数组类型的对象包含一个类型为T的N个子对象的连续分配的非空集合。
在你的特定例子中,你唯一能确定的是,如果使用reinterpret_cast,则a.x对应于arr [0]:
9.2.21:适当转换使用reinterpret_cast将指向标准布局结构对象的指针指向其初始成员(...)反之亦然。

自从这个答案被写出来以后,缺陷报告明确了在这种情况下表达式 a.x 的行为是未定义的。然而,标准布局仍然保证了 x 成员地址的偏移量为零。 - undefined

1
不,它并不保证一定能够正常工作,即使我所知道的所有编译器在常见架构上都可以使用,因为C语言规范中写道:6.2.6类型表示,6.2.6.1通用1:除了在本子句中声明的情况外,所有类型的表示均未指定。关于结构体中的默认填充,它没有说明任何内容。
当然,常见的架构最多使用64位,这是这些架构上double类型的大小,因此不应该有填充,您的转换应该可以工作。
但请注意:您正在明确调用未定义行为,并且下一代编译器在编译此类强制转换时可能会执行任何操作。

0

将数组作为结构体访问是未定义行为

首先,不,将数组作为结构体别名是未定义行为。 这只是一种严格别名违规,如[basic.lval] p11所述:

如果程序试图通过一个类型与以下类型之一不相似的glvalue访问对象的存储值,则行为是未定义的:

  • 对象的动态类型
  • 与对象的动态类型相对应的有符号或无符号类型
  • charunsigned charstd::byte类型

这些特殊情况都不适用,所以这只是未定义行为。 您有一个类型为"double数组"的对象,任意的struct无法别名这样的类型。

使用double & x() { return data[0]; }的解决方案是正确的。
指针互相转换并不能规避严格别名问题。
有些人可能认为可以通过使用类似以下方式来规避这些问题:
struct A { double arr[3]; };
// ...
double arr[3];
auto p = reinterpret_cast<A*>(&arr); // could p be used?

然而,使用p是无效的。 虽然类型A的对象与其第一个非静态数据成员是pointer-interconvertible的,但在这种情况下不存在类型A的对象。
这个规则只允许在AA::arr之间进行转换;它并不能规避严格别名规则。
如果我们暂时忽略严格别名规则会怎么样呢?
尽管将数组别名为struct显然是未定义行为,但编译器在保留reinterpret_cast并使其正常工作时相当宽容,即使这是未定义行为。
即使你做出这样的假设,你仍然会面临一个问题,那就是struct可能具有与数组不同的对齐和布局要求。如果你添加了断言,你可能会安全一些:
static_assert(sizeof(A) >= sizeof(double[3]));
static_assert(alignof(A) <= alignof(double[3]));

请注意,他的担忧大部分是理论上的;实际上,任何理智的编译器都会以相同的方式布局三个double和一个double[3]。
有没有比reinterpret_cast更安全的替代方法?
与其使用reinterpret_cast,你可以通过多种方式进行类型转换。
double arr[3] = { /* ... */ };

A a = std::bit_cast<A>(arr); // C++20 bit-casting, arguably the best way here

// Implicitly start the lifetime of an A in the same place as arr, keeping the
// value of the doubles.
// Note that arr is not usable as an array afterwards.
A* b = std::start_lifetime_as<A>(arr);

A c;
std::memcpy(&c, arr, sizeof(arr)); // old-school, type-pun through std::memcpy

请注意,所有的方法都假设A的布局与double[3]相同,并且double[3]的对齐方式至少与A一样严格。
另请参阅:在C++中进行类型转换的现代正确方法是什么? 关于反过来的情况,即将结构体视为数组,是不合法的。将包含doublestruct重新解释为double[3]是不合法的。如果struct包含double[3]成员,将A*重新解释为double*也是不合法的,因为数组与其元素类型不可互相转换。
然而,以下操作是合法的:
struct A { double arr[3]; };
A a;
double* d = a.arr;

0

MSVC实现std::complex使用数组解决方案,而LLVM libc++使用前一种形式。

我认为,只需检查您的libc++的std::complex实现,并与其使用相同的解决方案即可。


std::complex在标准中有一个特殊的例外,允许这样做。即使其他类型以相同的方式实现,也明确不允许这样做。 - undefined

-2
据我所知,答案是:是的。
唯一可能会让你困惑的是,如果在结构体中使用了一个非常不寻常的对齐设置的#pragma指令。例如,如果在您的计算机上double占用8个字节,并且#pragma指令告诉将每个成员对齐到16字节边界,那么可能会导致问题。除此之外,一切都很好。

-4

我不同意这里的共识。一个包含三个双精度浮点数的结构体与一个包含三个双精度浮点数的数组完全相同,除非你特别以不同方式对结构体进行打包,并且使用了一个奇数字节大小的双精度浮点数的奇怪处理器。

虽然这不是语言内置的功能,但我觉得这样做是安全的。从风格上讲,我不会这样做,因为这样只会造成混淆。


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