在什么情况下应该使用 vector::at 而不是 vector::operator[]?

152
我知道at()[]慢,因为它需要进行边界检查,这也在类似的问题中讨论过,比如C++ Vector at/[] operator speed::std::vector::at() vs operator[] << surprising results!! 5 to 10 times slower/faster!。我只是不明白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::at相比vector::operator[]有哪些优势?
什么时候应该使用vector::at而不是vector::size + vector::operator[]


15
非常好的问题!但我认为不太常用 at() 函数。 - Rohit Vipin Mathews
11
请注意,您示例代码中的 if (i < v.size()) v[i] = 2; 存在一种可能的代码路径,它根本不会将 2 分配给 v 的任何元素。如果这是正确的行为,那很好。但通常情况下,当 i >= v.size() 时,此函数无法执行任何有意义的操作。因此,使用异常来指示意外情况并没有任何特定的原因。许多函数仅使用 operator[] 而没有检查大小,并记录必须在范围内的 i ,并将由此导致的未定义行为归咎于调用方。 - Steve Jessop
3
使用 at 更加安全。例如,给定一个包含 100 个元素的 obj 向量。obj.at(143) = 69; 会立即引发错误。然而,obj[143] = 69; 将悄无声息地偷偷摸摸地改变数值,你可能察觉不到。 - daparic
我来这里是想听听为什么在Qt文档中提到的at()比operator更快,因为它永远不会导致深拷贝的发生。那么这个说法是特指Qt及其隐式数据共享吗? - undefined
8个回答

109
我觉得 vector::at() 抛出的异常并不是为了被周围的代码捕获。它们主要用于捕获代码中的错误。如果你需要在运行时进行边界检查,比如索引来自用户输入,那么最好使用一个 if 语句。因此,总体来说,请设计你的代码,以便 vector::at() 永远不会抛出异常。这样,如果它确实抛出异常,并且你的程序中止了,那么这就是一个 bug 的标志。(就像 assert() 一样)

1
+1 我喜欢如何区分处理用户错误输入(输入验证;无效输入可能是预期的,因此不被视为异常情况)和代码中的 bug(解引用超出范围的迭代器是异常情况)的解释。 - Bojan Komazec
那么你的意思是当索引依赖于用户输入时,应该使用 size() + [];在索引永远不会超出边界的情况下,使用 assert 进行易于未来调试的错误修复;在所有其他情况下使用 .at()(以防万一,因为可能会发生错误...)。 - LihO
8
如果您的实现提供了一个调试实现的 vector,那么最好使用它作为“以防万一”的选项,而不是在每个地方都使用 at()。这样,您可以希望在发布模式下获得更好的性能,以防你有需要的时候。 - Steve Jessop
4
是的,现在大多数STL实现都支持调试模式,即使对于operator[]也进行边界检查,例如http://gcc.gnu.org/onlinedocs/libstdc++/manual/bk01pt03ch17s03.html#debug_mode.using.mode,因此如果您的平台支持此功能,最好使用它! - pmdj
1
@pmdj 的观点很棒,我之前不知道...但链接已经失效了。:P 当前链接是:https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html - underscore_d
我知道这有点晚了,但是我想补充一下operator[]at()之间的性能差异非常明显! 我曾经因为使用 at() 而导致代码未能通过我们大学自动评分系统的定时测试... 简单地将所有调用替换为operator[]使得代码运行足够快以通过所有测试。 - Marcel Ferrari

19
它会强制我使用try-catch块进行包装。 不是的(try/catch块可以在上游)。当您想要抛出异常而不是让程序进入未定义行为领域时,它很有用。
我同意,大多数对向量的越界访问是程序员的错误(在这种情况下,您应该使用assert来更轻松地定位这些错误;大多数标准库的debug版本会自动执行此操作)。您不希望使用可以在上游被忽略的异常来报告程序员的错误:您希望能够修复错误。
由于向量的越界访问不太可能是正常的程序流程的一部分(如果是这种情况,您是正确的:在异常冒泡之前,请使用size进行事先检查),因此我同意您的诊断:at实际上是无用的。

