【问题标题】:C Compile-Time assert with constant array带有常量数组的 C 编译时断言
【发布时间】:2009-11-08 08:25:24
【问题描述】:

我有一个在编译时初始化的非常大的常量数组。

typedef enum {
VALUE_A, VALUE_B,...,VALUE_GGF
} VALUES;

const int arr[VALUE_GGF+1] = { VALUE_A, VALUE_B, ... ,VALUE_GGF};

我想验证数组是否正确初始化,例如:

if (arr[VALUE_GGF] != VALUE_GGF) {
    printf("Error occurred. arr[VALUE_GGF]=%d\n", arr[VALUE_GGF]);
    exit(1);
}

我的问题是我想在编译时验证这一点。我在这个线程中阅读了 C 语言中的编译时断言:C Compiler asserts。但是,那里提供的解决方案建议使用负值定义一个数组作为编译错误的大小:

#define CASSERT(predicate, file) _impl_CASSERT_LINE(predicate,__LINE__,file)
#define _impl_PASTE(a,b) a##b
#define _impl_CASSERT_LINE(predicate, line, file) \
   typedef char _impl_PASTE(assertion_failed_##file##_,line)[2*!!(predicate)-1];

并使用:

CASSERT(sizeof(struct foo) == 76, demo_c);

提供的解决方案对我不起作用,因为我需要验证我的常量数组值,而 C 不允许使用常量数组值初始化数组:

int main() {
   const int i = 8;
   int b[i];         //OK in C++
   int b[arr[0]];    //C2057 Error in VS2005

有什么办法可以解决吗?其他一些编译时断言?

【问题讨论】:

  • const int x = arr[0]; 有效吗?我认为问题不在于使用arr[0] 作为数组大小,而是在编译时尝试使用arr 数组中的值,无论出于何种目的。在这种情况下,您不能对数组内容进行静态断言。
  • 编译没有错误: const int arr[2] = {1,2}; const int x = arr[0];
  • 您能否澄清为什么您不想通过一个测试来执行此操作,您将切断main 函数并将其替换为一个检查断言的函数?您想要检查的属性是否与您的示例一样明显,可能是为了检查编译器的一致性,就像您的示例中一样?我想建议一个处理用户属性的静态分析工具,但如果你检查的是编译器,这将无济于事。
  • @Pascal- 我不想创建另一个测试并替换主函数,因为我不会消除程序员\QA 工程师忘记运行这个测试和应用程序将被发送给带有错误的客户。编译时错误将迫使程序员验证他的代码。关于检查编译器——这不是我的意图。

标签: c arrays assert static-assert


【解决方案1】:

在下面的代码中,请参阅第 6 行和第 9 行中声明的固定长度指针的额外分配。

如果没有为 WORKDAYS 枚举的所有值初始化 2 个数组,这将导致编译时出错。 Gcc 说:test.c:6:67:警告:从不兼容的指针类型初始化[默认启用]

想象一下某个经理将 SATURDAY 添加到工作周枚举中。如果没有额外的检查,程序将编译,但运行时会因违反分段而崩溃。

这种方法的缺点是它占用了一些额外的内存(我没有测试过编译器是否优化了它)。 这也有点骇人听闻,下一个家伙的代码中可能需要一些 cmets...

请注意,被测试的数组不应声明数组大小。设置数组大小将确保您保留了数据,但不能确保它包含有效的内容。

#include <stdio.h>

typedef enum { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, NOF_WORKDAYS_IN_WEEK } WORKDAYS;

const char * const workday_names[] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" };
const char * const (*p_workday_name_test)[NOF_WORKDAYS_IN_WEEK] = &workday_names;

const int workday_efforts[] = { 12, 23, 40, 20, 5 };
const int (*p_workday_effort_test)[NOF_WORKDAYS_IN_WEEK] = &workday_efforts;

main ()
{
  WORKDAYS i;
  int total_effort = 0;

  printf("Always give 100 %% at work!\n");

  for(i = MONDAY; i < NOF_WORKDAYS_IN_WEEK; i++)
  {
    printf(" - %d %% %s\n",workday_efforts[i], workday_names[i]);
    total_effort += workday_efforts[i];
  }
  printf(" %d %% in total !\n", total_effort);
}

顺便说一下,程序的输出是:

在工作中始终付出 100%!
- 12 % 星期一
- 23 % 周二
- 40 % 星期三
- 周四 20 %
- 周五 5 %
总计 100%!

【讨论】:

    【解决方案2】:

    问题在于,在 C++ 中,编译时常量表达式有以下限制(5.19 常量表达式):

    整型常量表达式只能包含字面量 (2.13)、枚举器、常量变量或使用常量表达式初始化的整型或枚举类型的静态数据成员 (8.5)、整型或枚举类型的非类型模板参数,以及 sizeof表达式。浮动文字 (2.13.3) 只有在转换为整数或枚举类型时才能出现。只能使用到整数或枚举类型的类型转换。特别是,除了 sizeof 表达式,不得使用函数、类对象、指针或引用,不得使用赋值、递增、递减、函数调用或逗号运算符。

    请记住,数组索引表达式实际上只是变相的指针运算(arr[0] 实际上是 arr + 0),并且指针不能用于常量表达式,即使它们是指向 const 数据的指针。所以我认为你对检查数组内容的编译时断言不走运。

    C 比 C++ 更受限制,在这些类型的表达式可以在编译时使用。

    但考虑到 C++ 的复杂性,也许有人可以想出一个跳出框框的解决方案。

    【讨论】:

    • const 指向 const 表达式的指针也不行吗?
    • @Eldad - 按照标​​准,是的。我想,一些编译器可以将它作为扩展提供。但一般来说,常量指针将由链接器修复,而不是编译器。我认为这至少是标准规定的行为背后的部分原因。
    【解决方案3】:

    您可以将您的断言表达为一个属性以使用静态分析器进行检查,并让分析器进行检查。这具有您想要做的一些属性:

    • 属性写在源码里,

    • 不会污染生成的二进制代码。

    但是,它与编译时断言不同,因为它需要在程序上运行单独的工具进行检查。也许这是对您尝试执行的编译器的健全性检查,在这种情况下,这无济于事,因为静态分析器不检查编译器做什么,只检查它应该做什么。 补充:如果是为了 QA,那么编写可以静态验证的“正式”断言现在风靡一时。下面的方法与您可能听说过的 .NET 合同非常相似,但它适用于 C。

    • 您可能不太重视静态分析器,但正是循环和函数调用导致它们变得不精确。他们更容易在初始化时清楚地了解正在发生的事情,在这些事情发生之前。

    • 一些分析器标榜自己是“正确的”,也就是说,如果您编写的属性超出了它们的能力范围,它们不会保持沉默。在这种情况下,他们抱怨他们无法证明这一点。如果发生这种情况,在您确信问题出在分析仪而不是阵列上之后,您将留在现在的位置,寻找其他方法。

    以我熟悉的分析仪为例:

    const int t[3] = {1, 2, 3};
    int x;
    
    int main(){
    
      //@ assert t[2] == 3 ;
    
      /* more code doing stuff */
    }
    

    运行分析器:

    $ frama-c -val t.i
    ...
    t.i:7: Warning: Assertion got status valid.
    Values of globals at initialization 
    t[0] ∈ {1; }
     [1] ∈ {2; }
     [2] ∈ {3; }
    x ∈ {0; }
    ...
    

    在分析器的日志中,你得到:

    • 它认为全局变量初始值的版本,
    • 及其对您在 //@ 注释中所写断言的解释。在这里,它通过一次断言并发现它是有效的。

    使用这种工具构建脚本的人会自动从日志中提取他们感兴趣的信息。 但是,作为负面说明,我必须指出,如果您担心最终会忘记测试,您还应该担心在代码修改后强制静态分析器通过被遗忘。

    【讨论】:

    • 谢谢。我会更深入地研究它,但我觉得这对我的目的来说有点矫枉过正,因为我只需要验证 2 个数组,而 frama-c 是一个 150MB 的压缩程序。
    【解决方案4】:

    没有。编译时断言在您的情况下根本不起作用,因为数组“arr[ARR_SIZE]”直到链接阶段才会存在。

    编辑:但 sizeof() 似乎不同,所以至少你可以这样做:

    typedef enum {VALUE_A, VALUE_B,...,VALUE_GGF} VALUES;
    const int arr[] = { VALUE_A, VALUE_B, ... ,VALUE_GGF};
    #define MY_ASSERT(expr)  {char uname[(expr)?1:-1];uname[0]=0;}
    ...
    // If initialized count of elements is/are not correct, 
    //   the compiler will complain on the below line
    MY_ASSERT(sizeof(arr) == sizeof(int) * ARR_SIZE)
    

    我已经在我的 FC8 x86 系统上测试了代码,它可以工作。

    编辑:注意到@sbi 已经确定了“int arr[]”的情况。谢谢

    【讨论】:

    • @Eldad:读一下。那么,在“const int arr[]”版本中,如果原样使用 ARR_SIZE 来表示预期的数组大小,那么当“添加新的 ENUM 但忘记在数组中插入新条目”时,实际的数组大小将不等于 ARR_SIZE , 所以上面的编译时断言 "MY_ASSERT(sizeof(arr) == sizeof(int) * ARR_SIZE)" 将起作用。
    • 我认为我的问题不够清楚。我更新了它: const int arr[VALUE_GGF+1] = { VALUE_A, VALUE_B, ... ,VALUE_GGF};
    【解决方案5】:

    由于我使用批处理文件来编译和打包我的应用程序,我认为 easiset 解决方案是编译另一个简单的程序,该程序将在我的所有数组中运行并验证内容是否正确。 我可以通过批处理文件运行测试程序,如果测试运行失败,我可以停止编译程序的其余部分。

    【讨论】:

      【解决方案6】:

      我无法想象为什么您会觉得有必要在编译时验证这一点,但是可以使用一种奇怪/冗长的 hack:

      typedef enum {
          VALUE_A, VALUE_B,...,VALUE_GGF
      } VALUES;
      struct {
          static const VALUES elem0 = VALUE_A;
          static const VALUES elem1 = VALUE_B;
          static const VALUES elem2 = VALUE_C;
          ...
          static const VALUES elem4920 = VALUE_GGF;
          const int operator[](int offset) {return *(&elem0+offset);}
      } arr;
      void func() {
          static_assert(arr.elem0 == VALUE_A, "arr has been corrupted!");
          static_assert(arr.elem4920 == VALUE_GFF, "arr has been corrupted!");
      }
      

      所有这些都在编译时起作用。不过,它非常老套和糟糕的形式。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-13
        • 1970-01-01
        • 2014-08-11
        • 1970-01-01
        相关资源
        最近更新 更多