C 语言:从两个具有不同 return 值的相似函数中创建一个通用函数

C-languge: Creating a generic function out of two similiar functions which have different return values

我正在用两种不同的参数数据类型编写两个不同的函数。此外,这两个函数都有不同的 return 类型。第一个函数 Function_One_u4 有两个 uint8 类型的参数:signal_one_u4signal_two_u4。另一方面,函数 Function_Two_u16 具有相似的模式,但两个信号的类型为 uint16signal_one_u16signal_two_u16。因此,第一个和第二个函数分别具有 return 类型 uint8uint16。此外,两个函数 return default 语句中的不同 ERROR 值。

uint8 Function_One_u4(const uint8 mode_u2,
                      const uint8 signal_one_u4,
                      const uint8 signal_two_u4)
{
    switch(mode_u2)
    {
        case NOT_ACTIVE_U2:
        {
            return signal_two_u4;
        }
        case ACTIVE_U2:
        {
            return signal_one_u4;
        }
        case ERROR_U2:
        {
            return signal_one_u4;
        }
        case NOT_AVLB_U2:
        {
            return signal_two_u4;
        }
        default:
        {

            return ERROR_U4; /* Return value of 15 */
        }
    }
}

uint16 Function_Two_u16(const uint8 mode_u2,
                        const uint8 signal_one_u16,
                        const uint8 signal_two_u16)
{
    switch(mode_u2)
    {
        case NOT_ACTIVE_U2:
        {
            return signal_two_u16;
        }
        case ACTIVE_U2:
        {
            return signal_one_u16;
        }
        case ERROR_U2:
        {
            return signal_one_u16;
        }
        case NOT_AVLB_U2:
        {
            return signal_two_u16;
        }
        default:
        {

            return ERROR_U16; /* Return value of 65535 */
        }
    }
}

void main(void)
{
    uint8  ret_val_u4  = Function_One_u4();
    uint16 ret_val_u16 = Function_Two_u16();
}

您会注意到函数几乎具有相同的逻辑 - 基于参数 uint8 mode_u2,它们 return 要么是第一个信号,要么是第二个信号。因此,在 templates 的帮助下制作一个泛型函数是有意义的。通用函数将避免重复 switch case 代码:

<T> Generic_Function_<T> (const uint8 mode_u2,
                          const <T> signal_one,
                          const <T> signal_two,
                          const <T> error)
{
    switch(mode_u2)
    {
        case NOT_ACTIVE_<T>:
        {
            return signal_two;
        }
        case ACTIVE_<T>:
        {
            return signal_one;
        }
        case ERROR_<T>:
        {
            return signal_one;
        }
        case NOT_AVLB_<T>:
        {
            return signal_two;
        }
        default:
        {
            return error;
        }
    }
}

uint8 Function_One_u4(const uint8 mode_u2,
                      const uint8 signal_one_u4,
                      const uint8 signal_two_u4)
{
    Generic_Function_<T>(mode_u2, signal_one_u4, signal_two_u4);
}

uint16 Function_Two_u16(const uint8 mode_u2,
                        const uint8 signal_one_u16,
                        const uint8 signal_two_u16)
{
    Generic_Function_<T>(mode_u2, signal_one_u16, signal_two_u16);
}

但是C语言不支持模板。我在网上发现可以使用预处理器宏创建 C 模板。但我也读到应该避免对函数使用宏,因为它们增加了在代码中引入错误的机会。 我正在用 C 语言编写一个安全关键的 MISRA 软件,所以代码中潜伏的意外错误并不是一件好事:)。

对于如何处理代码重复,还有其他建议吗? 提前谢谢您!

你已经涵盖了一切。您可以编写一个宏来为您生成函数,也可以复制新类型的逻辑。 C11 鼓励通过 _Generic preprocessor directive.

使用宏进行泛型编程

_Generic 被 MISRA 禁止,类似函数的宏也是如此。所以在 MISRA 下你需要同时使用这两个函数。

一种方法是在任何地方只使用 Function_Two_u16 函数,并将错误值作为第三个参数,如下所示。

