我应该担心指针投射过程中的对齐吗?

时间:2012-12-14 15:21:49

标签: c++ c casting alignment

在我的项目中,我们有一段代码:

// raw data consists of 4 ints
unsigned char data[16];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + 4));
i3 = *((int*)(data + 8));
i4 = *((int*)(data + 12));

我和我的技术负责人讨论过这段代码可能无法移植,因为它试图将unsigned char*转换为int*,这通常有更严格的对齐要求。但是技术主管说没关系,大多数编译器在转换后仍保持相同的指针值,我可以像这样编写代码。

坦率地说,我并不是真的相信。经过研究,我发现有些人反对使用上面的指针式演员,例如herehere

所以这是我的问题:

  1. 在实际项目中投射后取消引用指针真的很安全吗?
  2. C风格的演员和reinterpret_cast之间有什么区别吗?
  3. C和C ++之间有什么区别吗?

7 个答案:

答案 0 :(得分:35)

  

1。在实际项目中进行转换后取消引用指针真的很安全吗?

如果指针碰巧没有正确对齐,它确实会导致问题。我亲眼看到并修复了将char*转换为更严格对齐类型所导致的实际生产代码中的总线错误。即使您没有得到明显的错误,也可能会出现性能下降等不太明显的问题。即使您没有立即发现任何问题,严格遵守标准以避免UB是一个好主意。 (并且代码破坏的一条规则是严格的别名规则,§3.10/ 10 *)

如果缓冲区重叠(或者更好bit_cast<>()),更好的选择是使用std::memcpy()std::memmove

unsigned char data[16];
int i1, i2, i3, i4;
std::memcpy(&i1, data     , sizeof(int));
std::memcpy(&i2, data +  4, sizeof(int));
std::memcpy(&i3, data +  8, sizeof(int));
std::memcpy(&i4, data + 12, sizeof(int));

有些编译器比其他编译器更加努力,以确保char数组的排列比必要的更严格,因为程序员经常会这样做错误。

#include <cstdint>
#include <typeinfo>
#include <iostream>

template<typename T> void check_aligned(void *p) {
    std::cout << p << " is " <<
      (0==(reinterpret_cast<std::intptr_t>(p) % alignof(T))?"":"NOT ") <<
      "aligned for the type " << typeid(T).name() << '\n';
}

void foo1() {
    char a;
    char b[sizeof (int)];
    check_aligned<int>(b); // unaligned in clang
}

struct S {
    char a;
    char b[sizeof(int)];
};

void foo2() {
    S s;
    check_aligned<int>(s.b); // unaligned in clang and msvc
}

S s;

void foo3() {
    check_aligned<int>(s.b); // unaligned in clang, msvc, and gcc
}

int main() {
    foo1();
    foo2();
    foo3();
}

http://ideone.com/FFWCjf

  

2。 C风格的转换和reinterpret_cast之间有什么区别吗?

这取决于。 C风格的演员阵容根据所涉及的类型做不同的事情。指针类型之间的C风格转换将产生与reinterpret_cast相同的东西;参见§5.4显式类型转换(演员表示法)和§5.2.9-11。

  

3。 C和C ++之间有什么区别吗?

只要你处理C中合法的类型,就不应该这样做。


*另一个问题是C ++没有指定从一种指针类型转换为具有更严格对齐要求的类型的结果。这是为了支持甚至无法表示未对齐指针的平台。然而,今天典型的平台可以表示未对齐的指针,编译器将这种演员的结果指定为您期望的结果。因此,此问题是别名违规的次要问题。见[expr.reinterpret.cast] / 7.

答案 1 :(得分:27)

真的不行。对齐可能是错误的,并且代码可能违反严格的别名。你应该明确地解压缩它。

i1 = data[0] | data[1] << 8 | data[2] << 16 | data[3] << 24;

等。这绝对是明确定义的行为,作为奖励,它也与字节顺序无关,与指针演员不同。

答案 2 :(得分:6)

在示例中,如果初始char指针正确对齐,那么您在此处显示的内容几乎对所有现代CPU都是安全的。一般来说,这是不安全的,不能保证有效。

