ChatGPT解决这个技术问题 Extra ChatGPT

vector::at 与 vector::operator[]

我知道 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::atvector::operator[] 有什么优势?
我什么时候应该使用 vector::at 而不是 vector::size + vector::operator[] ?

+1 非常好的问题!!但我不认为 at() 是常用的。
请注意,在您的示例代码 if (i < v.size()) v[i] = 2; 中,有一个可能的代码路径根本没有将 2 分配给 v 的任何元素。如果这是正确的行为,那就太好了。但是,当 i >= v.size() 时,此函数通常无法执行任何操作。因此,它不应该使用异常来指示意外情况并没有什么特别的原因。许多函数只使用 operator[] 而不检查大小,记录 i 必须在范围内,并将生成的 UB 归咎于调用者。
使用 at 更安全。例如,给定一个包含 100 个元素的 obj 向量。 obj.at(143) = 69; 立即爆炸。然而,obj[143] = 69; 会在您不通知的情况下悄悄潜入。

p
pmdj

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


+1 我喜欢关于如何分开处理错误用户输入的解释(输入验证;可能预期无效输入,因此不被视为异常)......以及代码中的错误(取消引用超出范围的迭代器是异常的事物)
所以你说当索引取决于用户输入时我应该使用 size() + [],在索引永远不会越界的情况下使用 assert 以便将来轻松修复错误,在所有其他情况下使用 .at() (以防万一,导致可能发生错误......)
@LihO:如果您的实现提供了 vector 的调试实现,那么最好将其用作“以防万一”选项,而不是到处使用 at()。这样,您就可以希望在发布模式下获得更高的性能,以防万一您需要它。
是的,现在大多数 STL 实现都支持调试模式,它甚至可以检查 operator[],例如 gcc.gnu.org/onlinedocs/libstdc++/manual/…,所以如果您的平台支持这一点,您可能最好使用它!
@pmdj 奇妙的点,我不知道...但是孤立的链接。 :P 当前一个是:gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html
A
Alexandre C.

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

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

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

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


如果我没有捕获 out_of_range 异常,则调用 abort()
@LihO:不一定.. try..catch 可以出现在调用此方法的方法中。
如果没有别的,at 很有用,否则您会发现自己在编写类似 if (i < v.size()) { v[i] = 2; } else { throw what_are_you_doing_you_muppet(); } 的内容。人们通常会以“诅咒,我必须处理异常”来考虑抛出异常的函数,但只要你仔细记录你的每个函数可以抛出什么,它们也可以用作“太好了,我不必须检查条件并抛出异常”。
@SteveJessop:我不喜欢为程序错误抛出异常,因为其他程序员可以在上游捕获它们。断言在这里更有用。
@AlexandreC。好吧,官方对此的回应是 out_of_range 派生自 logic_error,其他程序员“应该”知道最好不要在上游捕获 logic_error 并忽略它们。如果您的同事不想知道他们的错误,assert 也可以忽略,因为他们必须使用 NDEBUG 编译您的代码;-) 每种机制都有其优点和缺陷。
T
Tony Delroy

与 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

在这种情况下,很难检查您是否不恰当地到达了输入的末尾,因为这非常依赖于遇到的确切标记。在每个使用点进行显式检查是很痛苦的,并且由于前/后增量、使用点的偏移量、关于某些早期测试的持续有效性的有缺陷的推理等开始出现,程序员错误的空间要大得多。


B
Brangdon

如果您有指向向量的指针,at 会更清楚:

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

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


...尤其是当您需要指向向量的第 n 个元素的指针时。
恕我直言,这还不足以成为偏爱 at() 的理由。只需编写:auto& vector = *pVector;,现在您可以执行 return vector[n]。此外,您真的应该避免直接使用指针(而不是引用),尤其是对于复杂的类。
@einpoklum 我的代码没有通过我们大学的自动评分器定时测试,因为 at()...简单地用 operator[] 替换所有调用使代码运行得足够快以通过所有测试。 at()operator[] 具有非常明显的性能差异。
e
einpoklum

