放置新问题

8
在这个我需要C++数组类模板,它是固定大小的、基于堆栈的,不需要默认构造函数答案中,我发布了一段使用char数组和placement new的代码。对我来说,这是非常正常的事情。但是根据评论,这段代码是错误的。
有人可以更详细地解释一下吗?
具体而言,数组可能出现什么问题。从评论中我理解到T x[size];可能无法适应char x[size*sizeof(T)];。 我不相信这是真的。
编辑:
我越来越困惑了。在结构的情况下,我知道什么是对齐方式。是的,当你有一个结构时,属性会以你所想象的不同偏移量开始。
现在我们回到了数组。你告诉我T x[size];char x[size*sizeof(T)];的大小是相同的,然而我不能像访问T数组那样访问char数组,因为可能存在某些对齐问题。当这些数组的大小相同时,如何会存在对齐问题呢?
编辑2:
好吧,我终于明白了,它可能会从错误的地址开始。
编辑3:
谢谢大家,你们可以停止发布了 :-) 哎呀,这真是让我感到烦恼。我从来没有意识到这是可能的。

1
你误解了这些评论。它“适合”,但是字符数组可能未对齐。 - Steve Jessop
@Steve 但这到底是什么意思,请解释一下。比如字符数组会以相反的顺序进行索引,还是什么?给我一些可能发生的例子。我只理解结构体和小/大端的对齐方式。 - Šimon Tóth
1
@让我静一静:在这里看一下http://en.wikipedia.org/wiki/Segmentation_fault#Bus_error,了解一下原因。 - Eugen Constantin Dinca
就像所有的C++事物一样,std::vector比数组更受青睐。因为内存是动态分配的,所以保证正确对齐。 - Martin York
@Martin,当你可以使用向量时,它确实很好。但对于我来说,创建一个没有默认构造函数的对象数组就成了不可能的领域。 - Šimon Tóth
显示剩余2条评论
5个回答

13

T x[size]数组总是恰好适合于size * sizeof(T)字节,这意味着char buffer[size*sizeof(T)]始终足够存储这样的数组。

我理解问题在于你的char数组不能保证对于存储类型为T的对象而言是正确对齐的。只有通过mallocnew分配的缓冲区才能保证对于所有标准数据类型(或由标准数据类型组成的数据类型)都正确对齐,且大小不超过该缓冲区大小,但是如果你只是显式地声明了一个char数组(作为本地对象或成员子对象),则没有这种保证。

对齐意味着在某些平台上可能严格(或不那么严格)要求按照比如4个字节的边界分配所有的int对象。例如,你可以将一个int对象放置在地址0x10000x1004,但是不能将一个int对象放置在地址0x1001。或者更准确地说,你可以这样做,但是任何试图将此内存位置作为int类型的对象访问的尝试都将导致崩溃。

当你创建一个任意的char数组时,编译器不知道你计划用它做什么。它可以决定将该数组放置在地址0x1001处。因为上述原因,如果在这样的非对齐缓冲区中尝试创建一个int数组,就会失败。

某些平台上的对齐要求是严格的,这意味着任何尝试使用不正确对齐数据的操作都将导致运行时失败。在其他一些平台上,它们的要求不那么严格:代码将工作,但性能会受到影响。

需要正确对齐有时意味着当你想在任意的char数组中创建一个int数组时,可能必须将int数组的开始位置从char数组的开始位置向前“移动”。例如,如果char数组位于0x1001,则你只能从地址0x1004(即索引为3的char元素)开始构造该数组。为了容纳被移动的int数组的尾部,char数组需要比size * sizeof(T)计算出的大小大3个字节。这就是为什么原始大小可能不够的原因。

一般来说,如果您的char数组没有任何对齐方式,那么您需要一个size*sizeof(T)+A-1字节的数组来容纳一个对齐(即可能偏移)的T类型对象数组,这些对象必须在A字节边界上对齐。


@Let_Me_Be: 它指的是数组在内存中的位置。该数组具有起始地址,该地址可能不是通过放置新的方式存储其他对象的有效起始地址。例如,如果您尝试在其中构造一个int,并且您的计算机不支持将ints存储在奇地址上,则在数组从奇地址开始的情况下,您就会失败。 - sellibitze
哦,终于有意思了。所以基本上对齐是在变量之前添加而不是之后? - Šimon Tóth
@Let_Me_Be:“之前”和“之后”是相对的术语。填充字节是为了对齐对象而在其之前添加的,但是在上一个对象“之后”添加。 - AnT stands with Russia
1
@Let_Me_Be:我认为AndreyT的回答应该已经解决了所有问题。此外,让我指出即将推出的C++标准将提供处理这个问题的工具(请参见std::aligned_storage<Size,Alignment>sizeofalignof运算符)。 - sellibitze
然而,OP并不是在询问任意的字符数组,而是关于一个作为类的第一个成员(同时包含size_t)的字符数组。这个类以及它的第一个成员难道不必至少对齐到4字节边界吗? - UncleBens
显示剩余4条评论