如果初始char指针未正确对齐,这将在x86和x86_64上运行,但在其他体系结构上可能会失败。如果您很幸运,它只会让您崩溃并且您将修复您的代码。如果您运气不好,未对齐的访问权限将由操作系统中的陷阱处理程序修复,您将会遇到可怕的性能而没有任何关于它为何如此缓慢的明显反馈(我们正在谈论)对某些代码来说,这是一个很慢的问题,这对于20年前的alpha来说是一个巨大的问题。)

即使在x86&amp; co,未对齐访问将会更慢。

如果您想在今天和将来保持安全,只需memcpy而不是像这样做任务。现代编译器可能会对memcpy进行优化并做正确的事情,如果没有,memcpy本身将具有对齐检测并且将做最快的事情。

另外,你的例子在一点上是错误的:sizeof(int)总是不是4。

答案 3 :(得分:4)

解压缩char缓冲数据的正确方法是使用memcpy

unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
memcpy(&i1, data, sizeof(int));
memcpy(&i2, data + sizeof(int), sizeof(int));
memcpy(&i3, data + 2 * sizeof(int), sizeof(int));
memcpy(&i4, data + 3 * sizeof(int), sizeof(int));

Casting违反了别名,这意味着编译器和优化器可以自由地将源对象视为未初始化。

关于你的3个问题:

  1. 不,由于别名和对齐,取消引用转换指针通常是不安全的。
  2. 不,在C ++中,C风格的转换是根据reinterpret_cast
  3. 定义的
  4. 不,C和C ++就基于强制转换的别名达成一致。基于联合的别名处理有所不同(在某些情况下C允许; C ++不支持)。

答案 4 :(得分:1)

更新: 我忽略了这样一个事实:确实较小的类型可能相对于较大的类型未对齐,就像在你的例子中一样。您可以通过反转构建数组的方式来缓解该问题:将数组声明为int数组,并在需要以此方式访问时将其强制转换为char *

// raw data consists of 4 ints
int data[4];

// here's the char * to the original data
char *cdata = (char *)data;
// now we can recast it safely to int *
i1 = *((int*)cdata);
i2 = *((int*)(cdata + sizeof(int)));
i3 = *((int*)(cdata + sizeof(int) * 2));
i4 = *((int*)(cdata + sizeof(int) * 3));

基元类型数组不会有任何问题。处理structured data(C中为struct)的数组时会出现对齐问题,如果数组的原始初始类型大于它被转换为的数组,请参阅上面的更新。

将一个char数组转换为int数组应该是完全可以的,前提是用sizeof(int)替换4的偏移量,以匹配代码应在其上运行的平台上的int的大小

// raw data consists of 4 ints
unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + sizeof(int)));
i3 = *((int*)(data + sizeof(int) * 2));
i4 = *((int*)(data + sizeof(int) * 3));

请注意,只有当您以某种方式从一个平台以不同的字节顺序共享该数据时,才会遇到endianness个问题。否则,应该完全没问题。

答案 5 :(得分:1)

您可能希望向他展示根据编译器版本的不同之处:

除了对齐之外,还有第二个问题:标准允许您将int*转换为char*,而不是相反(除非char*最初来自int* {1}})。 See this post for more details.

答案 6 :(得分:0)

是否必须担心对齐取决于指针所源自的对象的对齐方式。

如果转换为具有更严格对齐要求的类型,则它不可移植。

char数组的基数(在您的示例中)不需要比元素类型char具有更严格的对齐方式。

但是,无论对齐方式如何,指向任何对象类型的指针都可以转换为char *并返回。 char *指针保留原始的更强对齐。

您可以使用union来创建更强烈对齐的char数组:

union u {
    long dummy; /* not used */
    char a[sizeof(long)];
};

联盟的所有成员都从同一地址开始:开头没有填充。当在存储中定义union对象时,它必须具有适合于最严格对齐的成员的对齐。

我们上面的union u严格对齐long类型的对象。

违反对齐限制可能会导致程序在移植到某些体系结构时崩溃。或者它可能有效,但是会产生轻微到严重的性能影响,具体取决于是否在硬件中实现了错位的内存访问(以一些额外的周期为代价)或者在软件中(内核陷阱,软件模拟访问,需要付费)许多周期)。

相关问题