相当奇怪的叮当问题

Rather Strange clang issue

本文关键字:叮当 问题      更新时间:2023-10-16

在写这篇文章之前,我尝试了几次谷歌搜索,但说实话,我不知道该搜索什么。我有一个C++项目,并且一直很高兴地使用GNU编译器(g++)。今天我尝试用 clang++ 编译并得到一个段错误。

好吧,

好吧,我可以处理这个。在仔细阅读我的代码并打印一些东西后,我能够解决问题。然而,这个解决方案让我深感困扰和困惑。

情况是这样的:我正在使用一种树状数据结构来存储一个名为 Ligament 的类,但我将其存储在 std::vector 中。我通过存储"子"向量来做到这一点,这些向量实际上只是向量中父项和子项之间的整数偏移量。通过这种方式,我可以使用 this 指针访问儿童,即

child = this[offset];

然而,这些都不重要。这是这个问题:我有一个 Ligament::addChild(int) 函数,它接受一个整数并将其推送到作为 Ligament 成员的向量的背面:

void Ligament::addChild(uint32_t offset){
   children.push_back(offset);
}

非常简单的东西。通常,我会向addChild传递一个从称为fill的递归函数返回的参数:

//starting at root
uint32_t fill(vector<Ligament>& lVec, TiXmlElement * el){
    //store current size here, as size changes during recursion
   uint32_t curIdx = lVec.size();
   lVec.push_back(createLigament());
   //Add all of this Ligament's children
   TiXmlElement * i = el->FirstChildElement("drawable");
   for (; i; i=i->NextSiblingElement("drawable")){
      uint32_t tmp = fill(lVec, i) - curIdx;
      lVec[curIdx].addChild(tmp);
      //Does not work in clang++, but does in g++
      //lVec[curIdx].addChild(fill(lVec,i)-curIdx);
   }
   //return the ligament's index
   return curIdx;
}

fill 函数在 XML 元素上被调用,并首先遍历其子元素,深度优先。

抱歉,如果所有这些都不清楚,但问题的核心似乎是 for 循环中的内容。出于某种原因,我必须在将填充调用的返回值存储在变量中,然后再将其发送到 addChild 函数。

如果我不将其存储在临时变量中,似乎 addChild 函数不会更改子项的大小,但我无法想象为什么。

为了检查所有这些,我打印了这些调用前后儿童向量的大小,它从未超过 1。只有当我使用不是直接从函数返回的值调用 addChild 时,它才似乎有效。

在调用之前,我还打印出了 addChild 函数内部以及 for 循环内部的偏移量值。在所有情况下,这些值都是相同的,无论是在 clang++ 还是在 g++ 中。

由于问题已解决,我能够继续前进,但这是我希望能解决的问题。我做错了什么吗?

如果我能做更多的事情来使这个问题更清楚,请随意对我大喊大叫。

另外:我现在意识到通过这些递归通过引用传递 lVec 可能是不好的,因为push_back调用可能会导致地址更改。这是一个合理的担忧吗?

编辑:

因此,正如人们所指出的那样,我最后的担忧竟然与这个问题有关。fill 调用有可能调整向量的大小,而 lVec[curIdx] = 修饰符将更改向量中的元素。这些事情发生的顺序可能会产生严重的后果。

作为后续行动,使用 tmp 变量是否可以接受?仍然存在重新分配的问题...我想我会使用 SHR 的地图建议,然后在一切都说完后将其转换为矢量。

// Does not work in clang++, but does in g++:
lVec[curIdx].addChild(fill(lVec,i)-curIdx);

您看到的错误是由于依赖于评估顺序。由于fill(lVec, i)可能会导致lVec重新分配其元素,因此如果在fill(lVec,i)之前评估lVec[curIdx],程序将具有未定义的行为。未指定函数参数的计算顺序 - 以及确定要调用哪个函数的后缀表达式。

我认为这是未定义的行为。

您推入 Vector,并在同一命令中更改它。

一个编译器可能首先执行fill,另一个编译器可能先执行lVec[curIdx]

如果是这种情况,当您使用 map<uint32_t,uint32_t> 而不是 vector 时,它将适用于两个编译器。 因为 map 不需要内存是顺序的。