【问题标题】:std::vector inconsistent crash between push_back and insert(end(),x)push_back 和 insert(end(),x) 之间的 std::vector 不一致崩溃
【发布时间】:2012-07-25 15:22:05
【问题描述】:

将此代码放入MS Visual C++ 2010,编译(调试或发布),insert()循环会崩溃,push_back循环不会:

#include <vector>
#include <string>

using std::vector;
using std::string;

int main()
{
   vector<string> vec1;
   vec1.push_back("hello");

   for (int i = 0; i != 10; ++i)
      vec1.push_back( vec1[0] );

   vector<string> vec2;
   vec2.push_back("hello");

   for (int i = 0; i != 10; ++i)
      vec2.insert( vec2.end(), vec2[0] );

   return 0;
}

问题是 push_back() 和 insert() 都通过引用获取新项目, 并且当向量被重新分配以获得更多空间时,新项目在插入之前会失效。

GCC 应该也有这个问题。我没有检查 Clang,但这取决于它使用的是哪个 STD 库。

MSVC2010 在 push_back() 中有一些额外的代码,用于检测新项目是否实际上是向量中的项目。如果是这样,它会记录项目的索引,并在分配内存后使用该索引插入项目(而不是使用现在无效的引用)——使用 _Inside(_STD addressof(_Val))

MSVC 的额外代码是非标准的吗?

我担心的是我不确定我可能在什么代码中做了类似 vec.push_back(vec[1]);或 vec.insert(it, vec[2]); 我必须查看数百行甚至数千行使用 push_back 和 insert 的代码,这只是我自己的代码...... 3rd 方库也可能受到影响。

我假设使用这种技术可以使 GCC 以可怕的方式死掉(我没有看到额外的代码来处理这种情况,但 valgrind 在我的简单示例中没有检测到它,因此更难测试),

如何最好地发现和避免犯这个错误?

MSVC2010 的额外 push_back() 代码是否非标准?当 MSVC 发现以这种方式使用的向量时,是否应该改为检测并断言? (即安全计算计划)

我正在考虑破解 MSVC2010 和 GCC 的标头来检测这些情况。

还有其他想法吗?

谢谢, 保罗

PS:另请注意,如果您可以保证向量不需要调整大小,则此用法非常好(且有效)

【问题讨论】:

  • 代码在 g++ 4.6 下运行良好。 Valgrind 也没有发现任何问题。
  • 在 MSVC 2012 RC 下工作正常。
  • 我刚刚发现了这个确切的错误,已在 MSVC 2011 和 2012 中修复:connect.microsoft.com/VisualStudio/feedback/details/735732
  • 我认为“破解 MSVC2010 和 GCC 的标头来检测这些情况”是一个糟糕的主意。如果您的代码库确实有“数百行甚至数千行使用 push_back 和 insert 的代码”,那么您很可能是团队的一员,其他人不会看到您的 hack,因为它是标准库的一部分,而不是您自己的代码。此外,编译器更新可能会使您的更改消失。 +1 链接到 MS 错误。
  • 我最终破解了标头(首先检测,如果需要,稍后修复),因为它只需要 MSVC 2010,所以我只需要检测并解决在 MSVC 2012 之前使用它的情况. 理想情况下,MS 会针对此类错误发布补丁。另外,我可以记录黑客攻击。它在一个复杂的编译环境中,所以已经有一个很长的过程来编译所有需要的库。

标签: c++ visual-studio-2010 vector


【解决方案1】:

好的,我在virtualbox上安装了Win8 + MSVC2012试试看。天哪,Windows 8 的鼠标很烦人,没有按钮可以推动只是悬停,这对于窗口中的屏幕来说很难做到。

结果很有趣,但恕我直言仍然不一致。

MSVC 2010:错误来自移动语义,正如 ecatmur 所建议的那样。

