在循环中擦除向量时如何避免超出范围异常?

How to avoid out of range exception when erasing vector in a loop?

对于冗长的解释,我深表歉意。

我正在开发一个 C++ 应用程序,它将两个文件加载到两个二维字符串向量中,重新排列这些向量,构建另一个二维字符串向量,并将其全部输出到报告中。两个向量的第一个元素是一个代码,用于标识项目的所有者和向量中的项目。我在启动时将所有者的标识传递给程序,并在嵌套的 while 循环中遍历两个向量以找到具有匹配的第一个元素的向量。当我这样做时,我用前两个的分量构建第三个向量,然后我需要捕获任何不匹配的向量。

我使用语法 "vector.erase(vector.begin() + i)" 在两个原始数组匹配时从中删除元素。循环完成后,我有了新的第三个向量,剩下两个只有元素的向量,它们不匹配,而这正是我需要的。这工作正常,因为我尝试了文件中的各个所有者(该程序一次接受一个所有者)。然后我尝试了一个产生了超出范围的错误。

我无法弄清楚如何在不抛出错误的情况下在循环内执行擦除(交换和弹出或擦除-删除似乎不是可行的解决方案)。在这个程序中构建我的第三个向量后,我用两个额外的嵌套 while 循环解决了我的程序问题。

我想知道如何使擦除方法在这里起作用(因为它似乎是一个更简单的解决方案)或至少如​​何检查我的超出范围错误(并避免它)。这个特定所有者有很多 "rows";所以调试很乏味。在放弃并继续使用嵌套 while 解决方案之前,我确定第二次擦除引发了错误。我怎样才能完成这项工作,或者我在事后的嵌套时间是我能做的最好的吗?这是代码:

i = 0;
while (i < AIvector.size())
{
CHECK:
    j = 0;
    while (j < TRvector.size())
    {
        if (AIvector[i][0] == TRvector[j][0])
        {
            linevector.clear();
            // Add the necessary data from both vectors to Combo_outputvector 
            for (x = 0; x < AIvector[i].size(); x++)
            {
                linevector.push_back(AIvector[i][x]);  // add AI info
            }
            for (x = 3; x < TRvector[j].size(); x++) // Don't need the the first three elements; so start with x=3.
            {
                linevector.push_back(TRvector[j][x]); // add TR info 
            }
            Combo_outputvector.push_back(linevector); // build the combo vector

            // then erase these two current rows/elements from their respective vectors, this revises the AI and TR vectors 
            AIvector.erase(AIvector.begin() + i);
            TRvector.erase(TRvector.begin() + j);
            goto CHECK;  // jump from here because the erase will have changed the two increments
        }
        j++;
    }
    i++;
}

如前所述,您的 goto 跳到了错误的位置。只需将其移出第一个 while 循环即可解决您的问题。但是我们可以做得更好吗?

对于移动成本低的对象,可以使用 std::removestd::erase 干净地从向量中擦除,vectorstring 都是。然而,经过一番思考,我认为这不是您的最佳解决方案,因为您需要的功能不仅仅是检查两个容器中是否存在特定行,而且用擦除-删除习惯用法不容易表达。

保留当前结构,然后,我们可以使用迭代器作为循环条件。我们从中受益匪浅,因为 std::vector::erase return 是指向被擦除元素之后的下一个有效元素的迭代器。更不用说它无论如何都需要一个迭代器。有条件地擦除向量中的元素变得像

一样简单
auto it = vec.begin()
while (it != vec.end()) {
    if (...) 
        it = vec.erase(it);
    else
        ++it;
}

因为我们将 erase 的 return 值分配给 it,所以我们不必担心迭代器失效。如果我们删除最后一个元素,它 returns vec.end() 所以不需要特殊处理。

您的第二个循环可以完全删除。 C++ 标准定义了在 STL 容器内搜索的函数。 std::find_if 在容器中搜索满足条件的值,return 是它的迭代器,如果不存在则 end()。您还没有在任何地方声明您的类型,所以我假设这些行是 std::vector<std::string>>.

using row_t = std::vector<std::string>;
auto AI_it = AIVector.begin();
while (AI_it != AIVector.end()) {
    // Find a row in TRVector with the same first element as *AI_it
    auto TR_it = std::find_if (TRVector.begin(), TRVector.end(), [&AI_it](const row_t& row) {
        return row[0] == (*AI_it)[0];
    });

    // If a matching row was found
    if (TR_it != TRVector.end()) {
        // Copy the line from AIVector
        auto linevector = *AI_it;

        // Do NOT do this if you don't guarantee size > 3
        assert(TR_it->size() >= 3);
        std::copy(TR_it->begin() + 3, TR_it->end(),
            std::back_inserter(linevector));

        Combo_outputvector.emplace_back(std::move(linevector));

        AI_it = AIVector.erase(AI_it);
        TRVector.erase(TR_it);
    }
    else
        ++AI_it;
}

如您所见,切换到迭代器完全回避了您最初的问题,即弄清楚如何不访问无效索引。如果您不理解 find_if 参数的语法,请搜索术语 lambda。如果这个答案解释它们是什么,那超出了范围。

一些显着的变化:

  • linevector 现在已正确封装。没有理由在这个范围之外声明并重用。

  • linevector 只是从 AIVector 复制所需的行而不是 push_back 其中的每个元素,只要 Combo_outputvector (因此 linevector) 包含与 AIVectorTRVector.

  • 相同的类型
  • std::copy 用于代替 for 循环。除了稍短之外,它也更通用,这意味着您可以将容器类型更改为任何支持随机访问迭代器并在后面插入的类型,并且副本仍然有效。

  • linevector 移入 Combo_outputvector。如果您的矢量很大,这可能是一个巨大的性能优化!

您可能使用了非封装的 linevector,因为您希望在循环之外保留最后插入的行的副本。但是,这将禁止移动它。出于这个原因,按照我上面显示的那样进行操作会更快且更具描述性,然后在循环后简单地执行以下操作。

auto linevector = Combo_outputvector.back();