问题描述
我最近遇到了一小段代码,使我感到很奇怪。
#include <iostream>
#include <array>
#include <vector>
int main()
{
std::vector<std::array<int,2>> idx;
for (auto ii = 0; ii < 2 * 10; ii += 2)
{
idx.push_back ({ii,ii + 1});
}
auto &ind = reinterpret_cast<std::vector<int> &> (idx);
idx.push_back ({ 40,50 });
for (auto ii: ind)
{
std::cout << ii << std::endl;
}
}
我确定代码会给出错误的结果,但是它给了我预期的结果。 一些观察:
- 在这种情况下,reinterpret_cast的行为是不确定的。
- 这不是很好的代码
- 它在我测试过的所有编译器上均可工作。通过工作,我的意思是它给了我很好的结果。
话虽如此,我阅读了VS 2019的std :: vector实现。std::vector::size()
的值是根据_Mylast - _Myfirst
计算出来的。
我的问题是 _Mylast如何正确初始化?
我阅读了代码,但是找不到。这不是关于不确定行为的问题,而是关于C ++向量的实现。
解决方法
在此实现中,矢量获取大小的方法是:将指针指向最后一个元素,然后从该指针中减去指向数据开头的指针。该结果就是向量中元素的数量。当您使用idx
执行此操作时,由于_Mylast
与11 * sizeof(std::array<int,2>)
之间有_Myfirst
个字节,因此得到11个。当您使用ind
时,_Mylast
和_Myfirst
具有相同的值,因此它们是相同的字节数,但是现在它们是int*
而不是{{ 1}},这意味着编译器将根据std::array<int,2>*
来处理减法运算,从而得出方程式
sizeof(int)
给出22的“正确”结果。就标准而言,这是所有未定义的行为,但这就是为什么它似乎具有正确的结果的原因。如果vector允许将大小存储为类成员(允许这样做),那么您将从size = 11 * sizeof(std::array<int,2>) / sizeof(int)
和idx
中获得相同的值。
- UB的典型处理方法是什么都不做,并假设程序员知道他们在做什么,所以即使您错了,它也会采取最小的阻力之路,并继续做正确的事情。
- 典型的
vector
实现为三个指针:一个指向数据存储区(我们将称为datap
),一个指向数据存储区的末尾,因此您可以轻松地知道何时要从头开始,需要重新分配以获取更大的数据存储区(称为capp
),以及指向该数据存储区中使用的数据结尾的指针(endp
)。 - 通常,元素的数量是一些通用指针算法
endp - datap
,它将提供endp
和datap
之间的元素数量。
Visual Studio遵循典型的实现,因此使用较小的数组,因为我懒于绘制整个该死的东西,所以让我们看一下vector<array<int,2>>
的2个元素和4个容量。 / p>
+-------+-------+-------+-------+
| 1 | 2 | X | X |
+-------+-------+-------+-------+
^ ^ ^
datap endp capp
reinterpret_cast
用不同的眼光看待事物。它不会更改任何值,并且由于vector
的两个视图具有完全相同的成员,并且这些成员具有完全相同的大小datap
,因为一个视图与datap
位于同一位置对于其他。它将使用另一种类型来解释,在这种情况下,该类型的大小只有一半。 <vector<int>>
看起来像
+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | X | X | X | X |
+---+---+---+---+---+---+---+---+
^ ^ ^
datap endp capp
大小和容量的两倍。
您可以使用更复杂的结构来执行此操作,但是可能会填充,vtables和其他有趣的东西,从而使包含的int
不能完美对齐,这就是为什么人们说不要相信您的结果。 datap
,endp
和capp
将始终指向相同的位置,但是如果投射到vector<string>
,则大小甚至可能为1。
+-----------------------+-------+
| 1 |
+-----------------------+-------+
^ ^ ^
datap endp capp
实际上,vector
实现可能没有使用三个指针。 Visual Studio的下一个更新可以实现vector
,其中包含用于数据的指针和用于容量和大小的两个整数。如果满足vector
的要求,则某些天才可以用独角兽的角,凤凰羽毛和一堆Care Bear填充物实现。该实现也可能超出职责范围,并捕获错误并警告您(Visual Studio在vector
的调试版本中正是这样做的,以捕获运算符[]
中的越界访问,但是我不知道它怎么会用reinterpret_cast
欺骗编译器或入侵五角大楼。