是否可以使用gcov排除多余的、不可见的else分支?

huangapple go评论47阅读模式
英文:

Is it possible to exclude a redundant, invisible else branch with gcov?

问题

以下是您要翻译的代码部分:

想象一下以下函数:

int myFunction(my_enum_t x)
{
  int result = 0;

  if ((x != ENUM_VAL_A) &&
      (x != ENUM_VAL_B) &&
      (x != ENUM_VAL_C))
  {
    result = -1;
  }

  if (0 == result)
  {
     result = mutateSomething(x);
  }

  if (0 == result)
  {
    if (x == ENUM_VAL_A)
    {
       result = doOneThing();
    }

    else if (x == ENUM_VAL_B)
    {
       result = doThing2();
    }

    else if (x == ENUM_VAL_C)
    {
       result = doThing3();
    }
    /* 隐式的 else */
  }

  return result;
}

希望这能帮助您。如果您需要更多翻译,请告诉我。

英文:

Imagine the following function:

int myFunction(my_enum_t x)
{
  int result = 0;

  if ((x != ENUM_VAL_A) &&
      (x != ENUM_VAL_B) &&
      (x != ENUM_VAL_C))
  {
    result = -1;
  }

  if (0 == result)
  {
     result = mutateSomething(x);
  }

  if (0 == result)
  {
    if (x == ENUM_VAL_A)
    {
       result = doOneThing();
    }

    else if (x == ENUM_VAL_B)
    {
       result = doThing2();
    }

    else if (x == ENUM_VAL_C)
    {
       result = doThing3();
    }
    /* Invisible else */
  }

  return result;
}

Whether in the interests of clarity or to avoid complicating mutateSomething(), I would like to keep the checks on x before that call, however, gcov will see the invisible else where I've commented. That branch cannot be covered. I am, for this reason, using the --exclude-unreachable-branches switch of gcovr, but this branch is still regarded as uncovered.

Is there any way to avoid this without removing the checks on x at the start of the function?

I sometimes wrap the whole final else if

else if (x == ENUM_VAL_C)
{...}

in exclusion tags, on the basis that I am allowed 2% uncovered code, but this is not possible in sources with one or two functions.

答案1

得分: 4

  • else if (x == ENUM_VAL_C) --> else,因为在这一点上已经知道 x == ENUM_VAL_C,这可以消除不必要的测试和分支。

  • 正向测试 (== vs !=) 倾向于更容易理解,尤其是在测试复杂性增加的情况下。

  • 初始化为 0 从未被使用。考虑使用 int result = -1; 来将 result 初始化为错误值。

int myFunction(my_enum_t x) {
  int result = -1;
  // x 是否有效?
  if ((x == ENUM_VAL_A) || (x == ENUM_VAL_B) || (x == ENUM_VAL_C)) {
    result = mutateSomething(x);
    if (result == 0) {
      if (x == ENUM_VAL_A) {
        result = doOneThing();
      } else if (x == ENUM_VAL_B) {
        result = doThing2();
      } else {
        result = doThing3();
      }
    }
  }
  return result;
}
英文:

Candidate alternative:

  • else if (x == ENUM_VAL_C) --> else, as at this point x == ENUM_VAL_C is already known. This eliminates the unneeded test and branch.

  • Positive tests (== vs !=) tend to be easier to understand, especially as test complexity increases.

  • Initialization to 0 is never used. Consider int result = -1; to initialize result to an error value.

int myFunction(my_enum_t x) {
  int result = -1;
  // Is x valid?
  if ((x == ENUM_VAL_A) || (x == ENUM_VAL_B) || (x == ENUM_VAL_C)) {
    result = mutateSomething(x);
    if (result == 0) {
      if (x == ENUM_VAL_A) {
        result = doOneThing();
      } else if (x == ENUM_VAL_B) {
        result = doThing2();
      } else {
        result = doThing3();
      }
    }
  }
  return result;
}

答案2

得分: 2

Here's the translated portion:

由于您已经验证了函数开始时的x是三个允许的值之一,因此可以将最终的else if更改为else,这意味着您的逻辑以明确的else结束。例如:

if (x == ENUM_VAL_A)
{
   result = doOneThing();
}
else if (x == ENUM_VAL_B)
{
   result = doThing2();
}
else // 必须是ENUM_VAL_C
{
   result = doThing3();
}
/* 没有隐式的else */
}

我觉得这有点糟糕,但在进行代码覆盖和单元测试时,有时您必须这样做。具体来说,任何“双保险”和“这不可能发生”的代码路径都无法进行测试。希望您的测试能够捕捉到如果将来添加了ENUM_VAL_D的情况。

此外,一些标准(例如MISRA)要求if...else if...链以最终的else结束(MISRA-C-2012规则15.7)。我可以理解这样做的理由。

英文:

Since you have already verified that x is one of the three permissible values at the start of the function, you can change your final else if into an else, which means your logic ends with an explicit else. i.e.

    if (x == ENUM_VAL_A)
    {
       result = doOneThing();
    }
    else if (x == ENUM_VAL_B)
    {
       result = doThing2();
    }
    else // it must be ENUM_VAL_C
    {
       result = doThing3();
    }
    /* No invisible else */
  }

I find it a bit horrible, but when you're doing code coverage and unit-testing, sometimes you've got to do things like that. Specifically, any "belts and braces" and "this can't happen" code paths can't be tested. Hopefully your tests will catch the case if, in the future, you add ENUM_VAL_D.

Aside: some standards (such as MISRA) demand that an if...else if... chain ends with a final else. (MISRA-C-2012 rule 15.7). I can see the justification for it.

答案3

得分: 0

以下是您请求的翻译部分:

作为一个习惯于处理与安全相关的代码,如MISRA C等的人,我在这里的推理中看到了一些问题:

  • “一个枚举只能有枚举列表中定义的一个值”是错误的。它只是一种被美化的整数类型。
  • “那个分支不可能被覆盖”除非枚举得到意外的值,实际上是可以覆盖的。

像这样查找意外执行分支的原因之一是进行静态分析/代码覆盖的原因之一。

现在,为什么这个枚举会得到除了 ENUM_VAL_AENUM_VAL_BENUM_VAL_C 之外的任何其他值呢?在嵌入式系统中,我们可能会开始讨论一些不太可能的事情,比如电磁干扰、RAM存储器不可靠、宇宙射线等。但更有可能的情况是,由于错误,RAM变量会意外地改变值。指针错误、越界访问、失控代码、栈溢出等等。

无论您拥有多高水平的代码质量和完整性,都加入“防御性编程”都是没有错的。这意味着您添加错误检查来捕获在理论上不应该发生的事情。在这种情况下,只需添加一个 else

else if (x == ENUM_VAL_C)
{
   result = doThing3();
}
else
{
  // 这实际上不是无法到达的代码。
}

在您的具体情况下,最有可能在这个 else 中写入 return -1。在其他情况下,只需写 else {} 可能就足够了。空 elseelse if 后面和根本没有 else 之间的区别在于前者意味着:“我实际上考虑了这种不太可能的情况,但它不会影响程序”。而跳过 else 只意味着:“我可能根本没有考虑这种情况”。添加这样的防御性编程检查/自我记录的代码在执行开销方面成本很低。


至于如何总体编写这个代码... 事实证明,多个返回实际上会使代码更易读。使用专用的枚举来表示返回代码,而不是“魔法数字”也会更好。

我还会放弃1980年代的“尤达条件”编码风格 (0 == something),而是配置编译器始终警告条件表达式中的赋值。在gcc中,可以使用 -Wall-Wparenthesis

对于具有相邻递增数字0、1、2等的枚举,我们还可以创建枚举常量和函数之间的对应关系。也就是说,ENUM_VAL_A 应该始终对应于 doOneThing。一种方法是用函数指针跳转表替换长的if-else链。

所以,给定一些枚举:

typedef enum
{
  ENUM_VAL_A,
  ENUM_VAL_B,
  ENUM_VAL_C,
  ENUM_N          // 枚举常量的数量
} my_enum_t;

typedef enum      // 明确的错误代码而不是“具有魔法数字的int”
{
  ERR_NONE,
  ERR_WRONG_INPUT,
  ERR_UNEXPECTED,
} err_t;

然后,我们可以完全重构这个函数,如下所示:

err_t myFunction (my_enum_t x)
{
  if ((x != ENUM_VAL_A) &&
      (x != ENUM_VAL_B) &&
      (x != ENUM_VAL_C))
  {
    return ERR_WRONG_INPUT;
  }

  my_enum_t result = mutateSomething(x);
  if ((result != ENUM_VAL_A) &&
      (result != ENUM_VAL_B) &&
      (result != ENUM_VAL_C))
  {
    return ERR_UNEXPECTED;
  }
  /* 现在我们确实验证了枚举不是无效或损坏的 */

  err_t (*const jumptable[ENUM_N]) (void) = 
  { 
    [ENUM_VAL_A] = doOneThing, 
    [ENUM_VAL_B] = doThing2, 
    [ENUM_VAL_C] = doThing3, 
  };
  
  return jumptable[result];
}

至于代码覆盖,当引入函数指针时可能会有点复杂。但就“圆锥复杂度”(可能的执行路径数量)而言,由于没有(嵌套的)if-else链或switch,这段代码要简单得多。

英文:

As someone who's used at working with safety-related code, MISRA C and similar, I see some flaws in the reasoning here:

  • "An enum can only have one of the values defined in the enumeration list" is false. It is just a glorified integer type.
  • "That branch cannot be covered" Except it can, in case the enum gets an unexpected value.

To find unexpected execution branches like this is one of the reasons to perform static analysis/code coverage to begin with.

Now why would this enum ever get any other value than ENUM_VAL_A, ENUM_VAL_B, ENUM_VAL_C? In embedded systems we might start to argue about somewhat unlikely things like electromagnetic interference, RAM memories being unreliable, cosmic rays etc. But far more likely, RAM variables will change values unexpectedly because of bugs. Pointer bugs, out of bounds access, runaway code, stack overflows and so on.

No matter what level of code quality and integrity you have, it is never wrong to include defensive programming. Meaning you add error checks to catch things that shouldn't be possible in theory. In this case by simply adding an else:

else if (x == ENUM_VAL_C)
{
   result = doThing3();
}
else
{
  // this is actually not unreachable code.
}

In your specific case it most likely makes most sense to write a return -1 inside this else. In other cases, it might be sufficient to just write else {}. The difference between an empty else after else if and no else at all is that the former means: "I have actually considered this unlikely scenario, but it will not affect the program". Whereas skipping the else just means "I have probably not considered this scenario at all". Adding such defensive programming checks/self-documenting code cost very little in terms of execution overhead.


As for how to write this whole thing in general... as it turns out, multiple returns will actually turn the code far more readable. As would using a dedicated enum for return codes instead of "magic numbers".

I would also drop 1980s "Yoda conditions" coding style (0 == something) and instead configure the compiler to always warn for assignment inside conditional expressions. With gcc that's -Wall or -Wparenthesis.

For an enum with adjacent, increasing number 0, 1, 2 etc we can also create a correspondence between an enumeration constant and a function. That is, ENUM_VAL_A should always correspond with doOneThing. One way to do this is to replace the long if-else chain with a function pointer jump table.

So given some enums

typedef enum
{
  ENUM_VAL_A,
  ENUM_VAL_B,
  ENUM_VAL_C,
  ENUM_N          // number of enumeration constants
} my_enum_t;

typedef enum      // explicit error codes over "int with magic numbers"
{
  ERR_NONE,
  ERR_WRONG_INPUT,
  ERR_UNEXPECTED,
} err_t;

Then we can completely overhaul the function like this:

err_t myFunction (my_enum_t x)
{
  if ((x != ENUM_VAL_A) &&
      (x != ENUM_VAL_B) &&
      (x != ENUM_VAL_C))
  {
    return ERR_WRONG_INPUT;
  }

  my_enum_t result = mutateSomething(x);
  if ((result != ENUM_VAL_A) &&
      (result != ENUM_VAL_B) &&
      (result != ENUM_VAL_C))
  {
    return ERR_UNEXPECTED;
  }
  /* now we have truly verified that the enum isn't invalid or corrupt */

  err_t (*const jumptable[ENUM_N]) (void) = 
  { 
    [ENUM_VAL_A] = doOneThing, 
    [ENUM_VAL_B] = doThing2, 
    [ENUM_VAL_C] = doThing3, 
  };
  
  return jumptable[result];
}

As for code coverage, it might get a little lost when we introduce function pointers. But in terms of "cyclomatic complexity" (number of possible execution paths), then this code is much simpler since there are no (nested) if-else chains or switch.

huangapple
  • 本文由 发表于 2023年5月25日 02:00:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/76326281.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定