0

Consider the following code:

const int g_foo = 1;

// (1):
_Static_assert(g_foo > 0, "g_foo > 0");  // error: expression in static assertion is not constant.
_Static_assert(g_foo > 2, "g_foo > 2");  // error: expression in static assertion is not constant.

void Foo(void) {
  const int foo = 1;

  // (2):
  _Static_assert(foo > 0, "foo > 0");  // No issue.
  _Static_assert(foo > 2, "foo > 2");  // error: static assertion failed: "foo > 2"

  // (3):
  _Static_assert(g_foo > 0, "g_foo > 0");  // No issue.
  _Static_assert(g_foo > 2, "g_foo > 2");  // error: static assertion failed: "g_foo > 2"
}

Compiling using gcc -std=c11 -O3 test.c with GCC version 7.4.0 produces the indicated error messages. A Compiler Explorer example using GCC 9.2 can also be found here.

Beginning at the static asserts labeled with (2), we are using a const-qualified local variable. At this optimization level, const int foo = 1; is optimized out and, consequently, the initializer is not evaluated. From what I understand, this classifies it as a constant-expression according to 6.6.3 of ISO/IEC 9899:2011* and allows it to be evaluated by _Static_assert**. So far so good.

The static asserts labeled with (1) attempt to evaluate expressions containing the global variable g_foo, but the fact that globals are allocated space in memory means that they actually need to be initialized. This evaluation automatically disqualifies them from being a constant-expression and GCC complains accordingly.

Then we get to (3). Suddenly, the static asserts that failed in the global scope start working. What's going on?

* I'm unfortunately using the 2010-12-02 committee draft instead of the final published spec.

** As an interesting aside, the use of a nonzero optimization level is important in this question. With -O0, the variable foo actually gets instantiated with a literal 1. However, this is no longer a constant-expression and the static asserts using foo start failing with "expression in static assertion is not constant."


Edits (2019-11-04):

  • Removed *(int *)&g_foo = -1; from the code block - it's undefined behavior and distracts from the main question.
  • Removed a superfluous quip about casting const qualified variables in C.
  • Added a link to Compiler Explorer to help with reproducing the issue.
user2465116
  • 388
  • 2
  • 10
  • *"However, const isn't that strong of a statement in C"* It is in definitions. Your assignment results in undefined behaviour. – user694733 Nov 04 '19 at 08:48
  • Um, casting away the const and modifying the object is explicitly undefined behavior (http://port70.net/~nsz/c/c11/n1570.html#6.7.3p6). Reasoning about such a program is mostly moot. I suggest you remove that bit, because it isn't really the subject of the question, and the UB detracts from what is an interesting case study otherwise. – StoryTeller - Unslander Monica Nov 04 '19 at 08:50
  • “ *(int *)&g_foo = -1;” Why are you doing that? Do not try to be smarter then Compiler. – Michi Nov 04 '19 at 09:02
  • 1
    `this classifies it as a constant-expression according to 6.6p3` - No. This all falls under [6.6.p10](https://port70.net/~nsz/c/c11/n1570.html#6.6p10) - `An implementation may accept other forms of constant expressions`. – KamilCuk Nov 04 '19 at 09:08
  • @user694733, Et Al.: Fair enough. The `const` assignment was a last minute addition to emphasize the weirdness with the static asserts. The fact that it's undefined behavior is clearly detracting form the main point so I've removed it from the question. – user2465116 Nov 04 '19 at 17:49
  • @klutt: I've added a link to Compiler Explorer to aid in reproducing this. However, this is the first time I've linked CE in a Stack Overflow question. Let me know if you can't comment the failing static asserts or something else isn't working. – user2465116 Nov 04 '19 at 18:10
  • @user2465116 Nice. I have added an answer that includes a workaround. I hope you like it. – klutt Nov 04 '19 at 19:09

2 Answers2

4

*(int *)&g_foo = -1; is explicitly undefined behavior as per C17 6.7.3/6:

If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined.

(the C11 draft n1548 says the very same)

Therefore you cannot sensibly reason about the behavior of this code.

Personally I get all 6 asserts to trigger. Tried both with an old gcc 4.8 as well as 9.2/trunk, with or without optimizer enabled. Works fine for me, but apparently you get different results on your local compiler. Such is the nature of undefined behavior.

Lundin
  • 195,001
  • 40
  • 254
  • 396
4

TL;DR

The reason is that a const object is not guaranteed to be set on compile time. For instance, this is perfectly valid:

void foo(int x) {
    const int y = x;
    int z;
    scanf("%d", &z);
    const int t = z+x;

The difference between a constant variable and a regular is that the regular can both be initialized and assigned, while the constant needs to be initialized, which basically means "assigned while declared".

In this particular example, there's no reason for it not to work, since you're using a static (globals are static) variable, and static variables needs a constant expression, just like _Static_assert. This is NOT valid for example:

void foo(int x) {
    static const int y = x; // Will not compile

The reason is that static variables are initialized at compile time. If you don't initialize them explicitly, they get the value zero. This is different from variables with automatic (non-static) storage. They contain garbage unless initialized.

So theoretically, they (who wrote the standard) could have made an exception for static constants, but they didn't. So a const variable does not count as a constant expression. Simple as that. I guess the reason is that they simply did not think it was worth it.

Long and technical version

The C standard requires the first argument of _Static_assert to be a constant expression of integer type.

static_assert-declaration:
         _Static_assert ( constant-expression , string-literal ) ;

https://port70.net/~nsz/c/c11/n1570.html#6.7.10

The standard says this about integer constant expressions:

An integer constant expression shall have integer type and shall only have operands that are integer constants, enumeration constants, character constants, sizeof expressions whose results are integer constants, _Alignof expressions, and floating constants that are the immediate operands of casts.

https://port70.net/~nsz/c/c11/n1570.html#6.6p6

Enumeration constants, character constants, sizeof expressions, _Alignof expressions, and floating constants are obviously not relevant here. That leaves us with only integer constants. And the standard says this about integer constants:

integer-constant:
      decimal-constant integer-suffixopt
      octal-constant integer-suffixopt
      hexadecimal-constant integer-suffixopt
decimal-constant:
      nonzero-digit
      decimal-constant digit
octal-constant:
      0
      octal-constant octal-digit
hexadecimal-constant:
      hexadecimal-prefix hexadecimal-digit
      hexadecimal-constant hexadecimal-digit
hexadecimal-prefix: one of
      0x   0X
nonzero-digit: one of
      1   2   3   4   5   6   7   8   9
octal-digit: one of
      0   1   2   3   4   5   6   7
hexadecimal-digit: one of
      0   1   2   3   4   5   6   7   8   9
      a   b   c   d   e   f
      A   B   C   D   E   F
integer-suffix:
      unsigned-suffix long-suffixopt
      unsigned-suffix long-long-suffix
      long-suffix unsigned-suffixopt
      long-long-suffix unsigned-suffixopt
unsigned-suffix: one of
      u   U
long-suffix: one of
      l   L
long-long-suffix: one of
      ll   LL

https://port70.net/~nsz/c/c11/n1570.html#6.4.4.1p1

Note that the definition of integer constant does NOT include variables declared with const qualifier. Declaring a variable as const only makes it impossible (or should I say illegal) to change it. It does not make it a constant expression. "Why not?" you may rightfully ask. Well I don't know, but I do suspect that there were some obscure historical reason when they standardized C that is not relevant today but still lives on. Remember that when designing API:s!

Therefore, g_foo does not count as a constant expression and the standard does not require any of your asserts to work.

But do note what we can read here:

An implementation may accept other forms of constant expressions.

https://port70.net/~nsz/c/c11/n1570.html#6.6p10

It's possible that it works for 2 and 3 because of compiler extensions. I guess you will have to read the compiler documentation for details or hope that someone else comes up with an answer.

What can you do?

There is a workaround that you can use. Notice above that enumeration constants counts as integer constants and thus also as integer constant expressions. Change

const int g_foo = 1;

to

enum { g_foo = 1; }
klutt
  • 30,332
  • 17
  • 55
  • 95