uint16 Function_Two_u16(const uint8 mode_u2,
                            const uint16 signal_one_u16,
                            const uint16 signal_two_u16, const uint16 error) 

这个 Function_Two_u16 也适用于 uint8 值。

通常,程序员在避免代码重复的尝试中倾向于过于极端。当然,应该尽可能避免,但不要不惜任何代价。在安全关键系统的上下文中,应避免泛型编程:

首先,你至少要在理论上能够证明所有的执行路径都是真实执行的——代码覆盖率。泛型编程可以在此处添加虚假安全性,如果您将 2 个函数合并为一个函数,那么该函数确实会执行,但仅适用于其中一个用例。

其次,安全关键系统必须是 100% 确定性的,没有未知参数。所以传统的泛型编程从一开始就没有多大意义。理想情况下,您应该能够将规范中的每个要求跟踪到特定的代码块,然后为该代码设计测试。使用专用函数而不是通用函数,这部分变得更加容易。


这里可以使用宏或 C11 _Generic(被 MISRA-C 禁止)的一些技巧,但我认为可以简化这个特定案例而不会使事情变得太复杂。为什么不简单地为“U 型”添加一个附加参数?我看不出为什么不能像这样重写函数的明显原因:

uint16_t function (uint8_t mode
                   uint8_t singal_one,
                   uint8_t signal_two
                   utype_t utype)
{
  uint16_t result;

  switch(mode)
  {
    case NOT_ACTIVE: { result = signal_two; break; }
    case ACTIVE:     { result = signal_one; break; }
    case ERROR:      { result = signal_one; break; }
    case NOT_AVLB:   { result = signal_two; break; }
    default:         
    { 
      if(utype==U2)
      {
        result = (uint16_t) ERROR_U2;
      }
      else
      {
        result = ERROR_U4;
      }
    }
  }
  
  return result;
}

我从参数中删除了 const,因为那部分只是与安全无关的主观编码风格。

来自 MISRA-C 上下文的注释:

  • 我从 stdint.h 切换到推荐类型而不是自制类型。
  • uint8_tuint16_t的转换在MISRA-C中没问题,它们属于相同的基本类型类别。您只需要避开关于将复合表达式分配给更宽类型的愚蠢规则,因此上面的复杂默认设置假设 ERROR_U2 是 8 位,但 ERROR_U4 是 16 位。
  • 不允许使用多个 return 语句。这有时是一个糟糕的规则,有时却很有意义。在这种情况下,您可以通过使用结果变量和单个 return 来简单地符合要求,而不会影响可读性。

我没有在静态分析中运行这段代码,但我认为它符合 MISRA-C。

仅供参考,从一般情况和 MISRA-C 的角度来看都值得怀疑的宏替代方案将如下所示:

#define function(n) uint16_t function_##n (uint8_t mode_u2, ...
   ...
  default: { result = ERROR_##n;

然后你可以让这个宏用 function(U2)function(U4) 生成 2 个不同的函数,并让宏将函数的内容扩展到不同的枚举等。不解决 return 键入虽然,所以你可能仍然需要像我的第一个例子那样做。

只需实现一次逻辑,然后根据需要调用其他函数。

uint16 Function_Two_u16(const uint8  mode_u2,
                        const uint16 signal_one_u16,
                        const uint16 signal_two_u16)
{
    switch(mode_u2)
    {
        case NOT_ACTIVE_U2:
        {
            return signal_two_u16;
        }
        case ACTIVE_U2:
        {
            return signal_one_u16;
        }
        case ERROR_U2:
        {
            return signal_one_u16;
        }
        case NOT_AVLB_U2:
        {
            return signal_two_u16;
        }
        default:
        {

            return ERROR_U16; /* Return value of 65535 */
        }
    }
}

uint8 Function_One_u4(const uint8 mode_u2,
                      const uint8 signal_one_u4,
                      const uint8 signal_two_u4)
{
    uint8 ret = ERROR_U4;
    uint16 val = Function_Two_u16(mode_u2, signal_one_u4, signal_two_u4);
    if (val != ERROR_U16)
    {
        ret = (uint8)val;
    }
    return ret;
}