0

T 可能与 char 对齐方式不同。

此外,Itanium ABI(例如)为非 POD 数组指定 cookie,因此它知道在删除时要跨越多少元素(以调用析构函数)。通过 new 进行的分配如下(如果我没记错的话):

size_t elementCount;
// padding to get native alignment for 1st element
T elements[elementCount];

因此,一个16字节对齐的对象的分配是:

size_t elementCount; // 4
char padding[16 - sizeof(elementCount)];
T elements[elementCount]; // naturally aligned

在某些系统上,char可以对齐到1,因此...您可以看到不对齐和大小问题的位置。内置类型不需要调用其dtors,但其他所有内容都需要。


“T 可能与 char 对齐方式不同。” 这是什么意思? - Šimon Tóth
@Let_Me_Be 类型的对齐方式由编译器确定,基于对象的大小和内容 - 它通常是针对你所针对的平台的一个良好隐藏的实现细节。这意味着所有对象都在这个字节边界上创建。对于 char 类型(虽然实现定义),它可能(假设)被放置在 1 字节边界上。对象/类通常具有更大的对齐值,例如 4 - 这意味着如果 char 前面有一个 T(例如在函数中),则堆栈上的 T 可能会浪费 3 个字节。编译器假定所有参数都以自然方式传递/创建... - justin
如果一个对象没有按照自然对齐方式传递,那么编译器所做的地址假设将导致程序以异常方式运行,因为程序将从可能偏离几个字节的地址读取和写入。 - justin

0
在某些系统上,内存访问必须是“对齐的”。为了简单起见,这意味着地址必须是类型的“对齐要求”的某个整数倍(请参见C++标准的3.9/5)。
例如,假设sizeof(int) == 4
int *intarray = new int[2];        // 8 bytes
char *charptr = (char *)intarray;  // legal reinterpret_cast
charptr += 1;                      // still 7 bytes available
*((int*)charptr) = 1;              // BAD!

charptr的地址不是4的倍数,因此如果您的平台上int需要4字节对齐,则程序具有未定义行为。

同样地:

char ra[8];
int *intptr = reinterpret_cast<int*>(ra);
intptr[0] = 1;  // BAD!

ra的地址不能保证是4的倍数。

不过这没关系:

char ra = new char[8];
int *intptr = reinterpret_cast<int*>(ra);
intptr[0] = 1;  // NOT BAD!

因为new保证分配的char数组对于任何小到可以适应分配的类型都是对齐的(5.3.4/10)。
放弃自动变量,很容易看出编译器为什么可以不对齐数据成员。考虑:
struct foo {
    char first[1];
    char second[8];
    char third[3];
};

如果标准保证second是4字节对齐的(仍然假设int是4字节对齐的),那么这个结构体的大小至少为16(其对齐要求至少为4)。实际上,由于标准的写法,编译器可以将此结构体大小设置为12,没有填充和对齐要求。

我完全理解第一个,但我不知道为什么第二个是错误的。 - Šimon Tóth
好的,在第一个例子中,“charptr”保证不是4的倍数,我通过在对齐值上加1来确保这一点。在第二个例子中,“ra”可能位于4的倍数地址,但也可能不是。标准对此并不关心。 - Steve Jessop
是的,我没有意识到first[1]可以从非对齐地址开始。我以为这是不可能的。 - Šimon Tóth
@Let_Me_Be:就像我在另一个问题中所说的那样,我不知道标准中是否禁止这样做。这并不一定意味着没有任何规定,但在有人提供证据之前,我仍然坚持我的观点 :-) - Steve Jessop

0

§5.3.4/10:

一个新表达式将请求的空间量作为类型为std::size_t的第一个参数传递给分配函数。该参数不应小于正在创建的对象的大小;如果对象是数组,则可以大于正在创建的对象的大小。对于char和unsigned char数组,new表达式的结果与分配函数返回的地址之间的差异必须是任何对象类型最严格对齐要求(3.9)的整数倍,该对象类型的大小不超过正在创建的数组的大小。
这允许使用用new分配的char数组来放置构建适当大小的其他类型的对象。预分配的缓冲区必须在堆上分配。否则,您可能会遇到alignment问题。

0

char x[size*sizeof(T)];可能不考虑对齐,而T x[size];则会考虑。在使用需要16字节对齐的SSE类型时,对齐(2)也非常重要。


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