【问题标题】:How defensive should you be? [duplicate]你应该如何防守? [复制]
【发布时间】:2009-12-16 18:06:31
【问题描述】:

可能重复:
Defensive programming

今天早上我们就防御性编程主题进行了一场精彩的讨论。我们进行了代码审查,其中传入了一个指针,但没有检查它是否有效。

有些人认为只需要检查空指针。我质疑它是否可以在更高级别进行检查,而不是通过它传递的每个方法,并且如果另一端的对象不满足某些要求,则检查 null 是一种非常有限的检查。

我理解并同意检查 null 总比没有检查好,但在我看来,只检查 null 会提供一种错误的安全感,因为它的范围有限。如果要确保指针可用,请检查是否有多个 null。

您在这个主题上有什么经验?对于传递给从属方法的参数,您如何在代码中编写防御措施?

【问题讨论】:

  • 这种问题总是让我想起海森堡先生 (en.wikipedia.org/wiki/Werner_Heisenberg)。
  • 防御性编程只是单元测试不足或不正确的症状,像 Erlang 这样的语言实际上通常不鼓励防御性编程,因为它往往会掩盖更大的问题,我倾向于同意这一点。跨度>
  • @jldupont:你越“防御”,你的代码就越臃肿?
  • 如果你想防御,你首先不应该使用原始指针。对于 byref 参数,一般来说,永远不能不指向任何东西的指针,使用T&。对于可以为 null(或可选 byrefs)的指针,请使用 boost::optional<T&>

标签: c++ defensive-programming


【解决方案1】:

在 Code Complete 2 的错误处理章节中,我被介绍了路障的概念。本质上,路障是严格验证所有输入的代码。路障内的代码可以假设任何无效输入都已被处理,并且接收到的输入是好的。在路障内,代码只需要担心路障内其他代码传递给它的无效数据。断言条件和明智的单元测试可以增加你对封闭代码的信心。通过这种方式,您在路障中非常防御性地编程,但在路障内则不那么如此。另一种思考方式是,在路障中,您始终正确处理错误,而在路障内,您只需在调试构建中断言条件。

就使用原始指针而言,通常您能做的最好的事情就是断言指针不为空。如果您知道该内存中应该包含什么,那么您可以确保内容以某种方式保持一致。这就引出了一个问题,即为什么该内存没有被包裹在一个可以验证其自身一致性的对象中。

那么,为什么在这种情况下使用原始指针?使用引用或智能指针会更好吗?指针是否包含数字数据,如果是,将其包装在一个管理该指针生命周期的对象中会更好吗?

回答这些问题可以帮助您找到一种更具防御性的方法,因为您最终会得到一个更容易防御的设计。

【讨论】:

  • 不要忘记,你应该对你无法控制的事情采取极端的防御措施,即“我预计我会耗尽内存吗?”或“我会用完线程吗?”除此之外,+1。
  • 我同意@wheaties,但我想补充一点,如果您正在开发 API,“在您的控制之外”也可能意味着客户端代码。通过 API 公开的任何东西都应该有大量的防御性代码(可能只在调试版本中,以提高性能)。
  • 智能指针非常棒……但它们无法防止空指针错误。您可以像拥有一个空值原始指针一样轻松地拥有一个空值智能指针。实际上更简单,因为原始指针可能未初始化而不是 null。
【解决方案2】:

最好的防御方法不是在运行时检查指针是否为空,而是避免使用开头可能为空的指针

如果传入的对象不能为null,使用引用!或者按值传递!或者使用某种智能指针。

进行防御性编程的最佳方法是在编译时捕获错误。 如果一个对象为 null 或指向垃圾被认为是错误的,那么你应该让那些东西编译错误。

最终,您无法知道指针是否指向有效对象。因此,与其检查 一个 特定的极端情况(这远不如真正危险的情况,指向无效对象的指针那么常见),而是通过使用保证有效性的数据类型来消除错误。

我想不出另一种主流语言可以让您在编译时捕获与 C++ 一样多的错误。使用该功能。

