简体   繁体   中英

_Static_assert in unused generic selection

It looks like the typeof operator is likely to be accepted into the next C standard, and I was looking to see if there was a way to leverage this to create a macro using portable ISO-C that can get the length of an array passed into it or fail to compile if a pointer is passed into it. Normally generic selection can be used to force a compiler error when using an unwanted type by leaving it out of the generic association list, but in this case, we need a default association to deal with arrays of any length, so instead I am trying to force a compiler error for the generic association for the type we don't want. Here's an example of what the macro could look like:

#define ARRAY_SIZE(X) _Generic(&(X), \
        typeof(&X[0]) *: sizeof(struct{_Static_assert(0, "Trying to get the array length of a pointer"); int _a;}), \
        default: (sizeof(X) / sizeof(X[0])) \
)

The problem is that _Static_assert is tripping even when the generic association selected is the default association. For sake of simplicity, since the issue at hand is not related anything being introduced in C23, we'll make a test program that works explicitly to reject a pointer to int:

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_SIZE(X) _Generic(&(X), \
        int **: sizeof(struct{_Static_assert(0, "Trying to get the array length of a pointer"); int _a;}), \
        default: (sizeof(X) / sizeof(X[0])) \
)

int main(void) {

    int x[100] = {0};
    int *y = x;
    int (*z)[100] = {&x};

    printf("length of x: %zu\n", ARRAY_SIZE(x));
    printf("length of y: %zu\n", ARRAY_SIZE(y));
    printf("length of z: %zu\n", ARRAY_SIZE(z));
    printf("length of *z: %zu\n", ARRAY_SIZE(*z));
    return EXIT_SUCCESS;

}

Building the above with -std=c11 , I find _Static_assert tripping on all expansions of ARRAY_SIZE when I would expect to only have problems with the pointers that will use the int ** generic association.

According to 6.5.1.1 p3 of the C11 standard for Generic Selection,

None of the expressions from any other generic association of the generic selection is evaluated

Is this a bug in gcc and clang, or is there something I've missed in the standard that would cause the compile-time evaluation of this _Static_assert in the unused generic association?

It doesn't matter which generic selection is evaluated.

When the expression that is part of a _Status_assert has the value 0, this is considered a constraint violation and the compiler is required to generate a diagnostic.

You can't really mix _Static_assert with expressions that should return a value, such as a function-like macro. You could perhaps work around that with a "poor man's static assert", like one of the ugly tricks we used before C11:

#define POOR_STATIC_ASSERT(expr) (int[expr]){0}

#define CHECK(X) _Generic((&X), \
        int **: 0,\
        default: (sizeof(X) / sizeof(X[0])) \
)

#define ARRAY_SIZE(X) ( (void)POOR_STATIC_ASSERT(CHECK(X)), CHECK(X) )

Here the comma operator is called to have the macro CHECK return the size or zero, in case a type is valid or not. Then call the same macro again to have that one returned from the function-like macro ARRAY_SIZE . This will lead to some cryptic error from an ISO C compiler such as "error: ISO C forbids zero-size array".


The next problem is that &(X) in _Generic is by no means guaranteed to boil down to a int** so this macro isn't safe or reliable. Regarding array sizes though, there's a trick we can use. A pointer to an array of no size (incomplete type) is compatible with every array of the same element type no matter it's size. The macro could be rewritten as:

#define POOR_STATIC_ASSERT(expr) (int[expr]){0}

#define CHECK(X) _Generic((&X),              \
        int (*)[]: sizeof(X) / sizeof(X[0]), \
        default: 0)

#define ARRAY_SIZE(X) ( (void)POOR_STATIC_ASSERT(CHECK(X)), CHECK(X) )

This will work for any int array no matter size but fail for everything else.

Utilizing some of the suggestions from Lundin's answer, I have come up with the following solution to the simplified problem:

#define STATIC_ASSERT_EXPRESSION(X, ERROR_MESSAGE) (sizeof(struct {_Static_assert((X), ERROR_MESSAGE); int _a;}))

#define NO_POINTERS(X) _Generic(&(X), \
    int (*)[]: 1, \
    default: 0 \
)

#define ARRAY_SIZE(X) ( (void)STATIC_ASSERT_EXPRESSION(NO_POINTERS(X), "Cannot retrieve the number of array elements from a pointer"), (sizeof(X) / sizeof(X[0])) )

For the actual use-case to be type generic using typeof, which should be coming to the C23 standard, replace the NO_POINTERS macro using this:

#define NO_POINTERS(X) _Generic(&(X), \
    typeof(*X) (*)[]: 1, \
    default: 0 \
)

By moving the _Static_assert outside of the Generic Selection, it will only be evaluated with the value that actually returns from the selection, so it won't get fired off for existing in an unused selection. Additionally, the number of elements calculation was also removed from the generic selection so that expression of the generic selection could safely be used in a Static Assert even if your array was a Variable-Length array which requires its size to be calculated at run-time.

The Static Assert itself is placed inside of an anonymous struct that we take the sizeof so that it is a part of an expression. And then as in Lundin's example, we use the comma operator to have that expression evaluated, and then thrown out and use the results of the array size calculation.

With this, we reject pointers while getting the number of elements in both static arrays and VLAs, plus we get a nice compiler error message when trying to pass in a pointer.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM