push_back和insert之间的std :: vector不一致崩溃(end(),x)

时间:2012-07-25 15:22:05

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

将此代码放入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()都通过引用获取新项目, 当向量被重新分配以获得更多空间时,新项目在插入之前就会失效。

海湾合作委员会也应该有这个问题。我没有检查过Clang,但这取决于它使用的是哪个STD库。

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

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

我担心的是我不知道我可能做了什么代码,比如vec.push_back(vec [1]);或vec.insert(it,vec [2]); 我必须查看使用push_back和insert的数百行代码,而这只是我自己的代码......第三方库也可能受到影响。

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

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

MSVC2010的额外push_back()代码是非标准的吗?当MSVC找到以这种方式使用的向量时,它应该检测并断言吗? (即安全计算倡议)

我正在考虑攻击MSVC2010和GCC的标题来检测这些情况。

还有其他想法吗?

谢谢, 保罗

PS:请注意,如果您可以保证不需要调整矢量大小,这种用法非常好(并且效率很高)

4 个答案:

答案 0 :(得分:5)

好的,我在virtualbox上安装了Win8 + MSVC2012来试用它。 Geez Windows 8使用鼠标很烦人,没有任何按钮可以推动悬停,这对于窗口中的屏幕来说很难。

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

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

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

请注意,由于push_back(&amp;&amp;)中的额外代码(黑客?)而没有及早发现问题 - 请参阅底部与MSVC2012相关的进一步评论。

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

在MSVC 2012中,通过正确选择insert(it,const T&amp; 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()用于强制&amp;&amp;移动要选择的方法。 在这两种情况下,代码都应该导致灾难,并希望崩溃。

但是,在MSVC 2012中,push_back()循环工作正常,因为push_back(&amp;&amp;)中有一些额外的代码可以检测_Val是否与向量位于同一地址空间中,如果是这样,副本而不是移动。 但是,如果新项目不是严格地在同一个内存空间但仍然是原始向量的一部分(例如pimpl指针)怎么办?我可以想象让push_back(&amp;&amp;)死掉的方法。

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

insert()循环没有这个hack,这也意味着错误地使用std :: move()只会导致腐败。就个人而言,我更喜欢快速失败而不是失败 - 当你在向客户展示时。

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

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

  2. 升级到MSVC 2012 RC。我将不得不等到它成为黄金,然后它将按预期工作(其他部分有新的和令人兴奋的错误)。

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

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

  5. 进一步更新 我修好了标题。结果很简单......

    MSVC2010的插入(&amp;&amp;)声明如下所示:

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

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

    iterator insert(const_iterator _Where, _Ty&& _Val)
    

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

答案 1 :(得分:2)

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

Per 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]);

答案 2 :(得分:1)

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

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

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

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

但是有太多的#ifdef让我弄清楚它到底在做什么。

所以我想答案是升级到MSVC 2012,或者至少破解标题,以便我知道我还需要小心。

答案 3 :(得分:1)

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

从标准的一部分来看,对别名没有限制,因此代码是合规的,不应该有未定义的行为。