Cyan
Cyan

Reputation: 13948

static assert for C90 on gcc

static_assert() is a pretty great capability available since C11.

For pre-C11 compilers though, this capability must be emulated. It's not too hard, there are many examples available over Internet.

For example :

#define STATIC_ASSERT(CONDITION, MSG) \
typedef char static_assert_##MSG[(CONDITION)?1:-1]

This makes it possible to transfer an error message in the condition, which is handy to explain what's going wrong if it ever gets triggered.

However, this MSG is a lot different from the one in C11's static_assert() :

This is so different from C11's static_assert() that it seems impossible to create a macro which would switch transparently between the C11 and the C90 version depending on the compiler.

In an effort to accept an error message which "looks like C11", aka a string with double quote, I've tested a new macro :

#define STATIC_ASSERT(CONDITION, MSG) \
typedef char static_assert[((void)(MSG), ((CONDITION)?1:-1))]

Using the , comma operator, this macro should accept MSG as a string, and just disregard it. But it will be displayed in case of error, which is the intention.

It works fine on clang, but not of gcc : error: variably modified at file scope.

I'm trying to understand why, and if there is a work around

Upvotes: 3

Views: 2523

Answers (2)

Brent
Brent

Reputation: 4283

One-liner

#define STATIC_ASSERT(CONDITION, MSG) { typedef char test[(CONDITION)?1:-1]; (void)(test*) #MSG; } (void)0

Upvotes: 1

Joseph Quinsey
Joseph Quinsey

Reputation: 9962

If you replace the typedef-array-trick with the enum-trick, then you will get something that seems to work with both clang and gcc:

#define CONDITION 1

#define TOKENPASTE(a, b) a ## b // "##" is the "Token Pasting Operator"
#define TOKENPASTE2(a,b) TOKENPASTE(a, b) // expand then paste
#define static_assert(x, msg) enum { TOKENPASTE2(ASSERT_line_,__LINE__) \
    = 1 / (msg && (x)) }

static_assert( CONDITION, "This should pass");
static_assert(!CONDITION, "This should fail");

This gives me, with gcc for example, on line 9 of foo.c:

foo.c:9: warning: division by zero [-Wdiv-by-zero]
 static_assert(!CONDITION, "This should fail");
 ^
foo.c:9: error: enumerator value for 'ASSERT_line_9' is not an integer constant
 static_assert(!CONDITION, "This should fail");
 ^~~~~~~~~~~~~

(Here the gcc switch -ftrack-macro-expansion=0 is used, as the extra error messages are not that helpful and just add noise.)

Note that some mangling of the name is still necessary, which you omitted. Here the text ASSERT_line_ is combined with the variable __LINE__. This ensures a unique name, provided:

  • You don't use it twice on a single line.
  • You don't use it in header files (or trust to luck).
  • Your code doesn't happen to use the identifiers like ASSERT_line_9 elsewhere.

For header files, you will need to add somewhere a single word with only identifier characters. For example:

#define static_assert3(x, msg, file) enum { TOKENPASTE2(file,__LINE__) = \
    1 / (msg && (x)) }
#define static_assert(x, msg) static_assert3(x, msg, my_header_h_)

If this fails on line 17, gcc will give an error such as:

 error: enumerator value for 'my_header_h_17' is not an integer constant

An alternative for the mangling in header files is to replace __LINE__ with __COUNTER__. I've not used it, because it is non-standard and because clang was slow to adopt it. But now it has been in gcc, msvc, and clang for about five years.


You could try the same modification with your typedef-array idea, and replace the comma operator with &&. Then your gcc error changes into a warning. For example, modifying your godbolt example to:

typedef char static_assert_2["hello world!" && (CONDITION) ? 1 : -1];

gives the unwanted warning: variably modified 'static_assert_2' at file scope for gcc.

Upvotes: 2

Related Questions