在重载模板类的构造函数中,我有以下 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
最佳答案
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::vector
或std::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()
)。
–
-
@PeterCordes
s.puzzle
是std::array
。在此上下文中s.size()
与 相同s.puzzle.size()
。我必须看看是否可以在此上下文之外重现此情况 – 可能还会发生其他事情。
–
|
–
int main(){}
,我无法使用适用于 x86-64 Linux 的主流 Clang 18.1 重现它。 。不幸的是,Godbolt 的 clang 安装没有可用于-target arm64-apple-darwin -stdlib=libc++
从 Linux 交叉编译 macOS 的标头。cout << x
用foo(x)
我只给出原型的函数替换 ,我得到了一个看起来很正常的循环:–
–
–
–
|