有时访问 const ptr 的未定义行为

Undefined behaviour accessing const ptr sometimes

我有一个头文件定义为

#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("%i\n", arr);

        printf("%f\n", arr[0]);
        printf("%f\n", 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 output

decompiled msvc release

在此声明中

const double* arr = B<size>().arr;

声明了一个指向临时数组(的第一个元素)的指针,该数组在声明后将不再存在

因此取消引用指针会导致未定义的行为。

似乎完全巧合的是,较小的分配总是在不会被 printf 中存在的 rep stosd 指令擦除的位置进行寻址。不像我最初认为的那样是由奇怪的编译器优化引起的。

What does the "rep stos" x86 assembly instruction sequence do?

我也不知道为什么我决定这样做。不完全是我问的问题,但我最终想要一个编译时查找 table 所以真正的解决方案是 static inline constexpr auto arr = B<size>() on c++20。这就是代码看起来很奇怪的原因。

当你写道:

const double* arr = B<size>().arr;

上面的语句初始化一个指向const double指针,即const double*命名为arr使用 临时 数组对象。由于此临时数组对象将在 full-expression 结束时被销毁,因此使用 arr 将导致 未定义的行为 .

Why does this undefined behaviour only occur sometimes?

Undefined behavior means anything1 can happen including but not limited to the program giving your expected output. But never rely(or make conclusions based) on the output of a program that has undefined behavior.

所以您看到(也许看到)的输出是未定义行为的结果。正如我所说,不要依赖具有 UB 的程序的输出。程序可能会崩溃。

因此,使程序正确的第一步是删除 UB。 然后并且只有那时你可以开始对程序的输出进行推理。


1有关未定义行为的更准确的技术定义,请参阅 this 其中提到:没有对程序行为的限制.