问题在于 v.insert(v.end(),v[0]);将选择 insert(it, T && val) 方法,这在两个方面是错误的: 1) 它可能导致 v[0] 的破坏。似乎没有,这向我表明 const& 引用被保留,新版本是通过复制而不是移动创建的。 并且 2) 在调整向量大小之前,代码路径不会复制 val。

请注意,由于 push_back(&&) 中的额外代码(黑客攻击?),该问题并未很快被注意到 - 请参阅底部有关 MSVC2012 的进一步评论。

(注意 insert(it,const&) 会在调整向量大小之前先正确复制新项目,因此如果选择了正确的方法,则根本没有问题。

在 MSVC 2012 中,通过正确选择 insert(it, const T & val) 方法来解决此问题, 但是你仍然可以看到 push_back() 有一些额外的代码来“修复”不正确的用法。

考虑这个测试:

#include <vector>
#include <string>

using std::vector;
using std::string;

int main()
{
   vector<string> vec1;
   vec1.push_back("hello");

   for (int i = 0; i != 1000; ++i)
   {
       string temp = vec1[0];
      vec1.push_back( std::move(vec1[0]) );
   }

   vector<string> vec2;
   vec2.push_back("hello");

   for (int i = 0; i != 1000; ++i)
   {
       string temp = vec2[0];
      vec2.insert( vec2.end(), std::move(vec2[0]) );
   }

   return 0;
}

在这两种情况下,std::move() 用于强制选择 && 移动方法。 在这两种情况下,代码都会导致灾难并有望崩溃。

然而,在 MSVC 2012 中,push_back() 循环工作正常,因为 push_back(&&) 中有一些额外的代码可以检测 _Val 是否与向量在同一个地址空间中,如果是,则进行复制而不是比移动。 但是,如果新项目不是严格地在相同的内存空间中但仍然是原始向量的一部分(例如 pimpl 指针)怎么办?我可以想象如何让 push_back(&&) 像它应该的那样死掉。

当然这实际上不是必需的,如果程序员说 std::move() 那么这就是应该发生的,对吧?额外的检查肯定会使用一些不必要的 CPU 周期。

insert() 循环没有这种技巧,这也意味着错误地使用 std::move() 有时只会导致损坏。就我个人而言,我更喜欢快速故障而不是仅在您向客户端演示时发生故障。

所以...解决方案...

  1. 不要使用 v.insert(v.end(), v[0]) 或类似的。这是一个不合理的要求,因为第 3 方代码(例如 Boost、VTK、QT、tbb、xml 库等)可能会在其数百万行代码中的某个地方使用它。 我使用的所有第 3 方库,我都会重新编译,所以无论我的代码受到什么影响,它们也会受到影响。

  2. 升级到 MSVC 2012 RC。我必须等到它变成金牌,然后它才能按预期工作(在其他部分有新的和令人兴奋的错误)。

  3. 破解标头以检测使用情况。我已经这样做了,但检测工作的唯一时间是代码实际运行时。

  4. 破解标题以修复插入(&&)。 (并重新编译所有库/项目 - 叹息)。 最简单的方法是简单地注释掉 insert(&&) 变体(然后我们回到 C++11 之前的性能)。 另一种方法是使用相同的 push_back(&&) hack,尽管我不认为这是一种可靠的方法。也许 push_back(&&) 也应该被注释掉。

进一步更新: 我修复了标题。结果很简单……

MSVC2010 的 insert(&&) 声明如下所示:

template<class _Valty>
iterator insert(const_iterator _Where, _Valty&& _Val)

MSVC2012 的 insert(&&) 删除了模板部分,现在看起来像这样:

iterator insert(const_iterator _Where, _Ty&& _Val)

所以我只是从 MSVC2010 的 insert() 中删除了模板化的 _Valty,现在选择了正确的方法。它现在也匹配 push_back(&&) 的声明方式(即参数上没有模板)。 emplace*(&&) 方法仍有模板化参数,但没有 const& 混淆。

【讨论】:

    【解决方案2】:

    编辑:最初我的印象是插入现有元素可能是未定义的行为;我不再相信它,原因如下:

    根据How to insert a duplicate element into a vector?,标准中没有禁止插入对现有元素的引用的语言。指代迭代器和引用失效的语言只能被解读为指代操作完成后的行为(在没有其他指示的情况下)。

    请注意,根据The behavior of overlapped vector::insert,指定insert(it, first, last) 的迭代器参数不应是序列中的迭代器; push_back 上没有任何此类语言意味着特别允许对序列的引用(根据inclusio unius est exclusio alterius的法律原则)。

    查看您链接的错误报告,我猜 MSVC 在这种情况下崩溃是由于他们的代码在存在 C++11 移动语义的情况下中断,并且不是故意的。 g++ 通过(我认为)将插入的元素复制到新分配的内存中的适当位置来处理这种情况,在复制/移动现有元素之前:

    void insert(it, const T &t) {
        if (size() + 1 > capacity()) {
            T *new_data = (T *) malloc(sizeof(T) * capacity() * 2);
            new (&new_data[it - begin()]) T(t);
            // move [begin(), it) to [new_data, &new_data[it - begin()])
            // move [it, end()) to [&new_data[it - begin() + 1], &new_data[size() + 1])
        }
        ...
    }
    

    您可以使用自己的类模板包装std::vector,而不是修改标题。如果您要修改标准实现,请注意不要破坏确保不会发生重新分配的代码:

    v.reserve(v.size() + 1);
    v.push_back(v[0]);
    

    【讨论】:

    • 谢谢。我有一种感觉,标准委员会会考虑这个特殊问题,因此提出了这个问题。重新包装向量,我之前考虑过并做过,但是我必须重新实现所有构造函数并调整所有直接使用 std::vector 的代码,所以我希望避免这种情况。这似乎是一个非常严重的问题,以至于我认为以前必须对此采取一些措施。
    • 评论编辑:我不确定它与移动语义有关。如果它只是通过 const 引用传递,并且在调整向量大小之前未复制新项目,则该错误仍然会发生。我正在尝试查看 MSVC 2012 的矢量标头,但我没有看到任何额外的代码来修复错误,所以我很困惑。
    【解决方案3】:

    查看 4.4 的实现,push_back 和 insert 在需要增长缓冲区时调用 _M_insert_aux 来增长缓冲区,首先复制新元素(这意味着别名不是问题,因为此时尚未触及原始对象),然后是所有先前存在的元素。所以实现是好的。

    作为标准的一部分,别名没有限制,所以代码是合规的,不应该有未定义的行为。

    【讨论】:

    • 感谢您查看 GCC 标头,我想我可以在其中看到一些副本。实际上,我想它必须始终在调整大小之前进行复制,因为虽然新项目可能不是向量中的项目,但它可能是项目的成员,因此在调整大小期间仍然无效。 MSVC 的方法是仅在新元素的地址落在向量内存范围内时才进行复制。我想知道这是否足以保证正确性?
    • @user92979:这取决于该测试控制的代码。如果不查看源代码,我真的无法判断。
    【解决方案4】:

    在这里回答我自己的问题,

    我发现了一个与我的代码几乎相同的错误报告: http://connect.microsoft.com/VisualStudio/feedback/details/735732

    如上面评论中所述,它显然已在 MSVC 2012 中修复。

    我更深入地检查了 GCC 代码,它在这里提到可能相关: 00326 // 三个操作的顺序由 C++0x 规定 00327 // 情况下,移动可能会改变属于的新元素 00328 // 到现有向量。这只是呼叫者的问题 00329 // 通过 const lvalue ref 获取元素(参见 23.1/13)。

    但是#ifdef 太多了,我无法弄清楚它到底在做什么。

    所以我想答案是升级到 MSVC 2012,或者至少破解标头,这样我就知道还有哪些地方需要小心。

    【讨论】:

    • 请注意,由于我不明白 MSVC 2012 是如何修复该错误的,因此我还没有认为该问题已解决。
    猜你喜欢
    • 2021-06-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-15
    • 1970-01-01
    • 2013-10-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多