21

将此代码放入 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:另请注意,如果您可以保证向量不需要调整大小,则此用法非常好(且有效)

4

4 回答 4

5

好的,我在virtualbox上安装了Win8 + MSVC2012来试一试。Geez 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 库等)可能会在其数百万行代码中的某个地方使用它。我使用的所有 3rd 方库,我都会重新编译,所以无论我的代码受到什么影响,它们也会受到影响。

  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& 混淆。

于 2012-07-26T06:33:21.217 回答
2

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

Per如何将重复元素插入向量中?标准中没有任何语言禁止插入对现有元素的引用。指代迭代器和引用失效的语言只能被解读为指代操作完成后的行为(在没有其他指示的情况下)。

请注意,根据重叠向量::插入的行为,指定迭代器参数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]);
于 2012-07-25T15:39:22.937 回答
1

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

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

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

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

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

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

于 2012-07-25T15:49:49.420 回答
1

查看 4.4 的实现,push_backinsert他们需要增加缓冲区调用_M_insert_aux来增加缓冲区时,首先复制新元素(这意味着别名不是问题,因为此时原始对象还没有被触及)和然后是所有先前存在的元素。所以执行很好。

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

于 2012-07-25T16:27:26.843 回答