让编译器检查数组初始值设定项的数量

Posted

技术标签:

【中文标题】让编译器检查数组初始值设定项的数量【英文标题】:Have compiler check the number of array initializers 【发布时间】:2013-03-07 11:08:41 【问题描述】:

初始化一个数组(在 C++ 中,但任何适用于 C 的解决方案都可能在这里工作)使用少于元素的初始化器是完全合法的:

int array[10] =  1, 2, 3 ;

但是,这可能是一些不为人知的错误的来源。有没有办法让编译器 (gcc) 检查一个特定数组的初始化器数量,如果声明的大小与实际大小不匹配,则发出警告甚至错误?

我知道我可以使用int array[] = 1, 2, 3 ;,然后可以使用涉及sizeof(array) 的静态断言来验证我的期望。但我在其他翻译单元中使用array,所以我必须用明确的大小声明它。所以这个技巧对我不起作用。

【问题讨论】:

我不确定是否有警告,但任何为此给出错误的编译器都是不合格的编译器。我无法想象编译器供应商会在他们的产品中添加这样的选项,这就是静态分析工具的用途。 GCC 有 -Wmissing-field-initializers 但它只适用于其他聚合,而不是数组,可能是因为大多数人不希望它警告数组。你不能使用单元测试来确保数组包含正确的值并且尾随元素没有被零初始化吗? @JonathanWakely 反过来,std::array 是一个聚合! (事实上​​,我确实不喜欢这个警告。) @Luc Danton 话虽如此,但我确信 C 数组的很多 OP 问题都会随着std::array 而消失。您现在甚至可以使用 进行初始化,对吧? @LucDanton,这是一个不同的警告,-Wmissing-braces,由于std::array,默认情况下它没有为 GCC 4.8 启用。 【参考方案1】:

(根据要求从评论中提升)

如果数组中的值对系统的正确功能很重要,并且最后初始化为零的值会导致错误,那么我只需添加一个单元测试来验证数组是否包含正确的数据,而不是试图在代码中强制执行它。

【讨论】:

我同意单元测试是个好主意,但您似乎暗示静态代码检查不会增加价值。我相信添加静态断言比单元测试有一些好处(但不必替换单元测试): - 当修改代码或阅读代码编写时所做的假设是明确的(按合同设计) - 静态断言是编译器进行单元测试的一种形式,它们自动运行,因此是一种很好的持续集成和测试形式。 “您似乎暗示静态代码检查不会增加价值”。不,我不是这么说的。 “我会使用测试来验证这个特定的约束”并不意味着“静态断言是无用的,不要费心使用它们”。 为什么你更喜欢单元测试而不是试图在代码中强制执行它? 因为在代码中强制执行它的建议解决方案很脆弱,并且可能会提供错误的安全感。明确检查需求的单元测试很容易编写并且很难出错。添加编译时静态检查以及很好,但是“如果数组中的值对系统的正确功能很重要,并且最后具有零初始化值会导致错误,那么我只需添加一个单元测试来验证数组是否包含正确的数据”。【参考方案2】:

由于您在其他翻译单元中使用array,它显然具有外部链接。在这种情况下,您可以声明它两次,只要声明给它相同的类型。所以只需声明它两次,一次允许编译器计算初始值设定项,一次指定大小。将此行放在一个源文件中,在任何声明 array 的标头之前:

int array[] =  1, 2, 3 ;

稍后在同一个文件中,放入声明array#include 行,例如:

extern int array[10];

如果两个声明的数组大小不同,编译器会报错。如果它们相同,编译器将接受它们。

【讨论】:

经常被忽视的功能之一。 :) 这在 C 和 C++ 中是否相同,顺便说一句? @AlexeyFrunze:不太一样; C++ 将这两行都视为定义,但在第二行开头插入 extern 使其成为不是定义的声明。 很好,我试过了,但我把这两个声明反过来了,这没有给出诊断(出于显而易见的原因,现在我考虑了一下)跨度> 这种方法的问题是定义数组的源文件包含带有导出声明的头文件。因此,在处理 int array[] 时,编译器已经看到了 extern int array[10],因此将再次默默地假定 10 作为大小。 您可以将定义包含在标头中,在声明之前,但由诸如DEFINE_ARRAY 之类的宏保护,该宏仅由源文件设置.. 虽然有点丑陋和脆弱跨度> 【参考方案3】:

我有个主意。

#define C_ASSERT(expr) extern char CAssertExtern[(expr)?1:-1]

#define NUM_ARGS__(X, \
                      N64,N63,N62,N61,N60, \
  N59,N58,N57,N56,N55,N54,N53,N52,N51,N50, \
  N49,N48,N47,N46,N45,N44,N43,N42,N41,N40, \
  N39,N38,N37,N36,N35,N34,N33,N32,N31,N30, \
  N29,N28,N27,N26,N25,N24,N23,N22,N21,N20, \
  N19,N18,N17,N16,N15,N14,N13,N12,N11,N10, \
  N09,N08,N07,N06,N05,N04,N03,N02,N01,  N, ...) N

#define NUM_ARGS(...) \
  NUM_ARGS__(0, __VA_ARGS__, \
                 64,63,62,61,60, \
  59,58,57,56,55,54,53,52,51,50, \
  49,48,47,46,45,44,43,42,41,40, \
  39,38,37,36,35,34,33,32,31,30, \
  29,28,27,26,25,24,23,22,21,20, \
  19,18,17,16,15,14,13,12,11,10, \
   9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

#define DECL_INIT_ARRAYN(TYPE, NAME, COUNT, N, ...) \
  C_ASSERT(COUNT == N); \
  TYPE NAME[COUNT] =  __VA_ARGS__ 

#define DECL_INIT_ARRAY(TYPE, NAME, COUNT, ...) \
  DECL_INIT_ARRAYN(TYPE, NAME, COUNT, NUM_ARGS(__VA_ARGS__), __VA_ARGS__)

DECL_INIT_ARRAY(const int, array3_3, 3, 1, 2, 3);

int main(void)

  DECL_INIT_ARRAY(const int, array5_4, 5, 1, 2, 3, 4);
  DECL_INIT_ARRAY(const int, array5_6, 5, 1, 2, 3, 4, 5, 6);
  return 0;

输出(ideone):

prog.c: In function ‘main’:
prog.c:33:3: error: size of array ‘CAssertExtern’ is negative
prog.c:34:3: error: size of array ‘CAssertExtern’ is negative
prog.c:34:3: error: excess elements in array initializer [-Werror]
prog.c:34:3: error: (near initialization for ‘array5_6’) [-Werror]
prog.c:34:3: error: unused variable ‘array5_6’ [-Werror=unused-variable]
prog.c:33:3: error: unused variable ‘array5_4’ [-Werror=unused-variable]
prog.c:34:3: error: unused variable ‘CAssertExtern’ [-Werror=unused-variable]
cc1: all warnings being treated as errors

UPD:OP 找到了一个更短的 C++11 解决方案,基于使用 __VA_ARGS__ 和静态/编译时断言的相同想法:

#include <tuple>

#define DECL_INIT_ARRAY(TYPE, NAME, COUNT, ...)                         \
  static_assert(COUNT ==                                                \
    std::tuple_size<decltype(std::make_tuple(__VA_ARGS__))>::value,     \
    "Array " #NAME " should have exactly " #COUNT " initializers");     \
  TYPE NAME[COUNT] =  __VA_ARGS__ 

DECL_INIT_ARRAY(const int, array3_3, 3, 1, 2, 3);

int main(void)

  DECL_INIT_ARRAY(const int, array5_4, 5, 1, 2, 3, 4);
  DECL_INIT_ARRAY(const int, array5_6, 5, 1, 2, 3, 4, 5, 6);
  return 0;

输出(ideone):

prog.cpp: In function ‘int main()’:
prog.cpp:13:3: error: static assertion failed: Array array5_4 should have exactly 5 initializers
prog.cpp:14:3: error: static assertion failed: Array array5_6 should have exactly 5 initializers
prog.cpp:14:3: error: too many initializers for ‘const int [5]’
prog.cpp:13:3: warning: unused variable ‘array5_4’ [-Wunused-variable]
prog.cpp:14:3: warning: unused variable ‘array5_6’ [-Wunused-variable]

【讨论】:

在我的例子中,COUNT 是几千个,所以它的计数宏会很长。但也许有一种 C++11 方法可以使这项工作?类似于std::tuple_size&lt;decltype(std::make_tuple(__VA_ARGS__))&gt;::value 或类似的东西。 是的,我的建议works。你想更新你的答案,我可以编辑它,还是我应该把它作为一个单独的答案发布? 酷!我会将您的部分纳入答案中。 我也是这么想的,例如声明一个临时数组并检查它的大小,但意识到如果它是全局的,这个数组可能不会被优化。模板魔法和新的 C++ 特性似乎有很大帮助。【参考方案4】:

我在 C99 中四处寻找对此的具体答案,并在此处找到了答案:How can I use “sizeof” in a preprocessor macro?

如果您没有定义数组的大小并使用:

int test[] = 1,2 
STATIC_ASSERT(sizeof(test)/sizeof(test[0]) == 3)
/* with C11 support: */
_Static_assert(sizeof(test)/sizeof(test[0]) == 3)
/* or more easily */
ASSERT_ARRAY_LENGTH(test, 3);

您可以轻松检测数组的大小是否符合您的预期。如果静态断言失败,将引发编译错误。没有运行时开销。可以在这里找到一个非常可靠的静态断言实现: Static assert implementation C

为了您的方便:

#define ASSERT_CONCAT_(a, b) a##b
#define ASSERT_CONCAT(a, b) ASSERT_CONCAT_(a, b)
/* These can't be used after statements in c89. */
#ifdef __COUNTER__
#define STATIC_ASSERT(e,m) \
;enum  ASSERT_CONCAT(static_assert_, __COUNTER__) = 1/(int)(!!(e)) 
#else
/* This can't be used twice on the same line so ensure if using in headers
* that the headers are not included twice (by wrapping in #ifndef...#endif)
* Note it doesn't cause an issue when used on same line of separate modules
* compiled with gcc -combine -fwhole-program.  */
#define STATIC_ASSERT(e,m) \
;enum  ASSERT_CONCAT(assert_line_, __LINE__) = 1/(int)(!! (e)) 
#endif

我在此之上添加了一个宏,专门用于验证数组的大小。元素的数量必须与指定的长度完全匹配:

#define ASSERT_ARRAY_LENGTH(array, length)\
STATIC_ASSERT(sizeof(array)/sizeof(array[0]) == length,\
    "Array is not of expected length")

如果您不需要支持 C99,您可以使用新的 C11 功能 _Static_assert。更多信息here。 如果不需要 C 支持,也可以依赖 c++ 静态断言:

std::size(test) == 3; /* C++ 17 */
(std::end(test) - std::begin(end)) == 3; /* C++ 14 */

【讨论】:

C11 支持static_assert,不需要手动操作。 是的,你是对的,我会将它添加到我的答案中。不过,我目前仅限于 C99。 另外,你的 C++ 断言是错误的:sizeof(test) == 3。在 C++17 中 std::size(test) == 3 将适用于数组,在 C++14 中 (std::end(test) - std::begin(end)) == 3 将适用。

以上是关于让编译器检查数组初始值设定项的数量的主要内容,如果未能解决你的问题,请参考以下文章

Java中数组初始值设定项中带有尾随逗号的数组

构造函数初始值设定项列表中长度未知的数组

如何通过可变参数模板将多个构造函数参数转发到数组初始值设定项列表?

C中如何实现数组的传值引用

React 中是不是仍需要带有自动绑定和属性初始值设定项的构造函数

C#3.0中的新增功能02 匿名类型