在调试版本中,不能保证 at()operator[] 慢;我希望它们的速度大致相同。不同之处在于 at() 准确地指定了在出现边界错误(异常)时会发生什么,而在 operator[] 的情况下,它是未定义的行为——在我使用的所有系统(g++ 和VC++),至少在使用正常调试标志时。 (另一个区别是,一旦我确定我的代码,我可以通过关闭调试来显着提高 operator[] 的速度。如果性能需要它 - 除非有必要,否则我不会这样做。)

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


@phresnel operator[] 不需要进行边界检查,但所有好的实现都可以。至少在调试模式下。唯一的区别是如果索引超出范围,它们会做什么:operator[] 中止并显示错误消息,at() 引发异常。
@phresnel 我提供的大部分代码都处于“调试”模式。您仅在性能问题实际需要检查时才关闭检查。 (Microsoft pre-2010 在这里有点问题,因为如果检查选项与运行时的选项不对应,std::string 并不总是有效:-MD,您最好关闭检查,{ 3},你最好戴上它。)
我更像是说“标准认可(保证)的代码”的阵营;当然你可以在调试模式下自由交付,但是在进行跨平台开发时(包括但不限于相同操作系统,但编译器版本不同的情况),依赖标准是发布的最佳选择,而调试模式被认为是程序员获得正确和强大的东西的工具:)
@phresnel 显然,您只依靠标准。但是如果某些平台确实保证了未定义行为的崩溃,那么不利用它是愚蠢的(除非分析器告诉你不能)。您永远无法 100% 确定您的代码中没有错误,而且至少在某些特定情况下,在某些特定平台上,您会崩溃,而不是破坏所有客户端数据,这让人放心。
另一方面,如果您的应用程序的关键部分被隔离并受异常安全 (RAII ftw) 等保护,那么对 operator[] 的每一次访问都应该被削弱吗?例如,std::vector<color> surface(witdh*height); ...; for (int y=0; y!=height; ++y)...。我认为对交付的二进制文件执行边界检查属于过早悲观。恕我直言,它应该只是对设计不佳的代码的创可贴。
l
ltjax

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

在这种特定情况下,用户输入确实是一个很好的例子。假设您想从语义上分析一个 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() 的迭代器。


a
ahj

根据 this 文章,抛开性能不谈,使用 atoperator[] 没有任何区别,只要保证访问在向量的大小范围内。否则,如果访问仅基于向量的容量,则使用 at 更安全。


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

注意: 似乎有些新人在没有礼貌地告诉问题的情况下对这个答案投了反对票。以下答案是正确的,可以验证here

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

这使 at 成为一种较慢的方法,但不使用 at 也是非常糟糕的建议。您必须查看绝对数字,而不是相对数字。我可以肯定地打赌,您的大部分代码都在执行比 at 更昂贵的操作。就个人而言,我尝试使用 at,因为我不希望一个讨厌的错误创建未定义的行为并潜入生产环境。


C++ 中的异常是一种错误处理机制,而不是调试工具。 Herb Sutter 解释了为什么抛出 std::out_of_range 或任何形式的 std::logic_error 实际上本身就是一个逻辑错误 here
@BigTemp - 我不确定你的评论与这个问题和答案有什么关系。是的,例外是备受争议的话题,但这里的问题是 at[] 之间的区别,我的回答只是说明了区别。当性能不是问题时,我个人使用“安全”方法。正如 Knuth 所说,不要过早优化。此外,无论哲学上的差异如何,在生产中尽早发现错误是件好事。
我还认为最好使用 at,只要它不在代码中对性能非常敏感的部分。立即抛出异常要好得多,而不是程序继续处理虚假数据,这可能会导致比不明显的性能差异更严重的问题。