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 not0
)a
(for any variablea
, evenconst
-qualified, of value0
)(int*)0
(because the pointer that is converted is notvoid*
)(void const*)0
(for the same reason, qualified versions ofvoid
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.
> 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
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.> 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
Yes, these come closer, though
is_same
tests if an expression isconst
-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.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.
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. Andcommon_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.
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.
Now if
X
is a constant zero expression thenconstzero_helper(0, X)
will call the first overload, but ifX
is an integer expression that is not compile time constant zero, then it will call the second overload becauseX
is not implicitly convertible tovoid *
. Thus, ifX
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 iffX
is compile time constant zero.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 typeint
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.