如果我没有捕获out_of_range异常,那么将会调用abort() - LihO
@LihO:不一定..try..catch可以存在于调用此方法的方法中。 - Naveen
12
即使没有别的好处,at也很有用,因为否则你会写出像这样的语句:if (i < v.size()) { v[i] = 2; } else { throw what_are_you_doing_you_muppet(); }。人们通常把抛出异常的函数看作是"该死,我必须处理异常",但只要你仔细记录每个函数可能抛出的异常,它们也可以被用作"太棒了,我不需要检查条件并抛出异常"。 - Steve Jessop
@SteveJessop:我不喜欢为程序错误抛出异常,因为其他程序员可以在上游捕获它们。在这里,断言更加有用。 - Alexandre C.
7
官方回应是out_of_range继承自logic_error,其他程序员“应该”知道不要在上游捕获logic_error并忽略它们。如果你的同事很想不知道他们的错误,也可以忽略assert,只是更难一些,因为他们必须使用NDEBUG编译你的代码;每种机制都有其优点和缺点。 - Steve Jessop

15

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

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

除了性能之外,第一个选项的代码更简单、更清晰。


尤其是当你需要指向向量的第n个元素时,情况会变得更加复杂。 - dolphin
1
在我看来,这不是足够好的理由去偏爱 at()。只需编写:auto& vector = *pVector;,现在您可以执行 return vector[n]。此外,您应该尽量避免直接通过指针(而不是引用)操作复杂类。 - einpoklum
@einpoklum 我们大学的自动评分器在计时测试中因为 at() 而无法通过某些代码... 只需将所有调用替换为 operator[],代码就可以快速运行以通过所有测试。at()operator[] 之间有一个非常明显的性能差异。 - Marcel Ferrari

11

使用 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
在这种情况下,很难检查您是否已经不适当地到达了输入的末尾,因为这非常依赖于遇到的确切标记。在每个使用点明确检查是很痛苦的,而且由于进行前/后增量、使用点的偏移量、有缺陷的推理等等,程序员出错的空间更大。

8
在调试版本中,无法保证at()operator[]慢;我认为它们的速度应该差不多。不同之处在于,at()指定了如果发生边界错误(即异常)会发生什么,而在operator[]的情况下,行为未定义——在我使用的所有系统(g++和VC++)中都会崩溃,至少在使用常规调试标志时。(另一个区别是,一旦我确定我的代码是正确的,通过关闭调试,可以获得大幅提高operator[]性能的速度。如果性能需要-除非必要,否则我不会这样做。) 实际上,at()很少适用。如果上下文环境使您知道索引可能无效,则可能需要显式测试(例如返回默认值或其他内容),如果您知道它不能无效,则需要中止(如果您不知道它是否无效,我建议您更加精确地指定函数接口)。然而,有几个例外情况,其中无效索引可能源自解析用户数据,并且错误应该导致整个请求中止(但不会使服务器崩溃),在这种情况下,异常是合适的,at()将为您完成。

