Jens Gustedt's Blog

August 22, 2013

testing compile time constness and null pointers with C11’s _Generic

Filed under: C11, C99, language, P99, preprocessor, syntax — Jens Gustedt @ 13:23

Sometimes in C it is useful to distinguish if an expression is an “integral constant expression” or a “null pointer constant”. E.g for an object that is allocated statically, only such expressions are valid initializers. Usually we are able to determine that directly when writing an initializer, but if we want to initialize a more complicated struct with a function like initializer macro, with earlier versions of C we have the choice:

  • Use a compiler extension such as gcc’s __builtin_constant_p
  • We’d have to write two different versions of such a macro, one for static allocation and one for automatic.

In the following I will explain how to achieve such a goal with C11’s _Generic feature. I am not aware of a C++ feature that provides the same possibilities. Also, this uses the ternary operator (notably different in C and C++), so readers that merely come from that community should read the following with precaution.

With a dummy type like this:

typedef struct p00_nullptr_test p00_nullptr_test;

an expression like the following will evaluate to a compile time constant of value 0 or 1, according to whether Y is a nullpointer constant:

_Generic((1 ? (p00_nullptr_test*)0 : (void*)(Y)),
         p00_nullptr_test*: true,
         default: false)

Or this will evaluate myFunc(Y) iff Y is a constant integer expression:

_Generic((1 ? (p00_nullptr_test*)0 : (void*)((Y)-(Y))),
         p00_nullptr_test*: 0,
         default: myFunc(Y))

To explain how such a thing can work, we first have to do a little digression to the mentioned ternary operator. Take the expression in the following as an exercise to test your C skills.

(1 ? (p00_nullptr_test*)X : (void*)Y)

The value of the expression is clearly the value of X, but what is its type?

Probably you figured it out: it depends, more precisely it depends on Y, but not on X. But what is even more curious it doesn’t only depend on the type of Y, which is cast away anyhow, but on its value: if Y is a nullpointer constant, and only then, the return type of the ternary expression is p00_nullptr_test*, otherwise it is void*.

(void*)Y

may or may not be a null pointer constant in the sense of the C standard. If Y is an integer constant of value 0 it is one, if Y is of non-integer and non-void* type or its value is not 0 it is not a null pointer constant.

Permitted for the first case is an integer literal (e.g 0, 0x0, '\0'), an enumeration constant of value 0 (enum { zero }), or a constant expression composed of literals, enumeration constants and sizeof expressions that evaluates to 0.

If Y is any of the following, the result is not a nullpointer constant:

  • 1 (value is not 0)
  • a (for any variable a, even const-qualified, of value 0)
  • (int*)0 (because the pointer that is converted is not void*)
  • (void const*)0 (for the same reason, qualified versions of void are not allowed)

Now let’s go back to our first type generic expression

_Generic((1 ? (p00_nullptr_test*)0 : (void*)(Y)),
         p00_nullptr_test*: true,
         default: false)

and look how it works. As said if Y is a nullpointer constant, the ternary expression has type p00_nullptr_test* and the first choice triggers. In all other cases the ternary has type void* and the default is chosen.

For the second type generic expression an additional reasoning applies. For any arithmetic expression (Y)-(Y) always has value 0, but it only is a nullpointer constant if Y is a constant integral expression.

For P99 I wrap some of this into macros that are ready to use:

P99_GENERIC_INTEGRAL_CONSTANT(EXP, XTRUE, XFALSE)  // is EXP a compile time integer constant
P99_GENERIC_NULLPTR_CONSTANT(PEXP, XTRUE, XFALSE)  // is PEXP 0 or (void*)0 at compile time
P99_GENERIC_PCONST(PEXP, NCONST, CONST)            // is *PEXP const qualified or not

all these accept arbitrary (but valid) expression as their 2nd and 3rd arguments.

The second macro can for example well be used for initialization of a pointer type.

#define TRUC_INITIALIZER(PEXP) {                                                    \
   .someField = 32,                                                                 \
   .pointerField = P99_GENERIC_NULLPTR_CONSTANT(PEXP, (truc*)0, truc_account(PEXP)),\
}

static truc the_static_truc = TRUC_INITIALIZER(0);
auto truc the_auto_truc = TRUC_INITIALIZER(42);

Here the initialization of the static variable will only see constant expressions. The auto in turn will have the function truc_account() called each time the execution reaches that initialization.

If you have other use cases of this idea that are not yet covered by existing macros in P99, please drop me a line.

Advertisements

