Some time ago I stumbled upon the idea of how a C construct, such as (expr0, expr1, expr2)
, evaluates (see "What does the comma operator , do?" for more context).
I've started experimenting with this, especially inside of function-like macros, and recently found a code which is rejected by some compilers, while accepted by others. It looks similar to the following snippet:
#include <stdio.h>
int main(void)
{
int arr[] = {0};
(1, arr[0]) = 30; // <--- potentially (in)valid code
printf("%d\n", arr[0]);
return 0;
}
As you can see, in order for this to work, (1, arr[0])
must be evaluated to lvalue arr[0]
, otherwise assignment would NOT be possible. However, I'm not sure if said behavior is valid or not. It "makes sense" and I found a use for it, but I also see why compiler devs would reject it.
The code above is rejected by gcc, clang and msvc (note that msvc is primarily a C++ compiler, while gcc and clang are C front-ends):
$ gcc main.c
main.c: In function ‘main’:
main.c:6:21: error: lvalue required as left operand of assignment
6 | (1, arr[0]) = 30;
| ^
$ clang main.c -Wno-unused-value
main.c:6:14: error: expression is not assignable
6 | (1, arr[0]) = 30;
| ~~~~~~~~~~~ ^
1 error generated.
$ cl main.c /nologo
main.c
main.c(6): error C2106: '=': left operand must be l-value
For comparison, g++, clang++ and tcc are fine with said code (note that tcc is a C compiler, while g++ and clang++ are C++ front-ends):
$ tcc main.c && ./a.out
30
$ g++ main.c && ./a.out
30
$ clang++ main.c -Wno-unused-value -Wno-deprecated && ./out
30
I also tried with some different command options, such as explicitly setting msvc to running in either /std:c++latest
and /std:c99
modes, or by setting different -std
for gcc/clang/g++/clang++ but it did not change anything.
At first, I thought it's a bug within tcc, since it's the only C compiler which does not reject the "faulty" code, but then I checked C++ front-ends and I'm not so sure about it any more. Especially since msvc rejects it unlike g++/clang++.
For reference, I'm on x86_64 Linux, using gcc/g++ 14.2.1, clang 18.1.8, msvc 19.40.33811 (running through wine), and tcc 0.9.28rc (mob@08a4c52d).
In C, the code is not valid. As N3096 6.5.17 says,
Then the right operand is evaluated; the result has its type and value.129
Note 129 says:
A comma operator does not yield an lvalue.
So, it is a "non-lvalue" (i.e. r-value) and cannot be used as an l-value.
For C++, on the other hand, per [expr.comma]
The type and value of the result are the type and value of the right operand; the result is of the same value category as its right operand,
Since it's the same value category as the right operand, it would be an l-value and would be valid.
Note that your code compiles fine in MSVC as a C++ file. MSVC, or more specifically CL, will treat .c files as C, and .cpp files as C++ by default. See here where the left compiler is MSVC forced to compile as C code, and the right is MSVC compiling as C++