你应该多么防守?

时间:2009-12-16 18:06:31

标签: c++ defensive-programming

  

可能重复:
  Defensive programming

我们今天早上就防御性节目的主题进行了很好的讨论。我们进行了代码审查,其中传入指针并且未检查它是否有效。

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

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

您对此主题有何经验?如何在代码中为传递给从属方法的参数编写防御措施?

14 个答案:

答案 0 :(得分:16)

在Code Complete 2中,在关于错误处理的章节中,我了解了路障的概念。本质上,路障是严格验证进入其中的所有输入的代码。街垒内的代码可以假设已经处理了任何无效输入,并且接收的输入是好的。在街垒内,代码只需要担心街垒内其他代码传递给它的无效数据。断言条件和明智的单元测试可以增加您对阻塞代码的信心。通过这种方式,你可以在街垒上进行非常防御性的编程,但在街垒内部却不那么严重。考虑它的另一种方法是在路障中,你总是正确地处理错误,而在路障中你只是在调试版本中断言条件。

就使用原始指针而言,通常你能做的最好就是断言指针不是null。如果您知道该内存中应该是什么,那么您可以确保内容在某种程度上是一致的。这就引出了一个问题,即为什么内存不会被包裹在一个可以验证它本身的一致性的对象中。

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

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

答案 1 :(得分:13)

防御的最佳方法是不要在运行时检查null的指针,而是避免使用可能为null的指针

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

进行防御性编程的最佳方法是在编译时捕获错误。 如果将对象视为空的错误或指向垃圾,则应该使这些事情编译错误。

最终,您无法知道指针是否指向有效对象。因此,不是检查一个特定的角落情况(这比真正危险的情况要少得多,指向无效对象的指针),使用保证有效性的数据类型使错误无法实现。

我想不出另一种主流语言,它允许你在编译时捕获与C ++一样多的错误。使用这种能力。

答案 2 :(得分:4)

无法检查指针是否有效。

答案 3 :(得分:3)

严肃地说,这取决于你想要给你带来多少虫子。

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

简而言之,如果你假设每个打电话给你的代码的人都会尽力毁掉你的生活,你可能会发现很多最糟糕的罪魁祸首。

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

具体示例:考虑记录的第三方搜索方法,以返回与您的请求匹配的值集合。不幸的是,该方法的文档中不清楚的是原始开发人员决定如果没有匹配您的请求,最好返回null而不是空集合。

所以现在,你称之为防御性和经过单元测试的方法思考(遗憾的是缺少内部空指针检查)和繁荣! NullPointerException,如果没有内部检查,你无法处理:

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

答案 4 :(得分:3)

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

好的代码由许多小函数/方法组成,并且为每个代码片段添加了十几行参数检查,这使得它更难以阅读并且难以维护。保持简单。

答案 5 :(得分:3)

我可能有点极端,但我不喜欢防守编程,我认为引入原则的是懒惰。

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

一般来说,我倾向于不直接使用'raw'类型。让我们来说明一下:

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中使用自验证对象时,它通常非常透明。

答案 6 :(得分:2)

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

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

答案 7 :(得分:1)

这很大程度上取决于;是由您的组外部的代码调用的方法,还是内部方法?

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

对于外部可见方法 - 如果您有任何方法 - 您应该始终仔细检查您的输入。总是

答案 8 :(得分:1)

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

答案 9 :(得分:0)

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

答案 10 :(得分:0)

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

如果代码位于您完全控制的私有接口中,并且/或者通过运行单元测试或某些调试构建测试(例如断言)检查null,则可以跳过空指针检查

答案 11 :(得分:0)

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

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

    然后在支持可能为具有默认派生“null behavior”的类型的空指针的情况下使用possibly_null_ptr<>模板。这使得在设计中明确表示“空对象”存在可接受的行为,这使得您的防御性实践记录在代码中 - 并且比一般指南或实践更具体。

答案 12 :(得分:0)

只有在需要对指针执行某些操作时才应使用指针。如指针算术横向某些数据结构。然后,如果可能的话,应该封装在一个类中。

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

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

答案 13 :(得分:0)

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

我们正在做飞行软件,软件错误的影响范围从轻微的烦恼到飞机/机组人员的损失。我们根据潜在的不利影响对不同的软件进行分类,这些影响会影响编码标准,测试等。您需要评估软件的使用方式和错误的影响,并设置您想要(并且可以承受的)防御级别。 DO-178B standard称之为“设计保证级别”。