c++ccompiler-errorslvaluecomma-operator

Is `(expression, lvalue) = rvalue` a valid assignment in C or C++? Why do some compilers accept/reject it?


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).


Solution

  • 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++