8 Comments

  1. > I am not aware of a C++ feature that provides the same possibilities.
    If you’re curious, the recommended approach in C++11 is to use nullptr, which has a distinct type std::nullptr_t: http://en.cppreference.com/w/cpp/types/nullptr_t
    Here’s how it was done pre-C++11: http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/nullptr

    Comment by mttpd — August 23, 2013 @ 15:47

    • Thanks for mentioning it, but I am aware of the nullptr feature. What I am not aware of is a feature in C++ that could test if an expression is a null pointer, or any other feature that would allow to statically observe type or compile time constness of an expression.

      Comment by Jens Gustedt — August 23, 2013 @ 16:03

      • > feature that would allow to statically observe type or compile time constness of an expression
        Ah, I see. If I understand you correctly, I think a mixture of std::conditional and std::is_const (on, more generally, std::is_same) may work for this:
        http://en.cppreference.com/w/cpp/types/conditional
        http://en.cppreference.com/w/cpp/types/is_same
        http://en.cppreference.com/w/cpp/types/is_const
        Take a look at the examples — is that what you’re talking about?

        You can also then wrap everything into a compile-time-evaluated function using constexpr:
        http://en.cppreference.com/w/cpp/language/constexpr

        Comment by mttpd — August 23, 2013 @ 16:18

        • Yes, these come closer, though is_same tests if an expression is const-qualified, which is not the same thing. And still I didn’t find that an integral expression is a constant integral expression in the sense of the C standard. (Basically those that you can use for dimensions of static arrays or values of enumeration constants.)

          Generally, C++ doesn’t seem to have something as powerful as _Generic which may at the end evaluate to different type, according to the choice expression. But because it has function overloading, templates and all that stuff it probably doesn’t need it.

          Comment by Jens Gustedt — August 23, 2013 @ 17:05

      • I see the point, I think that means testing for constexpr. This indeed seems to be an open question: http://stackoverflow.com/questions/13299394/is-is-constexpr-possible-in-c11
        Note that in C++11 we’d have to test for constexpr if we want to test for the “can use for dimensions of static arrays or values of enumeration constants” criterion.
        At the same time, it’s a good thing, since it means you can use the results of constexpr functions and expressions for these.
        For instance:
        https://blogs.oracle.com/pcarlini/entry/c_1x_tidbits_introducing_generalized
        http://akrzemi1.wordpress.com/2011/05/06/compile-time-computations/
        In the first example, “float array[square(9)]”, “array” is not a C99 VLA — in contrast, a constexpr function “square” is evaluated at compile time and thus its result can be used for array dimension at compile time as well.

        Regarding expression-dependent type — isn’t that what std::conditional does?
        I know that there’s also std::common_type, which uses ternary operator for compile-time type evaluation — http://en.cppreference.com/w/cpp/types/common_type — however, you’ve mentioned that there are differences between C and C++ here, so it may not be the same thing.

        Comment by mttpd — August 23, 2013 @ 18:01

        • No, if I understand it correctly, the return of std::conditional is a type, not an arbitrary expression of a type that is suitable with respect to the choice argument. And common_type still seems to have a quite different purpose.

          In any case, all that shows that here C and C++ are really different, so this justifies my initial warning in the blog. My personal interest lays in C. Sometimes these things can be helpful for C++ people, too. Here they probably aren’t.

          Comment by Jens Gustedt — August 23, 2013 @ 18:44

  2. I think something like this is possible in C++, at least if for the case when the expression you want to test is of a scalar type. However, I tried it with a compiler and couldn’t get this to work correctly yet.

    Namely, consider the following definitions.

    unsigned char constzero_helper(int p, void *x) { return 0; }
    struct bigstruct { unsigned char arr[32]; };
    struct bigstruct constzero_helper(int p ...) { return bigstruct(); }
    

    Now if X is a constant zero expression then constzero_helper(0, X) will call the first overload, but if X is an integer expression that is not compile time constant zero, then it will call the second overload because X is not implicitly convertible to void *. Thus, if X is an expression of any scalar type (integral, floating point, boolean, pointer, pointer to member), then (1 == sizeof(constzero_helper(0, (int)(bool)(X)))) should be true iff X is compile time constant zero.

    Comment by b_jonas — January 5, 2014 @ 21:14

    • I am really not an expert on these C++ things, so I would probably get this wrong. If you find a way that works, perhaps you might publish it somewhere and then I’ll link to it.

      Intuitively your approach doesn’t seem to work. 0 should always be of type int when it comes to parameter type matches. ... should catch all types that are not covered explicitly. But again, I am not an expert in C++ type conversion rules.

      Comment by Jens Gustedt — January 5, 2014 @ 23:04


RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Create a free website or blog at WordPress.com.