我在代码审查中对 std::copy 行感到担忧。我对其运行了 clang sanitizer,确认了该问题。我进入调试器,发现副本并未像我预期的那样溢出到结构中的下一个字段,我不明白为什么会这样。
在下面的程序中,在 std::copy 中,8
旨在表示 8 个元素(long 类型);但是 的类型src.arr[0]
是pointer to long[4]
。因此,我预计8
会尝试复制 8 个 long[4] 项,从而溢出 后面的数组arr
;即overflowArray
字段。但在我的测试中,它没有这样做,并且在审查的程序中它也没有溢出。
下面的程序中,clang 报告以下行未定义行为,但目前给出了预期结果。一个重要的问题:此代码在不同的平台和/或编译器上是否会导致不正确的结果?
std::copy(src.arr[0], src.arr[0] + 8, dst.arr[0]);
#include <cstdlib>
#include <iostream>
#include <numeric>
#include <cstring>
struct Astruct
{
long arr[2][4];
long overflowArray[8];
};
void printAstruct(Astruct str)
{
std::cout << "arr: ";
for( size_t i = 0; i < 2; ++i)
{
for( size_t j = 0; j < 4; ++j)
{
std::cout << str.arr[i][j] << " ";
}
}
std::cout << "; overflowArray: ";
for( size_t k = 0; k < 8; ++k)
{
std::cout << str.overflowArray[k] << " ";
}
std::cout << std::endl;
}
void fillArray(Astruct& );
int main()
{
Astruct src{};
Astruct dst{};
std::cout << "src size: " << sizeof(src.arr) << "; dst size:" << sizeof(src.overflowArray) << std::endl;
// SETUP
std::cout << "dst before copy" << std::endl;
printAstruct(dst);
long* srcStart = &src.arr[0][0];
memset(srcStart, 0xFF, sizeof(src.arr));
std::cout << "src:" << std::endl;
printAstruct(src);
// the Smell: src.arr[0] is type: array of 4 longs:
std::copy(src.arr[0], src.arr[0] + 8, dst.arr[0]); // <-- Undefined Behavior
std::cout << "\ndst after copy:" << std::endl;
printAstruct(dst);
}
输出:
src size: 32; dst size:32
dst before copy
arr: 0 0 0 0 0 0 0 0 ; overflowArray: 0 0 0 0 0 0 0 0
src:
arr: -1 -1 -1 -1 -1 -1 -1 -1 ; overflowArray: 0 0 0 0 0 0 0 0
dst after copy:
arr: -1 -1 -1 -1 -1 -1 -1 -1 ; overflowArray: 0 0 0 0 0 0 0 0 <-- I expected these 0's to be overwritten
看来我看到的是正确的,smell
正如 clang sanitizer 所确认的,但我不明白为什么没有发生溢出,因为8
应该将结束地址推进到 8long
秒之后。
用消毒剂冲洗并确认了气味:
runtime error: index 8 out of bounds for type 'long[4]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior
为什么 overflowArray 没有被覆盖?它可以在其他平台和/或编译器中被覆盖吗?
我把代码改成:
std::copy(&src.arr[0][0], &src.arr[0][0] + 8, &dst.arr[0][0]);
因为&src.arr[0][0]
是pointer to long
。但有人告诉我这太令人困惑了,而且原始地址和我更改的地址都相同。
让我们看看你的结构。
它看起来像这样:
有 8
long
秒用于arr[][]
,然后有 8long
秒用于overflowArray
(为了间隔目的,我将其缩短了)。现在,发生了什么事:
因此,
src.arr[0]
是 类型long[4]
。但是 的第一个参数std::copy
是值参数。因此它是按值传递的。当您传递一个
long[4]
值时,它会衰减为long*
指向数组的第一个元素。类似地,当您这样做时
src.arr[0]+8
,它会说“好的,添加long[4]
一个int
。这没有意义!让我们尝试衰减!”并且它再次将转换long[4]
为long*
指向数组第一个元素的。回首往事:
现在,这违反了 C++ 标准的部分内容;具体来说,我们有一个指向长度为 4 的数组的指针,并且我们正在前进 8 步。
在实践中,像数组一样处理
[2][4]
数组[8]
是可行的。如果 C++ 标准中的某些措辞说这是不允许的?编译器会忽略它。在 C 和 C++ 代码库中,使用[a][b][c]
数组(具有编译时常量大小)作为[a*b*c]
数组的变体太常见了,因此无法在此处强制执行任何类型的 UB。如果您接受可以将其视为
long arr[2][4]
一个连续的 8 个长整型块,那么arr[0]
通过arr[0]+8
(经过适当衰减后)确实是指向该内存块的起点和终点的指针。我手头没有标准来搜索这里到底发生了什么,但看起来数组已经退化为指针。可以通过创建围绕副本的包装器并打印其签名来轻松检查:
以上印刷品:
当调用如下命令时:
我只能推测这是因为 std::copy(以及上面的包装器)按值获取参数,并且没有对数组引用的重载。对数组的引用
long[4]
只是衰减为指向第一个元素的指针。然后第二个参数通过添加 8(即数组的大小)正确地计算出结束迭代器long[2][4]
。我会这样写吗?可能不会——这确实很令人困惑。