53

I'm using string literal tables a lot in my C code. These tables all look more or less like this:

static const char* const stateNames[STATE_AMOUNT] =
{
    "Init state",
    "Run state",
    "Pause state",
    "Error state",
};

The problem with the code above is if the table gets longer and is modified during development, I forget a comma from time to time. The code compiles without a problem with a missing comma, but my program ends up crashing as the last string is set to NULL. I used the MinGW and Keil compilers to verify.

Is there any way to generate a compiler warning for my initialization if the comma is missing?

S.S. Anne
  • 15,171
  • 8
  • 38
  • 76
Jonny Schubert
  • 1,393
  • 2
  • 18
  • 39

4 Answers4

63

Wrapping every const char* in a pair of parenthesis should solve the problem as shown in the following snippet:

static const char* const stateNames[5] =
{
    ("Init state"),
    ("Run state"),
    ("Pause state")     //comma missing
    ("Pause state3"),
    ("Error state")
};

If you forget a comma, you will get a compilation error similar to: error: called object is not a function or function pointer

LIVE DEMO


Note that if you forget the comma what actually happens is that C will actually concatenate the two (or more) strings until the next comma, or the end of the array. For instance let's say you forget the comma as shown in the following:

static const char* const stateNames[] =
{
    "Init state",
    "Run state",
    "Pause state" //comma missing
    "Pause state3" //comma missing
    "Error state"
};

int main(void)
{  
    printf("%s\n", stateNames[0]);
    return 0;    
}

This is what gcc-9.2 generates (other compilers generate similar code):

.LC0:
        .string "Init state"
        .string "Run state"
        .string "Pause statePause state3Error state" ; oooops look what happened
        .quad   .LC0
        .quad   .LC1
        .quad   .LC2
main:
        push    rbp
        mov     rbp, rsp
        mov     eax, OFFSET FLAT:.LC0
        mov     rdi, rax
        call    puts
        mov     eax, 0
        pop     rbp
        ret

It is clear that the last three strings are concatenated and the array as not the length you would expect.

Davide Spataro
  • 7,319
  • 1
  • 24
  • 36
34

You could let the compiler count the array and generate an error message if unexpected result:

enum { STATE_AMOUNT = 4 };

static const char* const stateNames[] =
{
    "Init state",
    "Run state",
    "Pause state"    // <--- missing comma
    "Error state",
};

_Static_assert( sizeof stateNames / sizeof *stateNames == STATE_AMOUNT,
        "oops, missed a comma" );

See this thread for ideas to implement _Static_assert if your compiler is very old and doesn't support it.

As a bonus, this can also help with when you add new states but forget to update the string table. But you may want to look into X Macros too.

M.M
  • 138,810
  • 21
  • 208
  • 365
11

I've always used a reference to an explicitly sized array to solve this.

// no explicit size here
static const char* const stateNames[] =
{
    "Init state",
    "Run state",
    "Pause state",
    "Error state",
};
static const char* const (&stateNameVerifier)[STATE_AMOUNT] = stateNames;

http://coliru.stacked-crooked.com/a/593fc2eac80782a6

main.cpp:10:32: error: reference to type 'const char *const [5]' could not bind to an lvalue of type 'const char *const [4]'
static const char* const (&stateNameVerifier)[STATE_AMOUNT] = stateNames;
Mooing Duck
  • 64,318
  • 19
  • 100
  • 158
  • 4
    A static assertion seems like a much more elegant solution. I suppose you got the habit of doing this before static assertions were implemented as part of the language? Do you still now see any advantage of this over a static assertion that verifies the expected size of the array? – Cody Gray - on strike Jan 27 '20 at 20:10
  • 2
    @CodyGray: Yeah, this was pre-static-assert now that you mention it – Mooing Duck Jan 27 '20 at 21:28
9

This doesn't bring the compiler in to help you, but I find writing it like below makes it easier for humans to not drop a comma:

static const char* const stateNames[STATE_AMOUNT] =
{
      "Init state"
    , "Run state"
    , "Pause state"
    , "Error state"
};
JonathanZ
  • 1,046
  • 11
  • 20
  • 3
    Appending something at the end is easier, too. You don't have to edit the previous line to add a comma. ( The main reason for the missing comma. ) – datafiddler Jan 28 '20 at 13:44
  • @datafiddler: Agreed. It's also useful for fine-tuning the list of columns in a SQL SELECT command, when you're commmenting and un-commenting them. You often want to change the last one; you rarely want to change the first. This way you don't have to modify multiple lines to comment out one item. – JonathanZ Jan 28 '20 at 15:48