19

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.

7
  • 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. Commented Mar 7, 2013 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? Commented Mar 7, 2013 at 11:14
  • @JonathanWakely In turn, std::array is an aggregate! (I do dislike that warning in tandem with it in fact.) Commented Mar 7, 2013 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? Commented Mar 7, 2013 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 Commented Mar 7, 2013 at 12:33

5 Answers 5

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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.
"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".
Why do you prefer a unit test instead of trying to enforce it in code?
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".
6

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.

8 Comments

One of those often neglected features. :) Is this the same in C and C++, btw?
@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.
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)
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.
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
|
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]

4 Comments

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<decltype(std::make_tuple(__VA_ARGS__))>::value or similar.
Yes, my suggestion works. Do you want to update your answer, may I edit it, or should I rather post that as a separate answer?
Cool! I'll incorporate your part into the answer.
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.
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 */

3 Comments

C11 supports static_assert, you don't need to do it by hand.
Yes, you are right, I will add it to my answer. I'm currently restricted to C99 however.
Also, your C++ assertion is wrong: sizeof(test) == 3. In C++17 std::size(test) == 3 will work for an array, in C++14 (std::end(test) - std::begin(end)) == 3 will work.
1

Another solution is to define the array of type which does not have the default constructor. This trick will prevent compiler from the default initialization of the entries above the limit.

#include <array>

struct int_container
{
  int value;
  constexpr int_container(int v) noexcept : value(v) {}
};

std::array<int_container, 3> Values {1, 2};

The code above will emit the following error:

./1.cpp:9:42: error: could not convert ‘<brace-enclosed initializer list>()’ from ‘<brace-enclosed initializer list>’ to ‘int_container’
    9 | std::array<int_container, 3> Values {1, 2};
      |                                          ^
      |                                          |
      |                                          <brace-enclosed initializer list>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.