18

Initializing an array (in C++, but any solution which works for C will likely work here as well) with less initializers than it has elements is perfectly legal:

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

However, this can be a source of obscure bugs. Is there a way to have the compiler (gcc) check the number of initializers for one specific array, and emit a warning or even an error if declared and actual size don't match?

I know I can use int array[] = { 1, 2, 3 }; and could then use static assertions involving sizeof(array) to verify my expectation there. But I'm using array in other translation units, so I have to declare it with an explicit size. So this trick won't work for me.

MvG
  • 57,380
  • 22
  • 148
  • 276
  • 5
    I 'm not sure about a warning, but any compiler that gave an error for this would be a non-conforming compiler. I can't imagine a compiler vendor adding such an option to their product, that's what static analysis tools are for. – Jon Mar 07 '13 at 11:11
  • 3
    GCC has `-Wmissing-field-initializers` but it only works for other aggregates, not arrays, probably because most people don't want it to warn for arrays. Can't you use unit tests to ensure the array contains the right values and trailing elements weren't zero-initialized? – Jonathan Wakely Mar 07 '13 at 11:14
  • @JonathanWakely In turn, `std::array` is an aggregate! (I do dislike that warning in tandem with it in fact.) – Luc Danton Mar 07 '13 at 11:46
  • @Luc Danton that being said though, I'm sure a lot of the OP problems with C arrays would just go away with `std::array`. You can even initialise with `{}` now, right? – Bingo Mar 07 '13 at 12:25
  • @LucDanton, that's a different warning, `-Wmissing-braces`, and it's not enabled by default for GCC 4.8, because of `std::array` – Jonathan Wakely Mar 07 '13 at 12:33
  • @Bingo: For `std::array array={1,2,3}` my gcc 4.7 issues a warning about missing braces. And `std::array array={{1,2,3}}` is still possible without warning, as it simply does classic initialization of the inner array. So I don't see `std::array` resolving my problems. – MvG Mar 07 '13 at 12:33
  • @JonathanWakely: Would you turn your suggestion about using a unit test into an answer? I guess this is the one I'll use for now, unless someone can come up with a more elegant solution. – MvG Mar 07 '13 at 12:44

4 Answers4

7

(promoted from a comment as requested)

If the values in the array are important to the correct functionality of the system, and having zero-initialized values at the end causes bugs, then I would just add a unit test to verify the array contains the right data, instead of trying to enforce it in the code.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • I do agree that a unit test would be a good idea yet, you seem to imply static code checks don't add value. I believe adding a static assert has some benefits over a unit test (but does not have to replace the unit test): - When the code is modified or read the assumptions made when the code was written are clear (design by contract) - Static assertions are a form of unit testing by the compiler, they run automatically and thus are a great form of continuous integration and testing. – Maarten Arits Jul 23 '18 at 12:11
  • _"you seem to imply static code checks don't add value"_. No, that's not what I said. "I would verify this particular constraint using a test" does not mean "static assertions are useless, don't bother using them". – Jonathan Wakely Jul 23 '18 at 12:55
  • Why do you prefer a unit test instead of trying to enforce it in code? – Maarten Arits Jul 23 '18 at 13:40
  • Because the proposed solutions for enforcing it in the code are fragile and could offer a false sense of security. A unit test that explicitly checks the requirement is easy to write and hard to get wrong. Adding compile-time static checks **as well** is fine, but "If the values in the array are important to the correct functionality of the system, and having zero-initialized values at the end causes bugs, then I would just add a unit test to verify the array contains the right data". – Jonathan Wakely Jul 23 '18 at 14:39
5

Since you are using array in other translation units, it apparently has external linkage. In this case, you are allowed to declare it twice, as long as the declarations give it the same type. So simply declare it twice, once allowing the compiler to count the initializers and once specifying the size. Put this line in one source file, before any header that declares array:

int array[] = { 1, 2, 3 };

Later in the same file, put an #include line that declares array, with a line such as:

extern int array[10];

If the array sizes differ in the two declarations, the compiler will report an error. If they are the same, the compiler will accept them.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • One of those often neglected features. :) Is this the same in C and C++, btw? – Alexey Frunze Mar 07 '13 at 12:32
  • @AlexeyFrunze: Not quite the same; C++ treats both lines as definitions, but inserting `extern` at the start of the second line makes it a declaration that is not a definition. – Eric Postpischil Mar 07 '13 at 12:35
  • Nice, I tried this but I put the two declarations the other way round, which _doesn't_ give a diagnostic (for obvious reason now I think about it) – Jonathan Wakely Mar 07 '13 at 12:37
  • 1
    Problem with that approach is that the source file defining the array includes the header file with the exported delaration. So by the time `int array[]` is processed, the compiler has already seen an `extern int array[10]` and will therefore silently assume 10 as the size again. – MvG Mar 07 '13 at 12:38
  • You could include the definition in the header, before the declaration, but guarded by a macro such as `DEFINE_ARRAY`, that is only set by the source file .. a bit ugly and fragile though – Jonathan Wakely Mar 07 '13 at 12:39
  • @MvG: Put the first line before any `#include` lines, or, if you need types, only after `#include` lines that declare types, not objects. – Eric Postpischil Mar 07 '13 at 12:39
  • Reordering stuff breaks my precompiled headers. I'm not saying that this makes your approach completely impossible, just that it comes at a cost. Will probably use a unit test for now, but will remember this for future cases where things might be different. – MvG Mar 07 '13 at 12:46
  • @MvG: Precompiled headers are intended to be an optimization, not a constraint. If they interfere in any way with writing code, then the precompiled headers or the processes for creating and using them should be changed, not the code. – Eric Postpischil Mar 07 '13 at 14:59
4

I have an idea.

#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;
}

Output (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: The OP has found a shorter C++11 solution, building upon the same idea of using __VA_ARGS__ and a static/compile-time assertion:

#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;
}

Output (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]
Alexey Frunze
  • 61,140
  • 12
  • 83
  • 180
  • In my case, `COUNT` is several thousand, so the counting macros for that would be quite long. But perhaps there is a C++11 way to make this work? Something along the lines of `std::tuple_size::value` or similar. – MvG Mar 07 '13 at 12:01
  • Yes, my suggestion [works](http://ideone.com/H1RMlM). Do you want to update your answer, may I edit it, or should I rather post that as a separate answer? – MvG Mar 07 '13 at 12:10
  • Cool! I'll incorporate your part into the answer. – Alexey Frunze Mar 07 '13 at 12:20
  • I was thinking along the same lines, e.g. declaring a temporary array and checking its size but realized this array might not be optimized out if it's global. Template magic and the new C++ features seem to help greatly. – Alexey Frunze Mar 07 '13 at 12:28
4

I looked around for a specific answer to this in C99 and found an answer here: How can I use “sizeof” in a preprocessor macro?

If you don't define the size of your array and use:

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);

you can easily detect if the sizeof the array is as you expected. A compilation error will be raised if the static assert fails. There is no run time overhead. A very solid implementation of the static assert can be found here: Static assert implementation C

for your convenience:

#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

I Added a macro on top of this one specifically for validating the sizeof an array. The number of elements must exactly match the specified length:

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

If you don't need to support C99 you can use the new C11 feature _Static_assert. More info here. If you don't require C support, you can also rely on the c++ static assert:

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