3

I have two versions of a macro carray_at for bounds-checked array access, but the second one fails to compile with an error, while the first works.

First Macro (Works)

#define carray_at(arr, pos) \ ((((int)(pos) < 0 || (size_t)(pos) >= carray_size(arr)) ? NULL : &((arr).data[pos])) 

Second Macro (Fails)

#define carray_at(arr, pos) \ ((int)(pos) < 0 || (int)(pos) >= carray_size(arr) ? (printf("Index out of bounds: %d\n", (pos)), NULL) : &((arr).data[pos])) 

When used like *carray_at(arr, 1) = 5, it gives:

[root@gxnserver 09:35:03 lattice /$ gcc testa.c testa.c: In function 'main': testa.c:29:5: warning: dereferencing 'void *' pointer 29 | *carray_at(value1, 1) = 5; | ^ testa.c:29:27: error: invalid use of void expression 29 | *carray_at(value1, 1) = 5; | ^ 

Questions: Why does the first macro work while the second fails?

How does the fprintf + comma operator affect type inference in the ternary?

What’s the correct way to add error logging while maintaining dereferencability?

test code as follows:

#include <stdio.h> #include <assert.h> #include <stdbool.h> // define fixed size of array #define DEFINE_FIXED_ARRAY(TYPE, NAME, SIZE) \ typedef struct { \ TYPE data[SIZE]; \ size_t size; \ } NAME; #define carray_size(arr) \ (arr.size) // First Macro (Works) // #define carray_at(arr, pos) \ // ((((int)(pos) < 0 || (size_t)(pos) >= carray_size(arr)) ? NULL : &((arr).data[pos]))) // Second Macro (Fails) #define carray_at(arr, pos) \ ((int)(pos) < 0 || (int)(pos) >= carray_size(arr) ? (printf("Index out of bounds: %d\n", (pos)), NULL) : &((arr).data[pos])) #define ARRAY_SIZE 128 DEFINE_FIXED_ARRAY(int, IntArray, ARRAY_SIZE) int main(){ IntArray value1; *carray_at(value1, 1) = 5; return 0; } 
1
  • ((int)(pos) ???? your size is size_t. Generally do not use macros like this. Commented Mar 25 at 11:11

2 Answers 2

5

You're running into an obscure edge case where the result type of the ?: operator depends on more than just the types of its operands. It also depends on whether an operand is a "null pointer constant".

There are 3 categories of expression defined as a null pointer constant by the C standard:

  1. An integer constant expression with value 0.
  2. Such an expression cast to void *.
  3. nullptr, in C23.

NULL is defined to expand to a null pointer constant. (From the behavior you observed, we can guess it's probably ((void *)0) on your setup.)

(printf(...), NULL) is not a null pointer constant. It doesn't matter that this expression evaluates to exactly the same value as NULL, which is a null pointer constant. Being a null pointer constant is a property of an expression, not a property of a value.


The rules for the result type of the ?: ternary operator are different for null pointer constants than for other expressions. Specifically, if the second and third operands of the ?: operator are a void * and a T* for some non-void type T, the result type depends on whether the void * is a null pointer constant.

If the void * is a null pointer constant, the result type of the ?: is T*. Otherwise, the result type is void *.

So in the version of your code that used a plain NULL, that's a null pointer constant, and the ?: evaluates to the other pointer type. In the version that threw in a printf, you no longer have a null pointer constant, and the ?: evaluates to a void *.

You cannot dereference a void *, hence the error.


For standard references, see the N3220 draft of the C23 standard, section 6.3.2.3 paragraph 3 for the definition of a null pointer constant, and section 6.5.16 paragraph 7 for the behavior of ?: with pointers.

Sign up to request clarification or add additional context in comments.

6 Comments

hey @user2357112, (printf(xxx), NULL) is not a null pointer constant, what datatype is it?
@Blueha: Depends on what NULL expands to. Could be an integer type, could be void *, could be nullptr_t.
I don't understand Depends on what NULL expands to, NULL is expand to (void *) 0?
@Blueha: The standard only says that NULL expands to a null pointer constant. It doesn't say what null pointer constant NULL should expand to. It could be 0. It could be ((void*)0). It could be nullptr. It could be (3 - 4 + 1). On your setup, it seems to be ((void*)0), but it doesn't have to be that.
in my code, NULL is expends to ((void*)0). You just said Depends on what NULL expands to. Does this mean (printf(xxx), NULL) = ((void*)0) = null pointer constant?
|
1

user2357112’s answer is correct, but let’s show the details.

First, let’s reduce the code to a minimum example:

int main(void) { char c; *(1 ? (void *) 0 : &c) = 0; // Line A: Okay. *(1 ? 0, (void *) 0 : &c) = 0; // Line B: Error. } 

Per C 2024 6.3.3.3, (void *) 0 is a null pointer constant:

An integer constant expression with the value 0, such an expression cast to type void *, or the predefined constant nullptr is called a null pointer constant.

Note that null pointer constant is neither a type nor merely a value; it is a specific grammatical construction with a specific value, zero. It is easily seen that (void *) 0 is a null pointer constant. However, 0, (void *) 0 is not a null pointer constant because it is not an integer constant expression or an integer constant expression cast to void *, which is because C 2024 6.6 says:

Constant expressions shall not contain assignment, increment, decrement, function-call, or comma operators, except when they are contained within a subexpression that is not evaluated.

Now we can move on to the conditional operator, ? :. In Line A, the second operand of 1 ? (void *) 0 : &c is a null pointer constant, and the third has type char *, which fits this case in C 2024 6.5.16:

If both the second and third operands are pointers,…; if one operand is a null pointer constant, the result has the type of the other operand…

So the result has the pointer type, char *. Then *(1 ? (void *) 0 : &c) dereferences a char *, and all is fine in Line A.

In Line B, the second operand of 1 ? 0, (void *) 0 : &c is not a null pointer constant. It is merely a value of type void *. This falls into this case in C 2024 6.5.16:

… one operand is a pointer to void or a qualified version of void, in which case the result type is a pointer to an appropriately qualified version of void.

So the type of 1 ? 0, (void *) 0 : &c is void *. Then *(1 ? 0, (void *) 0 : &c) attempts to dereference a void *, producing the error.

Supplement

We could reduce the problem further:

int main(void) { char c; *(1 ? 0 : &c) = 0; // Line A: Okay. *(1 ? 0, 0 : &c) = 0; // Line B: Error. } 

and then the error in Line B is not that the * applies to a void * but that the operands of ? : are an int and a char *, which violates the constraints for ? : in C 2024 6.5.16.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.