1
@phresnel operator[] 不需要进行边界检查,但是所有良好的实现都会这样做。至少在调试模式下是如此。唯一的区别在于如果索引超出范围时它们的处理方式:operator[] 会中止并显示错误消息,而 at() 则会抛出异常。 - James Kanze
1
@phresnel 我交付的大部分代码都是在“调试”模式下完成的。只有当性能问题实际需要时,才会关闭检查。 (Microsoft 2010年之前在这方面存在一些问题,因为如果检查选项与运行时不对应,则 std :: string 不总是起作用:-MD,您最好关闭检查 -MDd,您最好打开它。) - James Kanze
3
我更倾向于“按照标准规范编写代码”这一派别;当然,你可以在调试模式下交付程序,但在跨平台开发(包括但不限于相同操作系统不同编译器版本的情况)时,依赖标准是发布的最佳选择,而调试模式被视为程序员获得正确和健壮代码的工具。 - Sebastian Mach
@phresnel 显然,你只能依赖标准。但是,如果某个平台确保未定义行为会导致崩溃,那么不利用它就是愚蠢的(除非分析器告诉你不能这样做)。你永远无法100%确定代码中没有错误,而且至少在某些特定情况下,在某些特定平台上,你会崩溃,而不是破坏所有客户数据,这是令人放心的。 - James Kanze
如果存在性能问题,那么请务必关闭检查。然而,对于大多数应用程序来说并非如此。 - James Kanze
1
另一方面,如果您的应用程序的关键部分被隔离并受到保护,例如异常安全(RAII ftw),那么每个对operator[]的访问都应该被削弱吗?例如,std::vector<color> surface(witdh*height); ...; for (int y=0; y!=height; ++y)...。我认为在交付的二进制文件上强制执行边界检查属于过早的悲观主义。在我看来,它只应该是不良设计代码的临时措施。 - Sebastian Mach

1
使用异常的整个目的在于您的错误处理代码可以更远离问题点。
在这种特定情况下,用户输入确实是一个很好的例子。想象一下,您想要语义分析一个使用索引引用某种资源的XML数据结构,该资源在std::vector中进行内部存储。现在XML树是一棵树,所以您可能想要使用递归来分析它。在递归的深处,可能会有XML文件编写者发生访问冲突。在这种情况下,通常希望退出所有递归级别并拒绝整个文件(或任何“粗略”结构)。这就是异常出现的地方。您只需将分析代码编写为文件有效即可。库代码将负责错误检测,您只需在粗略级别上捕获错误即可。
此外,其他容器(如std::map)也有std::map::at,其语义与std::map::operator[]略有不同:在const map上可以使用at,而operator[]则不能。现在,如果您想编写容器不可知的代码,例如可以处理const std::vector<T>&const std::map<std::size_t, T>&,那么ContainerType::at将是您的选择。

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


1
根据this文章,除了性能之外,使用atoperator[]没有任何区别,只有在访问保证在向量大小内时才有效。否则,如果访问仅基于向量的容量,则更安全地使用at

3
外面有龙。如果我们点击那个链接会发生什么?(提示:我已经知道了,但在StackOverflow上,我们更喜欢不容易失效的评论,即提供关于您要表达的内容的简短摘要) - Sebastian Mach
谢谢你的提示。现在已经修复了。 - ahj

-1

注意:似乎有一些新手在不告知错误的情况下对此答案进行了负评。下面的答案是正确的,可以在这里进行验证。

实际上只有一个区别:at会进行边界检查,而operator[]则不会。这适用于调试版本和发布版本,并且标准已经非常明确地规定了这一点。就是这么简单。

这使得at成为一种更慢的方法,但不使用at也是非常糟糕的建议。你必须看绝对数字,而不是相对数字。我敢肯定,你的大部分代码都在执行比at更昂贵的操作。个人而言,我尽量使用at,因为我不想让一个恶心的bug导致未定义的行为并潜入到生产环境中。


2
C++中的异常机制旨在作为错误处理机制,而不是调试工具。Herb Sutter解释了为什么抛出std::out_of_range或任何形式的std::logic_error本身就是一种逻辑错误在这里 - Big Temp
1
@BigTemp - 我不确定你的评论与这个问题和答案有什么关系。是的,异常是一个备受争议的话题,但这里的问题是at[]之间的区别,我的回答只是简单地阐述了这一点。当性能不是问题时,我个人使用“安全”方法。正如Knuth所说,不要过早地进行优化。此外,无论哲学上的差异如何,及早发现错误总是好事。 - Shital Shah
我也认为,在代码的非常注重性能的部分之外,最好使用at。与程序继续使用虚假数据相比,立即抛出异常要好得多,因为后者可能会导致比不可感知的性能差异更严重的问题。 - Zitrax

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接