4

Is there a way in C to force a function caller to send a constant value (expect compilation error if not) to the function?

for example, consider this simple function:

int foo(int x, int y)
{
    assert( x < 10);
    return x+y;
}

if I can somehow tell the compiler that x should be sent with a constant value in all cases, the assert can be evaluated at compile time (even if the assert is actually called only in run time). Is there a way to do so?

Just to explain the motivation - in low resources systems having things evaluated at compile time can lower SW footprint and execution time significantly.

Thanks.

François Andrieux
  • 28,148
  • 6
  • 56
  • 87
Hanans
  • 93
  • 5

1 Answers1

5

First and foremost, here it is, a fully standard C11 solution:

#include <stdint.h>
#include <assert.h>

#define is_integral_constant_p(c) (_Generic(0 ? (void*)(uintptr_t)((c) - (c)) : (int*)(0), \
                                  int*: 1,                                            \
                                  default: 0)) 


#define foo(x, y) (is_integral_constant_p(x) ? real_foo : error_foo_x_is_not_a_constant_expression)(x, y)

extern int error_foo_x_is_not_a_constant_expression(int, int);
int real_foo(int x, int y)
{
    assert(x < 10);
    return x+y;
}

int main(void)
{
    static_assert(is_integral_constant_p(5), "5 is not a constant expression?");
    foo(5, 1);

    // int x = 0;
    // foo(x, 1);  // This will not link
}

The magic is in how the conditional expression works. If one of the operands is a null pointer constant, the type of the result is the type of the other pointer argument. Otherwise, if one argument is a void* the result is void*. Since (void*)(0) is a null pointer constant, the second operand is a null pointer constant only when (c) - (c) is an integral constant expression equal to 0. Which happens iff c is an integral constant expression.

The _Generic just selects a constant based on the type of the result, which we expect to be either int* in the true case, or void* otherwise. What's more the macro itself evaluates to an integral constant expression (and so could even be used in an assert).

The above is an adaptation of a technique used in the linux kernel (as discussed here), only without and GNU specific extension to C. Only C11 features are used.

Now, we implement foo as a macro that does our check on x, and either forwards to the "real_foo" that does the calculation, or to a function that is declared but not defined. So the program fails to link if x is not a constant.

See it live here.


As for your particular case, you can remove the assertion entirely and move the check into the foo macro. It would look something like this:

#define foo(x, y) (is_integral_constat_p(x) && (x < 10) ? real_foo : \
                   error_foo_x_is_not_a_constant_expression_or_more_than_10)(x, y)

The macro, combined with short circuit evaluation gets us the behavior you want. As you can see here.

At this point I feel I should caution you. This code may not be so straight-forward to protect in code review. So think carefully about whether or not any performance gain you may get is worth the explanations.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 1
    `_Generic` also lets you enforce assertions against the value by selecting pointer-to-array types (that differ in their element count), but I don't think you can easily combine that with a *readable* assertion against const-ness itself, because the assertion that uses the ICE will fail first. – Alex Celeste Oct 15 '18 at 15:20
  • @Leushenko - Yes. That's why I opted for a mere linker error. Though I'd be happy to see an improvement to this, if at all possible. – StoryTeller - Unslander Monica Oct 15 '18 at 15:21
  • Note: Interesting that `foo(5.1, 1)` as in `(void*)(uintptr_t)((5.1) - (5.1))` does not certainly form a _null pointer constant_. I suspect that is compiler dependent. As I see it, the "fully standard C11 solution:" relies on a compiler optimizing `(c) - (c)` to a constant 0. This may be something select compilers do, yet I see no requirement for it. – chux - Reinstate Monica Oct 15 '18 at 16:36
  • @chux - I don't follow. Are you saying some compilers accept that errounesly? – StoryTeller - Unslander Monica Oct 15 '18 at 16:45
  • No - more musing. I'm trying to determine if `(uintptr_t)((5.1) - (5.1))` is a "integer constant expression with the value 0", given the cast. If that was valid, why `foo(5.1, 1)` fails and `foo(5, 1)` works. Hmmm..... – chux - Reinstate Monica Oct 15 '18 at 16:47
  • 1
    @chux - If I [read correctly](https://port70.net/~nsz/c/c11/n1570.html#6.6p6), since `5.1` is not the immediate operand of the cast, it should not be an integer constant expression. – StoryTeller - Unslander Monica Oct 15 '18 at 16:53
  • I don't get it. Why does `0 ? (void*)(uintptr_t)((c) - (c)) : (int*)(0), ` compile with different types left and right of ':' and why does it matter how the left expression (before ':') looks like - when the condition `0` is always false and the right expression should always be taken? – Werner Henze Oct 17 '18 at 06:51
  • @WernerHenze - Follow the link in my answer. It has the complete explanation and references. No point repeating them in the comment section when there is a fantastic SO post dedicated to it. – StoryTeller - Unslander Monica Oct 17 '18 at 06:52