我有一个定义为的头文件
#pragma once
#include <iostream>
template<int size>
struct B
{
double arr[size * size];
constexpr B() : arr()
{
arr[0] = 1.;
}
};
template<int size>
struct A
{
const double* arr = B<size>().arr;
void print()
{
// including this statement also causes undefined behaviour on subsequent lines
//printf("%in", arr);
printf("%fn", arr[0]);
printf("%fn", arr[0]); // ???
// prevent optimisation
for (int i = 0; i < size * size; i++)
printf("%f ", arr[i]);
}
};
并用称之为
auto a = A<8>();
a.print();
现在,此代码仅在使用msvc发布模式编译时预期运行(全部使用c++17编译(。
预期输出:
1.000000
1.000000
msvc调试:
1.000000
-92559631349317830736831783200707727132248687965119994463780864.000000
gcc通过mingw(带和不带-g
(:
1.000000
0.000000
然而,这种行为是不一致的。如果我将double arr[size * size]
替换为double arr[size]
,则会给出预期的输出。当然,如果我在堆上分配arr
,就不会再有问题了。
我看了msvc调试构建的程序集,但没有发现任何异常。为什么这种不明确的行为只是偶尔发生?
asm输出
反编译的msvc发行版
在此声明中
const double* arr = B<size>().arr;
声明了一个指向临时数组(的第一个元素(的指针,该指针在声明之后将不活动
因此,取消引用指针会导致未定义的行为。
当你写:
const double* arr = B<size>().arr;
上述语句初始化指向const double
的指针,即名为arr
的const double*
,并使用临时数组对象。由于此临时数组对象将在完整表达式结束时销毁,因此使用arr
将导致未定义的行为。
为什么这种未定义的行为只会偶尔发生?
未定义的行为意味着任何1都可能发生,包括但不限于提供预期输出的程序。但是永远不要依赖(或根据未定义行为的程序的输出得出结论(。
所以您看到的输出(可能看到的(是未定义行为的结果。正如我所说,不要依赖于有UB的程序的输出。程序可能会崩溃。
因此,使程序正确的第一步是删除UB然后并且只有到那时您才能开始对程序的输出进行推理。
1对于未定义行为的更准确的技术定义,请参阅此处,其中提到:对程序的行为没有限制
似乎完全巧合的是,较小的分配总是在一个不会被printf中的rep stosd
指令擦除的位置进行寻址。并不像我最初认为的那样是由奇怪的编译器优化引起的。
什么是";rep-stos";x86汇编指令序列做什么?
我也不知道为什么我决定这样做。这不是我问的问题,但我最终想要一个编译时查找表,所以真正的解决方案是c++20上的static inline constexpr auto arr = B<size>()
。这就是为什么代码看起来很奇怪。