【问题标题】:MISRA demand a single point of exit for a function for a "lookup table" functionMISRA 要求“查找表”函数的单点退出
【发布时间】:2021-12-14 01:44:58
【问题描述】:

Misra 标准要求函数的单点退出,但我有以下“转换”代码

typedef enum { CASE_A, CASE_B, CASE_C } my_enum_t;

int my_conv_funct(my_enum_t value)
{
    switch(value)
    {
         case CASE_A:
             return 0;
         case CASE_B:
             return 1;
         case CASE_C:
             return 2;
         default:
             break;
    }
    log_error("ERROR!!!!!");
    assert(1==0);
}

这有效吗? 我需要将其转换为单个返回函数吗? 处理默认情况的最佳方式是什么?

这在理论上创建了一个无法访问的代码(错误是警告如果在枚举中添加一个值而不是添加相应的情况)

这是一个嵌入式系统,顺便说一句,这些断言会产生问题?

谢谢, 尼克

编辑:

如果没有错误,则不应调用默认情况(例如,程序员在枚举中添加另一个值并且不添加相应的情况

另一种选择是完全删除默认设置,但这违反了另一个 misra 规则

typedef enum { CASE_A, CASE_B, CASE_C } my_enum_t;

int my_conv_funct(my_enum_t value)
{
    switch(value)
    {
         case CASE_A:
             return 0;
         case CASE_B:
             return 1;
         case CASE_C:
             return 2;
    }
    //should never reach this line
    assert(1==0);
}

如果我编译并且没有指定枚举中的所有案例(我认为),这将生成一个警告

【问题讨论】:

  • 你创建了一个“int retVal;”最后返回。
  • 如果您在嵌入式系统上,您希望断言语句会发生什么?也许您希望 MCU 重新启动?那么你就可以这样做了。
  • 代码格式一般不正确。因为assert 是一个宏,可能会被预处理掉,从而导致函数的代码路径不返回有效值。最好定义一个错误返回码并将其用于默认路径(除了断言之外)。
  • 这是很棒的代码。但请记住:发明 misra 是为了限制/惩罚不合标准的程序员。
  • 如果您在嵌入式系统上,您需要有处理断言的代码。您可能想要做的事情是将相关调试信息存储在不会在重新启动时重新初始化的内存部分中,然后强制重新启动。一些有用的指导在这里barrgroup.com/Embedded-Systems/How-To/…。没有这种特殊代码,你的assert会做什么?

标签: c switch-statement default assert misra


【解决方案1】:

很简单:

int my_conv_funct(my_enum_t value)
{
    int result = -1;
    switch(value)
    {
         case CASE_A:
             result = 0;
             break;
         case CASE_B:
             result = 1;
             break;
         case CASE_C:
             result = 2;
             break;
         default:
             break;
    }
    if(result == -1)
    {
         log_error("ERROR!!!!!");
         assert(1==0);
    }
    return result;
}

【讨论】:

  • 为什么不将log_error("ERROR!!!!!"); 放在switch 语句的default 子句中?
  • 嗨,好吧,不,我可能没有解释自己。我的问题是我不想返回像 -1 这样的值来检查,如果程序员在枚举中添加一个新值并且没有在switch中添加对应的case。三个(或更多)选项是唯一可能的选项,因此该函数永远不应到达日志错误行。这个函数是内部调用的,没有用户输入,所以它不能返回除定义的案例之外的任何东西。
  • @NicolaLunghi 无法检测到它的编译时间。我认为您尝试解决 X-Y 问题。
【解决方案2】:

这有效吗?

它不符合您描述的 MISRA 规则。

我需要将其转换为单个返回函数吗?

为了遵守 MISRA 规则,是的。

处理默认情况的最佳方法是什么?

我们无法判断什么是适合您的特定情况和用途的“最佳”。

这是一个嵌入式系统,顺便说一句,这些断言会产生问题?

断言的想法是,它可以帮助您在开发过程中发现编程错误,但(原则上)它会通过旨在用于生产的代码中的构建选项被禁用。如果遵循该模型,则断言本身可能不会产生问题,但函数在默认情况下(如果禁用断言)不返回值的事实会产生问题。如果程序必须在执行默认情况时终止,那么它应该调用abort(),或其他具有该效果的函数。否则,它应该在默认情况下返回一个合理的值。

我可能会这样写函数:

int my_conv_funct(my_enum_t value)
{
    switch(value)
    {
         case CASE_A:
         case CASE_B:
         case CASE_C:
             break;
         default:
             log_error("ERROR!!!!!");
             assert(0);
             break;
    }
    return value;
}

函数现在只有一个退出点,如果它返回,则返回它的参数(隐式转换为类型int)。

【讨论】:

  • 我相信这些案例只是为了简单起见。更琐碎化并没有为答案提供任何价值
  • 您假设CASE_A 的整数值为0。这可能不是真的。
  • @AndrewHenle,我只假设问题中给出的my_enum_t 的定义与问题中的函数参数相同。在这种情况下,CASE_A 的值必须为 0。
  • @JohnBollinger 够公平的。我想我可能变得过于怀疑/愤世嫉俗......
  • 我还根据其名称和参数类型定义将原始函数实现解释为执行 type 转换(使用范围检查)而不是值转换。我认为这个答案中提出的实现特别清楚地表达了这一目的。
【解决方案3】:

首先请检查这个答案:Best practice for compute the function return value。 MISRA-C 规则是建议性的,我建议永久偏离它。我个人将其替换为以下规则:

“应避免在函数中使用多个返回语句除非它们使代码更具可读性/可维护性。”

避免从嵌套的复杂代码中的多个位置返回的理由是合理的,但在干净和可读的函数中则远非如此。

不过,在您的具体情况下,我可能会像这样重写函数(符合 MISRA 而不忽略规则):

uint32_t my_conv_funct (my_enum_t value)
{
  uint32_t result;

  switch(value)
  {
    case CASE_A: result = 0; break;
    case CASE_B: result = 1; break;
    case CASE_C: result = 2; break;
    default:
    {
      // error handling here
    }
  }
  return result;
}

或者(偏离规则):

uint32_t my_conv_funct (my_enum_t value)
{
  static const uint32_t lut[] = { CASE_A, CASE_B, CASE_C };

  for(size_t i=0; i<sizeof lut/sizeof *lut; i++)
  {
    if(lut[i] == value)
    {
      return i;
    }
  }

  /* error handling */

  return some_error_code;
}

这是假设项目的数量不大,在这种情况下,二分查找可能效率更低。

这又假设枚举常量不对应于 0、1 和 2,在这种情况下,整个函数都是无意义的。

【讨论】:

    【解决方案4】:

    MISRA C“单一出口”规则的基本原理是因为它是功能安全标准(例如 IEC 61508 和 ISO 26262)的要求。

    它也是建议,因此如果情况需要,可以不应用

    我个人的看法是,switch 语句很少需要多个出口 - 很容易通过结构来避免它们......但是在某些情况下(例如参数验证)可能有意义。

    --

    顺便说一句,assert() 的使用不符合 MISRA C,因为它扩展到 abort(),这违反了规则 21.8 - 这也是嵌入式系统中非常不受欢迎的行为(并且在托管环境)...

    -- 查看隶属关系的个人资料。

    【讨论】:

    • 不管我个人如何看待这个特定的规则,这都是一个奇怪的循环依赖。 IEC 61508 和朋友要求 MISRA-C(“安全子集”)。 MISRA-C 显然需要 IEC 61508 才能(据说)理解某些规则。这反过来意味着 MISRA-C 在常规嵌入式系统等“SIL 0”应用程序中没有意义。这是一个不幸的依赖,因为至少我相信 MISRA-C 对通用嵌入式系统也有很多好处,而不仅仅是安全关键。
    • 所以也许应该更新 MISRA-C 以包含它自己的基本原理/有效来源。例如 Hatton - Safer C,它经常在指南的其他地方被声明为来源,并且比 1970 年代的 Yourdon 书要好得多。 Safer C 的 6.1.3 简要介绍了多个返回,并且基本上得出的结论是,拥有它们没有问题,除非它们会增加圈复杂度。这也非常接近我自己对这条规则的重新定义——避免多次返回除非它们使代码更易读/更简单。
    • 不同意其循环... 61508 要求单次进入/退出... MISRA为此提供了咨询规则。同样,我经常在演讲中讨论这条规则,建议你需要动脑筋。问题是太多的盒子提示器不会尝试了解发生了什么。
    • IEC 61508 有“推荐”或“强烈推荐”,所以它实际上也相当模糊,尽管可能很难说服评估者偏离“强烈推荐”。至于 MISRA-C 规则,问题在于,当您确实使用您的大脑时,您会发现该规则没有有效的理由。无论是否咨询,规则最好基于科学证据或知名工程实践。
    • 再一次,我们将不得不同意不同意:-)
    【解决方案5】:

    更新后的问题现已扩展为包括:

    如果没有错误,则不应调用默认情况(例如,程序员在枚举中添加另一个值并且不添加相应的情况

    另一种选择是完全删除默认设置,但这违反了另一个 misra 规则

    我不同意...

    默认设置是作为错误捕获机制存在 - 特别是在实时/嵌入式系统中,数据值可能会发生意外变化(宇宙射线任何人),它是一位勇敢的现实世界工程师,不会防止意外发生。

    实际到达包含注释/* Can never reach here */defaultelse 子句的频率是多少?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-05-06
      • 2013-12-23
      • 1970-01-01
      • 1970-01-01
      • 2018-03-16
      • 2016-07-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多