vector::at vs. vector::operator[]



我知道at()[]慢是因为它的边界检查,这在类似的问题中也有讨论,比如C++Vector at/[]运算符速度或:std::Vector::at()vs operator[]<lt;令人惊讶的结果!!速度慢5到10倍/更快!。我只是不明白at()方法有什么好处。

如果我有一个像这样的简单向量:std::vector<int> v(10);,并且我决定在索引i的情况下使用at()而不是[]来访问它的元素,并且我不确定它的向量内边界是否存在,它会迫使我用try-catch块包装它

try
{
    v.at(i) = 2;
}
catch (std::out_of_range& oor)
{
    ...
}

尽管我可以通过使用size()并自己检查索引来获得相同的行为,但这对我来说似乎更容易也更方便:

if (i < v.size())
    v[i] = 2;

所以我的问题是:
与vector::operator[]相比,使用vector::at有哪些优势
我什么时候应该使用vector::at而不是vector::size+vector::operator[]

我想说vector::at()抛出的异常并不是真正想要被周围的代码捕获的。它们主要用于捕捉代码中的错误。如果您需要在运行时进行边界检查,因为例如索引来自用户输入,那么最好使用if语句。总之,设计代码的目的是vector::at()永远不会抛出异常,所以如果抛出异常,并且程序中止,这就是错误的迹象。(就像assert()

它迫使我用try-catch块包裹它

不,它没有(try/catch块可以在上游)。当您希望抛出异常而不是程序进入未定义的行为领域时,它非常有用。

我同意大多数对向量的越界访问都是程序员的错误(在这种情况下,您应该使用assert来更容易地定位这些错误;大多数标准库的调试版本都会自动为您这样做)。您不希望使用可以在上游吞噬的异常来报告程序员错误:您希望能够修复错误

由于对向量的越界访问不太可能是正常程序流的一部分(在这种情况下,你是对的:事先用size进行检查,而不是让异常出现),我同意你的诊断:at基本上是无用的。

at如果有指向向量的指针,则会更清晰:

return pVector->at(n);
return (*pVector)[n];
return pVector->operator[](n);

撇开性能不谈,第一个是更简单、更清晰的代码。

使用vector::at相对于vector:∶operator[]有什么优点?我什么时候应该使用vector::at而不是vector::size+vector:运算符[]?

这里的重要一点是,异常允许将正常的代码流与错误处理逻辑分离,并且单个catch块可以处理从无数抛出站点中的任何一个生成的问题,即使分散在函数调用中。因此,这并不是说at()对于一次使用来说一定更容易,而是当你有很多索引需要验证时,有时它会变得更容易,也不会混淆正常的案例逻辑。

同样值得注意的是,在某些类型的代码中,索引以复杂的方式递增,并不断用于查找数组。在这种情况下,使用at()确保正确的检查要容易得多。

作为一个真实世界的例子,我有一些代码将C++标记为词法元素,然后其他代码将索引移动到标记向量上。根据遇到的情况,我可能希望增加并检查下一个元素,如:

if (token.at(i) == Token::Keyword_Enum)
{
    ASSERT_EQ(tokens.at(++i), Token::Idn);
    if (tokens.at(++i) == Left_Brace)
        ...
    or whatever

在这种情况下,很难检查是否不适当地到达了输入的末尾,因为这在很大程度上取决于遇到的确切令牌。每个使用点的显式检查都是痛苦的,而且随着前/后增量、使用点的偏移、关于某些早期测试的持续有效性的有缺陷的推理等的出现,程序员错误的空间要大得多。

在调试构建中,不能保证at()operator[]慢;我希望他们的速度差不多。不同之处在于CCD_ 21准确地指定了在存在边界错误(异常)时将发生什么,其中,与operator[]的情况一样,它是未定义的行为——至少在使用正常调试标志时,我使用的所有系统(g++和VC++)都会崩溃。(另一个区别是,一旦我确定了我的代码,我就可以通过关闭调试来大幅提高operator[]的速度。如果性能需要,除非必要,否则我不会这么做。)

在实践中,at()很少是合适的如果上下文让你知道索引可能无效,你可能想要显式测试(例如返回默认值或其他什么),如果你知道它不可能无效,那么你想中止(如果你不知道它是否无效,我建议你更准确地指定函数的接口)。然而,也有一些例外,其中无效索引可能是由于解析用户数据而导致的,并且该错误应该会导致整个请求中止(但不会导致服务器停机);在这种情况下,一个例外是合适的,at()就可以了这是给你的。

使用异常的全部意义在于,您的错误处理代码可能离您更远。

在这种特定情况下,用户输入确实是一个很好的例子。想象一下,您想要对一个XML数据结构进行语义分析,该数据结构使用索引来引用您内部存储在std::vector中的某种资源。现在XML树是一棵树,所以你可能想使用递归来分析它。在递归的深处,XML文件的编写者可能会违反访问权限。在这种情况下,您通常希望跳出所有级别的递归,只拒绝整个文件(或任何类型的"粗糙"结构)。这就是at派上用场的地方。您可以像文件有效一样编写分析代码。库代码将负责错误检测,您可以在粗略级别上捕获错误。

此外,其他容器,如std::map,也有std::map::at,其语义与std::map::operator[]略有不同:at可以用于const映射,而operator[]不能。现在,如果您想编写与容器无关的代码,比如可以处理const std::vector<T>&const std::map<std::size_t, T>&的代码,那么ContainerType::at将是您的首选武器。

然而,所有这些情况通常出现在处理某种未经验证的数据输入时。如果您确信您的有效范围(通常应该如此),您通常可以使用operator[],但更好的是,使用begin()end()的迭代器。

根据本文,除了性能之外,使用atoperator[]没有任何区别,只有在保证访问在向量大小内的情况下。否则,如果访问仅基于矢量的容量,则使用at更安全。

注意:似乎有些新人在没有礼貌地说出错误的情况下否决了这个答案。下面的答案是正确的,可以在这里验证。

实际上只有一个区别:at进行边界检查,而operator[]不进行边界检查。这适用于调试构建和发布构建,标准对此进行了很好的规定。就这么简单。

这使得at是一个较慢的方法,但也非常不建议不要使用at。你必须看绝对数,而不是相对数。我可以放心地打赌,您的大多数代码都在执行比at更昂贵的操作。就我个人而言,我尝试使用at是因为我不希望一个讨厌的bug创建未定义的行为并潜入生产。

最新更新