繁体   English   中英

vector::at 与 vector::operator[]

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

我知道at()[]慢,因为它的边界检查,这也在类似的问题中讨论过,比如C++ Vector at/[] operator speed::std::vector::at() vs operator[] < <令人惊讶的结果!! 慢/快 5 到 10 倍! . 我只是不明白at()方法有什么用。

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

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

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

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

所以我的问题是:
使用vector::at比使用vector::operator[] 有什么优势?
我什么时候应该使用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::operator[] ?

这里的重点是异常允许将正常的代码流与错误处理逻辑分离,并且单个 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[]慢; 我希望它们的速度大致相同。 不同之处在于at()准确地指定了边界错误(异常)时会发生什么,而在operator[]的情况下,它是未定义的行为——我使用的所有系统(g++ 和VC++),至少在使用正常调试标志时。 (另一个区别是,一旦我确定我的代码,我就可以通过关闭调试来显着提高operator[]的速度。如果性能需要它 - 除非有必要,否则我不会这样做。)

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

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

实际上只有一个区别: at边界检查而operator[]没有。 这适用于调试版本和发布版本,并且这在标准中有很好的规定。 就这么简单。

这使得at方法变慢,但不使用at也是非常糟糕的建议。 你必须看绝对数字,而不是相对数字。 我可以肯定地打赌,您的大部分代码都在执行比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更安全。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM