Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I check that an expression is constant in C? [duplicate]

Say I have a scenario where I need to ensure that a value used in my code is a compile-time constant (e.g. perhaps a draconian interpretation of P10 rule 2 "fixed loop bounds"). How can I enforce this at the language-level in C?

C supports the notion of integer constant expression at the language-level. It must be possible to work out a way to take advantage of this so that only values complying with this specification can be used in expressions, right? e.g.:

for (int i = 0; i < assert_constant(10); ++i) {...

Some partial solutions that aren't really general enough to be useful in multiple situations:

  • Bitfields: a classic strategy for implementing static_assert in C prior to C11 was to use a bitfield whose value would be illegal when a condition failed:

    struct { int _:(expression); }
    

    While this could be easily wrapped for use as part of an expression, it isn't general at all - the maximum value of expression "[may] not exceed the width of an object of the type that would be specified were the colon and expression omitted" (C11 6.7.2.1), which places a very low portable limit on the magnitude of expression (generally likely to be 64). It also may not be negative.

  • Enumerations: an enum demands that any initializing expressions be integer constant expressions. However, an enum declaration cannot be embedded into an expression (unlike a struct definition), requiring its own statement. Since the identifiers in the enumerator list are added to the surrounding scope, we also need a new name each time. __COUNTER__ isn't standardized, so there's no way to achieve this from within a macro.

  • Case: again, the argument expression to a case line must be an integer constant. But this requires a surrounding switch statement. This isn't a whole lot better than enum, and it's the kind of thing you don't want to hide inside a macro (since it will generate real statements, even if they're easy for the optimizer to remove).

  • Array declaration: since C99, the array size doesn't even have to be a constant, meaning it won't generate the desired error anyway. It also again is a statement that requires introducing a name into the surrounding scope, suffering from the same problems as enum.

Surely there's some way to hide the constant check in a macro that's repeatable, passes the value through (so it can be used as an expression), and doesn't require a statement line or introduce extra identifiers?

like image 925
Leushenko Avatar asked Nov 22 '15 03:11

Leushenko


People also ask

What remains constant in C?

There are various types of constants in C. It has two major categories- primary and secondary constants. Character constants, real constants, and integer constants, etc., are types of primary constants. Structure, array, pointer, union, etc., are types of secondary constants.

What is constant identify different types of constants available in C with examples?

Constants of type float, integer, and character are known as Primary constants. Example for Primary constants: 1, 1.23, "Scaler", 'h', etc. As you can see that float, integer, and character are primary constants and we know that float, integer, and character are primary data types.

How do you declare a constant in C?

To make <var_name> a constant, you only need to add the const qualifier to this statement as follows: const <data_type> <var_name> = <value>; Adding the const keyword in the definition of the variable ensures that its value remains unchanged in the program. The const qualifier makes the variable read-only.

What are constant expressions?

A constant expression is an expression that can be evaluated at compile time. Constants of integral or enumerated type are required in several different situations, such as array bounds, enumerator values, and case labels. Null pointer constants are a special case of integral constants.


1 Answers

Turns out there is a way!

Although locally-allocated arrays are allowed to have variable length in C, the standard explicitly requires that such arrays not have an explicit initializer. We can forcibly disable the VLA language feature by giving an array an initializer-list, which will force the array size to be an integer constant expression (compile-time constant):

int arr[(expression)] = { 0 };

The content of the initializer doesn't matter; { 0 } will always work.

This is still slightly inferior to the enum solution because it requires a statement and introduces a name. But, unlike enumerations, arrays can be made anonymous (as compound literals):

(int[expression]){ 0 }

Since a compound literal has an initializer as part of the syntax, there's no way for it to ever be a VLA, so this is still guaranteed to require expression to be a compile-time constant.

Finally, because anonymous arrays are expressions, we can pass them to sizeof which gives us a way to passthrough the original value of expression:

sizeof((char[expression]){ 0 })

This has the added bonus of guaranteeing that the array never gets allocated at runtime.

Finally, with a bit more wrapping up, we can even handle zero or negative values:

sizeof((char[(expression)*0+1]){ 0 }) * 0 + (expression)

This disregards the actual value of expression when setting the array size (which will always be 1), but still considers its constant status; it then also disregards the size of the array and returns only the original expression, so the restrictions on array size - must be greater than zero - don't need to apply to the returned value. expression is duplicated, but that's what macros are for (and if this compiles, it won't be recomputed because a. it's a constant, and b. the first usage is within a sizeof). Thus:

#define assert_constant(X) (sizeof((char[(X)*0+1]){ 0 }) * 0 + (X))

For bonus points, we can use a very similar technique to implement a static_switch expression, by combining array sizes with C11's _Generic (this probably doesn't have many practical uses, but might replace some cases of nested ternaries, which aren't popular):

#define static_switch(VAL, ...) _Generic(&(char[(VAL) + 1]){0}, __VA_ARGS__)
#define static_case(N) char(*)[(N) + 1]

char * x = static_switch(3,
             static_case(0): "zero",
             static_case(1): "one",
             static_case(2): "two",
             default: "lots");
printf("result: '%s'\n", x); //result: 'lots'

(We take the address of the array to produce an explicit pointer-to-array type, rather than let the implementation decide whether _Generic promotes arrays to pointers or not; as of April 2016 this ambiguity was fixed in the language by DR 481 and its ensuing TC.)

This is slightly more restrictive than assert_constant because it won't admit negative values. By putting a +1 in both the controlling expression and all of the case values, though, we can at least let it accept zero.

like image 108
Leushenko Avatar answered Sep 28 '22 01:09

Leushenko