vector :: at与vector :: operator []


95

我知道这at()[]它的边界检查要慢,因为边界检查也有类似的问题,例如C ++ Vector at / []运算符速度:: std :: vector :: at()vs :: [] <<令人惊讶的结果!慢/快5到10倍!。我只是不明白该at()方法有什么用。

如果我有一个像这样的简单向量:std::vector<int> v(10);并且我决定通过使用at()而不是[]在有索引的情况下使用in 来访问其元素,i并且不确定其是否在向量范围内,它将迫使我用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 []


11
+1很好的问题!!但我不认为at()是常用的。
罗希特·维平·马修斯

10
请注意,在示例代码中if (i < v.size()) v[i] = 2;,有一个可能的代码路径根本没有分配2给的任何元素v。如果那是正确的行为,那就太好了。但是,当when时,此功能通常没有任何意义i >= v.size()。因此,没有特殊的原因为什么它不应该使用异常来指示意外情况。许多功能只是在使用时operator[]不检查大小,i必须在范围内的文档,并且将生成的UB归咎于调用者。
史蒂夫·杰索普

Answers:


74

我想说vector::at()引发异常并不是真正被周围的代码捕获的。它们主要用于捕获代码中的错误。如果您需要在运行时进行边界检查(例如,索引来自用户输入),则最好使用一条if语句。因此,总而言之,设计您的代码时vector::at()要永远不会引发异常,这样,如果发生异常,并且程序中止,则表明存在错误。(就像一个assert()


1
+1我喜欢如何分开处理错误的用户输入(输入验证;可能会认为无效的输入,因此不应该认为是例外)的解释...以及代码中的错误(取消引用超出范围的迭代器是例外)东西)
Bojan Komazec'2

因此,您说当索引取决于用户输入时,我应该使用size()+ []assert在索引永远都不会超出范围的情况下使用,以便将来和.at()其他所有情况下都可以轻松地修复错误(以防万一,可能会引起错误。) 。)
LihO 2012年

8
@LihO:如果您的实现提供了的调试实现,vector则最好将其用作“以防万一”选项,而不是at()在各处使用。这样,您可以希望在发布模式下获得更多性能,以防万一您需要它。
史蒂夫·杰索普

3
是的,如今,大多数STL实现都支持甚至对边界进行检查的调试模式operator[],例如gcc.gnu.org/onlinedocs/libstdc ++ / manual / ...,因此,如果您的平台支持此功能,则最好采用它!
pmdj 2012年

1
@pmdj奇妙的点,我不知道...但是孤立的链接。:P当前的一项是:gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html
underscore_d

16

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

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

我同意大多数对向量的访问都是程序员的错误(在这种情况下,您应该使用它assert来更轻松地定位这些错误;大多数标准库的调试版本都会自动为您执行此操作)。您不想使用可以被上游吞没的异常来报告程序员错误:您希望能够修复该错误

由于对向量的越界访问不太可能成为正常程序流程的一部分(在这种情况下,您是对的:事前检查size而不是让异常冒出来),我同意您的诊断:at本质上是没有用的。


如果我没有捕获到out_of_range异常,则abort()称为。
LihO '02

@LihO:不一定..the try..catch可以存在于调用此方法的方法中。
Naveen

12
如果没有其他要求,那么at在一定程度上很有用,否则您会发现自己编写类似的内容if (i < v.size()) { v[i] = 2; } else { throw what_are_you_doing_you_muppet(); }。人们经常以“诅咒,我必须处理异常”的方式来考虑抛出异常的函数,但是只要您仔细记录每个函数可以抛出的异常,它们也可以用作“伟大的,我不会必须检查条件并引发异常”。
史蒂夫·杰索普

@SteveJessop:我不喜欢抛出程序错误的异常,因为其他程序员可能会将它们捕获到上游。断言在这里更有用。
Alexandre C.

6
@AlexandreC。好吧,对此的官方回应是out_of_range从派生的logic_error,其他程序员“应该”比向logic_errors上游并忽略它们更了解。assert如果您的同事不希望知道自己的错误,也可以忽略不计,这会更加困难,因为他们必须使用NDEBUG;-) 编译代码,每种机制都有其优点和缺点。
史蒂夫·杰索普

11

与vector :: operator []相比,使用vector :: at有什么优点?什么时候应该使用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

在这种情况下,很难检查您是否不合适地到达了输入的末尾,因为这很大程度上取决于遇到的确切标记。在每个使用点进行显式检查是很痛苦的,随着前/后增量,使用点的偏移量,对某些较早测试的持续有效性的错误推理等等,程序员有更多的错误余地。


10

at 如果您有指向向量的指针,则可以更清楚:

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

除了性能,第一个是更简单明了的代码。


