【问题标题】:Real world use-case for the `at()` indexing function in the C++ std library?C++ std 库中“at()”索引函数的真实用例?
【发布时间】:2011-04-13 07:19:24
【问题描述】:

C++的容器vector,deque,...除了operator[index]之外还提供at(index)访问器函数来访问容器元素。

The difference between this member function and member operator function operator[] is that deque::at signals if the requested position is out of range by throwing an out_of_range exception.

我的代码中从来没有,也从来不需要这个函数,因为在我的 C++ 代码中访问可能超出范围的元素是没有意义的。始终编​​写代码以访问正确的索引(或在索引无法匹配的情况下产生有意义的错误/异常。)

我会对在生产代码中使用at() 的真实示例(可能来自一些开源项目,因为这会添加一些上下文)感兴趣。

也许有人可以举例说明使用at() 有意义的算法问题。

注意:我最近在一些单元测试代码中使用了它,在这些代码中添加索引检查代码被认为不值得麻烦,并且考虑了at() 抛出的 out_of_range 异常足够的信息+上下文以防测试中断。

注意:关于this answer by ildjarn - 我确实想就此展开讨论或评论战。我对“积极”的发现很感兴趣,即 具体的例子已经被使用过。谢谢。

【问题讨论】:

    标签: c++ collections standard-library


    【解决方案1】:

    好吧,当您不控制正在使用的索引时(例如,如果它是由您的代码的客户端传入的),您应该手动检查它是否在范围内,或者使用at 获取报告的异常(您可以捕获并使用自己的错误报告通知调用者,或者简单地向上传播标准异常)。

    换句话说,检查输入参数是被调用函数的责任,但它是使用if 语句显式执行此操作,还是使用at 而不是[] 隐式执行此操作,还有待商榷。如果我要做的只是抛出一个out_of_range 异常(如果传入的索引大于或等于集合的大小),我想我会让at 这样做并拯救自己一些编码。

    静默传回不良数据几乎从来都不是最佳解决方案。简单地将 x[7] 传回四元素整数甲板的问题在于调用者认为它是一个有效的零。 不是这种情况。

    【讨论】:

    • 我认为基于异常处理的输入验证是个坏主意。始终明确检查。
    • 我不同意这一点,@musiKk。验证是验证,无论是在您的代码中检查还是从较低级别抛出。我不会忽略文件操作的异常,然后在每次操作后手动检查状态标志 - 捕获 ios_base::failure 并将错误处理隔离到 catch 位要容易得多。
    • @paxdiablo:感谢您的一般情况,但请注意,您的回答fails 无法提供您所见过的实际用法example自己使用或使用过。你有没有使用过at()自己做输入验证?
    • @ildjarn:“抛出一个可能未处理的异常” - 因为调用者的正确性何时成为被调用者的关注点?如果被调用者被记录为抛出,调用者需要处理它。
    • @ildjarn:我专门回应了我引用的那一点。这是一个误导性的评论,因为它暗示被调用者在某种程度上不得不考虑调用者中的错误。
    【解决方案2】:

    在我看来,at() 是一个100% 没用的 成员函数。仅在标准库容器的有效范围内访问是使用该容器的前提条件,违反任何前提条件应使用assert 处理,而不是通过抛出一个例外。 at() 的存在绝不有助于容器维护其先决条件/不变量,实际上只会通过使适当的边界检查访问看起来不是成为先决条件来混淆问题。

    即,为最终可能由程序员错误引起的事情抛出异常是非常愚蠢的。有关更详细的解释,请参阅此线程,特别是 D. Abrahams 的帖子;虽然可能很长,但绝对值得一读:comp.lang.c++.moderated: Exceptions

    编辑:为了澄清对 OP 的补充说明,我是说根据我的 C++ 经验——专业、开源和其他方面——我从未遇到过使用标准容器的at(),并维护它实际上没有在生产代码中使用。进一步的 cmets 或详细说明只是为了合理化我认为是这种情况的原因。

    【讨论】:

    • assert 在这里是个坏主意,因为它通常在生产代码中不活跃,而且除此之外,您希望您的代码在生产环境中严重退出.如果你愿意,一定要明确地处理它(这不是必需的,但我理解你这样做的观点)但是如果你的用户因为你的断言而丢失数据,他们就会把你钉在十字架上。
    • 我的意思是你不知道进入你的函数的数据的最终来源。这可能是程序员出错了,可能是一个错误或有许多其他原因应该被程序员抓住。还有许多程序员无法控制的其他来源(用户、文件等)。但是,使用您的代码的程序员的无能并不能免除您使自己的代码防弹的责任。无论是通过检查还是异常来完成,我都不在乎。但我不认为断言是这里的答案。
    • 我将在此处留下讨论,因为它会引起问题。我的最后一点很简单,你永远不应该假设你的调用者正在传递有效的数据。时期。我现在就闭嘴:-)
    • @paxdiablo : 我们当然可以达成一致,如果不是如何处理无效数据的话。 ;-D
    • @ildjarn:我同意这令人困惑,尽管只是非常轻微,C++ 程序员需要足够聪明才能记住这些奇怪的东西。然后,在由于没有入界索引而无法访问向量的情况下,是否选择抛出异常是编码风格的问题。写if ((i >= 0) and (i < vec.size()) return vec[i]; else throw out_of_rage(); 并不比写return vec.at(i); 在道德上更好、更易于维护、更简单。所以 如果 无论如何你都想要一个例外,at 很有用。如果你不这样做,那就不是。
    【解决方案3】:

    我一直发现at() 有用的一个用例是促进复杂用户输入的解析。例如,在分析 C++ 代码时,我发现自己在检查语法结构时沿着一系列词汇标记移动。逻辑通常类似于“如果这个标记是一个标识符,而下一个是一个 Equals,那么它应该是一个赋值,所以提前扫描一个分号标记以建立表达式的标记范围”。在这样的代码中使用at() 意味着您可以轻松地在与当前点的某个偏移处表达期望,ala:

    if (tokens.at(i) == Switch)
    {
        if (tokens.at(++i) != Left_Parentheses)
            // throw or return to say input program's broken...
        if (tokens.at(++i) == ...)
            ...
    }
    

    每当您尝试解析无效程序时,都会出现异常。增加位置发生在整个代码中的许多地方,以至于不断地重新验证大小将是一场噩梦(冗长且极易出错),因为在这种情况下,您只会意识到程序需要多大才能有效,因为应用语法规则。与功能等效的替代方案相比,在此处使用 at() 简洁、健壮、直观且性能合理。

    FWIW - 快速搜索我们的生产代码(200k 行,大部分是在我加入团队之前编写的)发现了 at() 的十几个用法。

    【讨论】:

      【解决方案4】:

      我的情况宁愿是:为什么不使用它

      除非您处于应用程序的性能关键部分,否则您应该始终支持std::out_of_range 而不是未定义的行为,至少这是我的信条。

      在实践中,我通常将我正在处理的所有代码转换为使用 checked 访问。对于大多数代码来说,性能损失是不可见的,至少我有一个很好的报告,其中包含有关当前执行上下文的信息(在根级别的 catch(std::exception const&) 中生成)而不是导致我的代码的内存损坏几次后失败(或者更糟,看起来它起作用了)。

      我同意首先验证输入,我同意您应该事先检查您的访问权限...但以防万一您忘记或遇到错误,最好有一个at()

      使用[] 而不是at() 就像携带一把上膛的枪,没有/(分别)在口袋里装上安全装置。你可以忘记戴上它,但心甘情愿地取下它?这太疯狂了。

      【讨论】:

      • @Matthieu:太好了,你是我在我的一个 cmets 中描述的假设的 C++ 程序员 :-) 就是说,考虑到 C++ 在其余时间里的表现,我认为可以进行更好的类比12 把上膛的枪放在你的口袋里,at() 在其中一把上安了保险。其他人没有安全措施。我怀疑这就是为什么大多数 C++ 程序员不太担心 1 的原因,他们认为如果他们做出任何突然的动作,老实说,他们是否被脚中弹 11 次或 12 次并不重要——他们会避免突然动作。
      • 如果您正在使用该库的任何良好实现,则定义了 未定义的行为。中止程序,这是您想要的。如果先决条件失败,您(通常)想要中止,而不执行析构函数。
      • @James Kanze:是也不是。如果出现未定义的行为,我想要中止,因为状态未知。如果先决条件失败,则取决于执行环境:在测试中,中止并给我一个不错的核心转储;在生产中,(温和地)警告用户出现问题并尽可能多地记录。不回应用户是您可以提供的最令人沮丧的体验之一。
      • @Matthieu :您在哪个平台上,异常(例如 std::out_of_range)给出“带有回溯的好报告”?如果异常被捕获,并且我认为它极有可能被捕获,因为它是一个 std::exception 派生对象,你通常得到零回溯信息,如果没有捕获到异常,你的程序被终止,就像在无效索引访问的情况下很可能一样。
      • @James: at 是根据具体情况使用的。例如,您可以决定在一个紧密的循环中放弃它。编译器选项影响整个翻译单元。而且我肯定有兴趣知道您在谈论哪个编译器选项,我不知道存在这种情况(只知道检查过的 STL,但它太慢了,无法激活生产......并且有二进制兼容性问题)
      【解决方案5】:

      经过快速搜索,我发现 Inkscape(svg 编辑器)、Google v8、Android、Chromium 和 Ogre 等都使用了此功能。这个(基本)列表取自一个简单的google search,使用正则表达式at\([0-9]+\)

      使用\.at\([0-9a-z_]+\)代替之前的表达式gives more generic results,并添加OpenJdk和丰富的sourceforge项目。

      【讨论】:

      • @Martin:谢谢 =)(我已经删除了剩余的 cmets)
      【解决方案6】:

      v.at(-1) 不会像 v[-1] 那样失败(你会得到一个例外)

      【讨论】:

        【解决方案7】:

        我同意这里很多人的观点,at 大多没用;但是,当使用指向容器(或列表)的指针时,它可能看起来会更好:

        std::vector<std::vector<int> >::iterator i;
        for (i = v2.begin(); i != v2.end(); ++i)
        {
            int x1 = (*i)[3]; // UGLY
            int x2 = i->at(3); // Looks OK
        }
        

        我认为使用at 时这段代码看起来更好。

        【讨论】:

          【解决方案8】:

          Stroustrup 建议在所有位置使用at,除非您确定索引将在有效范围内。

          他建议像下面这样的代码可以使用[] 操作符。

          for (int i = 0; i < v.size(); ++i)
          {
              // use v[i] - we are sure it will be a valid index
          }
          

          在其他情况下,使用at

          【讨论】:

            猜你喜欢
            • 2011-01-12
            • 1970-01-01
            • 2012-10-18
            • 2019-08-11
            • 2011-01-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多