在重载模板类的构造函数中,我有以下 for 循环。原始循环更复杂,但我删除了除x在调试期间进行有用计算之外的所有变量,以便在更大的结构中获得最小示例。这就是目前的情况:

for (int x = 8-1; x >= 0; x--)
{
    std::cout << x << "-";
    if (x < 0) break;
}

当我运行它时,它会打印:7-6-5-4-3-2-1-0--1--2--3--4--5--6--7--8--9--10--11-(…)

调试器显示反汇编(ARM)如下:

    0x102aeef74 <+100>: mov    x0, x19
    0x102aeef78 <+104>: mov    x1, x20
    0x102aeef7c <+108>: bl     0x102bbb1f4               ; symbol stub for: std::__1::basic_ostream<char, std::__1::char_traits<char>>::operator<<(int)
    0x102aeef80 <+112>: mov    x1, x21
    0x102aeef84 <+116>: mov    w2, #0x1
    0x102aeef88 <+120>: bl     0x102acfbc8               ; std::__1::__put_character_sequence[abi:v160006]<char, std::__1::char_traits<char>> at ostream:753
    0x102aeef8c <+124>: sub    w20, w20, #0x1
->  0x102aeef90 <+128>: b      0x102aeef74               ; <+100> at LexPermutationPDB.h:108:13

循环条件似乎被优化掉了。如果你将代码从上下文中提取出来(例如,只将循环放在主函数中),它就可以正常工作。我猜是某个地方存在未定义的行为,导致了糟糕的优化。

我在 MacOS Ventura 中运行 Xcode 版本 15.2(15C500b),代码处于发布模式,即-Os

在添加从出现问题的类继承的新类时出现错误。新类重载单个函数并使用不同的构造函数。此错误发生在调用构造函数时,但父类已经初始化之后。

我本来打算尝试不同的编译器,但是还有其他方法可以找出问题所在吗?也许新版本的编译器已经修复了某些问题。

– 更新 –

以下是失败的一个最小工作示例:

% cat test.cpp                 
#include <iostream>
#include <array>

template <int width, int height>
class MyClass {
public:
    MyClass()
    {
        Reset();
    }
    void Reset()
    {
        for (size_t x = 0; x < size(); x++)
            puzzle[x] = x;
    }
    size_t size() const { return width*height; }
    std::array<int, width*height> puzzle;
};

int main(void)
{
    MyClass<4, 4> s;
    for (int x = 7; x >= 0; x--)
    {
        if (x < 0) break;
        std::cout << x << "-";
    }
    //std::fill(s.puzzle.begin(), s.puzzle.end(), -1);
    std::fill(&s.puzzle[0], &s.puzzle[s.size()], -1);
}
% g++ -std=gnu++17 -Os test.cpp
% ./a.out                      
7-6-5-4-3-2-1-0--1--2--3--4--5--6--7--8--9--10 (...)

编译为

Apple clang version 15.0.0 (clang-1500.0.40.1)
Target: arm64-apple-darwin22.6.0
Thread model: posix

14

  • 4
    当我运行这个时我们也想运行这个。应该提供一个


    – 

  • 将其放入 中int main(){},我无法使用适用于 x86-64 Linux 的主流 Clang 18.1 重现它。 。不幸的是,Godbolt 的 clang 安装没有可用于-target arm64-apple-darwin -stdlib=libc++从 Linux 交叉编译 macOS 的标头。cout << xfoo(x)我只给出原型的函数替换 ,我得到了一个看起来很正常的循环:


    – 

  • 这是一个具有模板和继承的相当复杂的类层次结构,但我会努力删除更多的东西,看看是否可以缩小问题范围,这样我就可以得到比原始代码更小的示例。


    – 

  • 3
    一般来说,结果越奇怪,您越需要提供可重现问题的(MRE)。我们无法重现的问题,我们无法帮助您修复,如果您发现编译器错误,您需要将 MRE 连同错误报告一起交给开发团队,以便他们重现并修复它。


    – 

  • 4
    循环后面的代码是否可能导致未定义的行为?如果是这样,编译器可能会认为它是不可到达的,并推断循环终止条件必须始终为假。这对它来说是完全合法的。


    – 



最佳答案
1

线索来自 Nate Eldredge 在评论中的一条评论。for 循环后面的一行是:

std::fill(&s.puzzle[0], &s.puzzle[s.size()], -1);

应该是:

std::fill(s.puzzle.begin(), s.puzzle.end(), -1);

请注意这也有效:

std::fill(&s.puzzle[0], &s.puzzle[s.size()-1]+1, -1);

第一行访问超出了数组末尾的内容,这导致了未定义的行为,从而导致 for 循环被错误地优化。(请注意,这两个版本的代码在许多不同的编译器上都能正常工作。如果不看 C++ 标准,我就不清楚这是一个错误还是我在标准中不知道的东西。)

教训:即使问题出现在 for 循环中,我也应该在出现明显问题之后的代码中查找错误。(事实上,在汇编中循环之后没有显示任何代码,这暗示它已被优化,但我没有注意到。)

7

  • 奇怪的是它没有产生警告,clang 仍然这样运行吗?如果发生这种情况,GCC 会发出警告


    – 

  • 但这真的是 UB 吗?我认为创建一个指向数组末尾“元素之后”的指针是合法的,前提是你不遵循它。我想代码在语法上看起来就像是遵循它一样,即使 & 运算符在语义上意味着它不是。这是&*无操作经验法则​​的反例吗?


    – 

  • @NateEldredge:s.puzzle必须是std::vectorstd::array或其他容器才能支持.size().end(),因此其operator[]函数可能返回一个引用,而该引用可能必须是一个真实对象。我认为,获取引用对象的地址来取消它“太晚了”。或者,如果这对于标准容器来说是安全的,那么可能是 OP 的自定义类。


    – 


  • @Swift-FridayPie 我希望它至少会使用 发出警告-Wall,但可能不会。您可能必须禁用优化并使用-fsanitize=undefined以避免循环变得无限,并让执行实际上到达 UB,其中清理器插入了额外的代码。另一方面,我仍然无法使用主线 clang 18 使用 withstd::vector<int>作为vec.size()大小来重现此问题。godbolt.org/z/Yhzrx3f4r 似乎正在使用单独的大小,s.size()而不仅仅是容器的大小(应该是s.puzzle.size())。


    – 


  • @PeterCordess.puzzlestd::array。在此上下文中s.size()与 相同s.puzzle.size()。我必须看看是否可以在此上下文之外重现此情况 – 可能还会发生其他事情。


    –