【讨论】:

  • Haskell/Eiffel 在这方面还是不错的,不知道能不能算主流:x
  • 是的,我在写 Haskell 的时候也想到了它(虽然我不知道 Eiffel),但是是的,我并没有真正认为它是主流。 (从某种意义上说,遇到一个用 C++ 编写的应用程序是很常见的,但我会惊讶地看到一个用 Haskell 编写的“在野外”使用的程序。它们存在,毫无疑问,它们是只是不太常见。
  • @jalf:我对你的帖子很感兴趣。然而,对于那些不了解 C++ 基础知识的人(比如我)来说,很难看出这将如何实现。你能解释一下如何让编译器捕获一个空对象吗?
  • @conciliator:在 C++ 中,空对象是不可能。如果你定义了一个结构Foo,并声明了一个变量Foo bar,那么bar保证包含一个Foo类型的对象。它不能做其他任何事情。 Foo* 类型的变量可以存储指向Foo 对象的指针,也可以为空。但是上面的bar 无论如何都不能为空。定义变量时,会调用Foo 构造函数,如果失败,则抛出异常,因此我们离开bar 可见的范围。所以我们有一个不变量,如果bar 是可见的,那么它就是一个有效的对象
  • 这就是基本的想法。在 C# 或 Java 中,一切都是引用,因此(几乎)一切都可以为空。在 C++ 中,只有指针类型可以为 null,并且只有在请求时才能得到指针。
【解决方案3】:

没有办法检查指针是否有效。

【讨论】:

  • 根据您的情况,您很可能能够“检查指针是否有效”。如果您创建了对象,则可以将其注册到“已知良好指针”表中。然后,您可以在使用前检查指针是否在表中。如果你没有创建它,那么你的答案是正确的。
【解决方案4】:

说真的,这取决于你想给你造成多少错误。

检查空指针绝对是我认为必要但不充分的事情。您可以使用许多其他可靠的原则,从代码的入口点(例如,输入验证 = 该指针是否指向有用的东西)和出口点(例如,您认为指针指向的东西有用但它碰巧导致你的代码抛出异常)。

简而言之,如果您假设每个调用您的代码的人都会竭尽全力毁掉您的生活,那么您可能会发现很多最坏的罪魁祸首。

为清楚起见进行编辑:其他一些答案正在谈论单元测试。我坚信测试代码有时比它正在测试的代码更有价值(取决于谁在衡量价值)。也就是说,我也认为单元测试也是必要的,但对于防御性编码来说还不够。

具体示例:考虑一种记录在案的第 3 方搜索方法以返回与您的请求匹配的值的集合。不幸的是,该方法的文档中没有明确说明的是,原始开发人员认为如果没有符合您的请求,则返回 null 而不是空集合会更好。

因此,现在,您将防御性和经过良好单元测试的方法称为思考(遗憾的是缺少内部空指针检查),并且繁荣! NullPointerException,如果没有内部检查,您将无法处理:

defensiveMethod(thirdPartySearch("Nothing matches me")); 
// You just passed a null to your own code.

【讨论】:

    【解决方案5】:

    我是“让它崩溃”设计学院的忠实粉丝。 (免责声明:我不从事医疗设备、航空电子设备或核电相关软件的工作。)如果你的程序崩溃了,你启动调试器并找出原因。相反,如果您的程序在检测到非法参数后继续运行,那么到它崩溃时,您可能根本不知道出了什么问题。

    好的代码由许多小函数/方法组成,而在每一个 sn-ps 代码中添加十几行参数检查会使其更难阅读和维护。保持简单。

    【讨论】:

    • 我实际上不同意,当您的代码崩溃时,可能需要一段时间才能诊断出哪个方法实际引入了错误状态。尤其是当它在有人真正尝试使用它之前被传递了很多......我更喜欢调试构建的快速失败原则,这需要测试......但是,我倾向于测试 MINIMAL 以了解我打算做什么。
    【解决方案6】:

    我可能有点极端,但我不喜欢防御性编程,我认为是懒惰引入了原理。

    对于这个特定的例子,断言指针不为空是没有意义的。如果你想要一个空指针,没有比使用引用更好的方法来实际执行它(并同时清楚地记录它)。而且它的文档实际上将由编译器强制执行,并且在运行时不会花费 ziltch!

    一般来说,我倾向于不直接使用“原始”类型。让我们举例说明:

    void myFunction(std::string const& foo, std::string const& bar);
    

    foobar 的可能值是多少?好吧,这几乎仅限于 std::string 可能包含的内容......这是非常模糊的。

    另一方面:

    void myFunction(Foo const& foo, Bar const& bar);
    

    好多了!

    • 如果人们错误地颠倒了参数的顺序,编译器会检测到
    • 每个类单独负责检查值是否正确,用户没有负担。

    我倾向于使用强类型。如果我有一个只应由字母字符组成且最多 12 个字符的条目,我宁愿创建一个包含 std::string 的小类,并在内部使用一个简单的 validate 方法来检查分配并通过那个班代替。这样我就知道,如果我测试验证例程 ONCE,我实际上不必担心该值可以到达我的所有路径 > 当它到达我时它将被验证。

    当然,我不认为代码不应该被测试。只是我更喜欢强封装,输入的验证在我看来是知识封装的一部分。

    因为没有任何规则可以毫无例外地出现……暴露的接口必然会被验证代码臃肿,因为你永远不知道会发生什么。但是,对于 BOM 中的自我验证对象,它通常是非常透明的。

    【讨论】:

      【解决方案7】:

      “单元测试验证代码做了它应该做的事情” > “生产代码试图验证它没有做它不应该做的事情”。

      我什至不会自己检查 null,除非它是已发布 API 的一部分。

      【讨论】:

        【解决方案8】:

        这在很大程度上取决于;有问题的方法是否曾被您组外部的代码调用过,还是内部方法?

        对于内部方法,您可以进行足够的测试以使其成为一个有争议的问题,如果您正在构建目标是尽可能提高性能的代码,您可能不想花时间检查您非常确定的输入是对的。

        对于外部可见的方法——如果有的话——你应该总是仔细检查你的输入。总是。

        【讨论】:

          【解决方案9】:

          从调试的角度来看,最重要的是您的代码是快速失败的。代码越早失败,越容易找到失败点。

          【讨论】:

            【解决方案10】:

            对于内部方法,我们通常坚持使用断言进行此类检查。这确实会在单元测试中发现错误(你有很好的测试覆盖率,对吗?),或者至少在使用断言运行的集成测试中。

            【讨论】:

              【解决方案11】:

              检查空指针只是故事的一半, 您还应该为每个未分配的指针分配一个空值
              最负责任的 API 也会这样做。
              检查空指针在 CPU 周期中的成本非常低,一旦交付应用程序崩溃可能会使您和您的公司损失金钱和声誉。

              如果代码在您完全控制的私有接口中,您可以跳过空指针检查和/或您通过运行单元测试或一些调试构建测试(例如断言)来检查空指针

              【讨论】:

              • 如果空指针输入是由于其他地方的程序员错误造成的,那么找到它并不能防止应用程序崩溃,并且会损失金钱和声誉。如果调用者可以向您传递您的 API 不允许的输入,那么他们可以忽略错误返回值并最终崩溃。它所做的只是在错误发生时更快速、更轻松地诊断错误(缓解)。
              • 不言而喻,发现问题之后必须采取一些纠正措施,我指出,随着发布日期的临近,发现(和纠正)错误变得更加昂贵,它可能会-一旦你经过它就会火箭。
              【解决方案12】:

              我想在这个问题中解决一些问题:

              1. 编码指南应指定您直接处理引用或值,而不是使用指针。根据定义,指针是仅在内存中保存地址的值类型——指针的有效性是特定于平台的,并且意味着很多东西(可寻址内存的范围、平台等)
              2. 如果您发现自己因任何原因需要指针(例如动态生成和多态对象),请考虑使用智能指针。智能指针通过“普通”指针的语义为您提供了许多优势。
              3. 例如,如果一个类型具有“无效”状态,那么该类型本身应该提供这种情况。更具体地说,您可以实现 NullObject 模式,该模式指定“错误定义”或“未初始化”对象的行为方式(可能通过抛出异常或提供无操作成员函数)。

              您可以创建一个执行 NullObject 默认值的智能指针,如下所示:

              template <class Type, class NullTypeDefault>
              struct possibly_null_ptr {
                possibly_null_ptr() : p(new NullTypeDefault) {}
                possibly_null_ptr(Type* p_) : p(p_) {}
                Type * operator->() { return p.get(); }
                ~possibly_null_ptr() {}
                private:
                  shared_ptr<Type> p;
                  friend template<class T, class N> Type & operator*(possibly_null_ptr<T,N>&);
              };
              
              template <class Type, class NullTypeDefault>
              Type & operator*(possibly_null_ptr<Type,NullTypeDefault> & p) {
                return *p.p;
              }
              

              如果您支持可能指向具有默认派生“空行为”的类型的空指针,请使用possibly_null_ptr&lt;&gt; 模板。这在设计中明确表明“空对象”存在可接受的行为,这使得您的防御实践记录在代码中 - 并且比一般准则或实践更具体。

              【讨论】:

              • 这实质上就是 Pavel 在问题的 cmets 中提到的 boost::optional&lt;T&gt;
              • 但是optional&lt;T&gt; 并不比指针“更好”工作,因为您仍然需要检查它是否已设置(就像普通指针一样)。我建议将“Null Object”行为作为设计的一部分,并像使用普通指针一样使用possibly_null_ptr&lt;&gt;,仅具有默认的“null”特性。
              【解决方案13】:

              只有当你需要对指针做一些事情时才应该使用指针。比如指针算术横向一些数据结构。然后如果可能的话,应该封装在一个类中。

              如果将指针传递给函数以对其指向的对象执行某些操作,则改为传递引用。

              防御性编程的一种方法是尽可能断言所有内容。在项目开始时它很烦人,但后来它是单元测试的一个很好的辅助。

              【讨论】:

                【解决方案14】:

                许多答案解决了如何在代码中编写防御的问题,但没有太多关于“你应该如何防御?”的说法。这是您必须根据软件组件的关键性来评估的东西。

                我们正在开发飞行软件,软件错误的影响范围从轻微的烦恼到飞机/机组人员的损失。我们根据影响编码标准、测试等的潜在不利影响对不同的软件进行分类。您需要评估您的软件将如何使用以及错误的影响,并设置您想要(并且可以负担)的防御级别。 DO-178B standard 将此称为“设计保证级别”。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-02-25
                  • 1970-01-01
                  • 2016-10-29
                  • 2016-11-14
                  • 2020-06-08
                  • 1970-01-01
                  • 2012-04-11
                  相关资源
                  最近更新 更多