...特别是在您需要指向向量的第n个元素的指针时。
海豚2014年

4

首先,是否at()还是operator[]较慢没有规定。当没有边界错误时,我希望它们的速度差不多,至少在调试版本中是如此。区别在于,它at()指定了发生界限错误(异常)的确切情况,在这种情况下operator[],就像的情况一样,它是未定义的行为-至少在以下情况下,我使用的所有系统(g ++和VC ++)都崩溃了使用正常的调试标志。(另一个区别是,一旦确定了我的代码,可以operator[] 通过关闭调试来大幅提高速度。如果性能要求,除非有必要,否则我不会这样做。)

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


4
operator[]不被迫进行边界检查,而为什么要进行边界检查时,为什么还要期望它们具有相同的速度at()呢?您是在暗示缓存,推测和分支缓冲区问题吗?
塞巴斯蒂安·马赫2012年

@phresnel operator[]不需要进行边界检查,但是所有好的实现都可以。至少在调试模式下。唯一的区别是,如果索引超出范围,它们将执行以下操作:operator[]中止并显示错误消息,并at()引发异常。
詹姆斯·坎泽

2
抱歉,错过了您的“处于调试模式”属性。但是,我不会在调试模式下衡量代码的质量。在发布模式下,仅需要进行检查at()
塞巴斯蒂安·马赫2012年

1
@phresnel我提供的大多数代码都处于“调试”模式。您仅在性能问题真正需要时才关闭检查。(Microsoft 2010年前的版本在这里有点问题,因为std::string如果检查选项与运行时的检查选项不对应,则并非总是有效:-MD并且您最好关闭检查-MDd,并且最好)
詹姆斯·坎泽

2
我更倾向于说“标准认可(保证)代码”;当然,您可以自由地以调试模式进行交付,但是在进行跨平台开发时(包括但不限于相同的操作系统,但编译器版本不同),依靠标准是发布和调试模式的最佳选择被认为是程序员用来使该事情基本上正确和健壮的工具:)
Sebastian Mach 2012年

1

使用异常的全部要点是您的错误处理代码可能会更遥远。

在这种特定情况下,用户输入确实是一个很好的例子。假设您想从语义上分析XML数据结构,该结构使用索引来引用您内部存储在中的某种资源std::vector。现在XML树是一棵树,因此您可能想使用递归进行分析。在递归中,XML文件的编写者可能会发生访问冲突。在那种情况下,您通常希望突破所有递归级别,而只拒绝整个文件(或任何类型的“粗化”结构)。这是at派上用场的地方。如果文件有效,则只需编写分析代码即可。库代码将负责错误检测,您可以在粗糙级别上捕获错误。

同样,其他容器(如std::map)也具有std::map::atstd::map::operator[]:at 稍有不同的语义,可以在const映射上使用,而operator[]不能。现在,如果你想写容器无关代码,喜欢的事,可以处理任何const std::vector<T>&或者const std::map<std::size_t, T>&ContainerType::at将是你的首选武器。

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


1

根据文章,性能之外,它没有任何区别使用at或者operator[],只有当访问是保证是向量的大小之内。否则,如果访问仅基于向量的容量,则使用会更安全at


1
外面有龙。如果单击该链接会怎样?(提示:我已经知道了,但是在StackOverflow上,我们希望注释不会受到链接腐烂的影响,即提供您要说的内容的简短摘要)
Sebastian Mach 2012年

谢谢你的提示。现在已修复。
ahj 2012年

0

注意:似乎有些新人对这个答案不满意,而不必礼貌地告诉自己出了什么问题。以下答案是正确的,可以在此处验证。

实际上只有一个区别:at边界检查是否有,而边界检查operator[]没有。这不仅适用于调试版本,也适用于发行版本,并且由标准非常明确地指定。就这么简单。

这会使at方法变慢,但不建议使用它也是一个不好的建议at。您必须查看绝对数字,而不是相对数字。我可以肯定地说,您的大多数代码执行的操作都比花费更多at。就我个人而言,我尝试使用它at是因为我不想让讨厌的bug产生未定义的行为并潜入生产环境。


1
C ++中的异常是一种错误处理机制,而不是调试工具。香草萨特解释了为什么投掷std::out_of_range或任何形式的std::logic_error本身并,事实上,一个逻辑错误在这里
大温度

@BigTemp-我不确定您的评论与该问题和答案如何相关。是的,例外是一个备受争议的话题,但这里的问题是和之间的区别at[]而我的回答只是说明了区别。当性能不是问题时,我个人使用“安全”方法。正如Knuth所说的,不要做过早的优化。而且,无论是在哲学上还是在差异上,尽早发现bug都是很好的。
Shital Shah
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.