Search code examples
c99postfix-operatorcompound-literalsprefix-operator

Using postfix/prefix increment operators in compound literals in C99


There is an example borrowed from CARM (C A Reference Manual, Samuel P. Harbison III, Guy L. Steele Jr., 2002, Prentice Hall), page 218-219. I write all of three code-chunk in one source:

#include <stdio.h>

void f1(){
    int *p[5];
    int i=0;
    m:
    p[i]=(int [1]){i};
    if(++i<5)goto m;
    printf("f1: ");
    for(i=0;i<5;++i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("\n");
    fflush(stdout);
}

void f2(){
    int *p[5];
    int i=0;
    p[i]=(int [1]){i++};
    p[i]=(int [1]){i++};
    p[i]=(int [1]){i++};
    p[i]=(int [1]){i++};
    p[i]=(int [1]){i++};
    printf("f2: ");
    for(i=0;i<5;++i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("\n");
    fflush(stdout);
}

void f3(){
    int *p[5];
    int i;
    for(i=0;i<5;i++){
        p[i]=(int [1]){i};
    }
    printf("f3: ");
    for(i=0;i<5;++i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("\n");
    fflush(stdout);
}

int main(){ f1(); f2(); f3(); }

f2 function doesn't work properly:

user@debian:~/test7$ gcc -std=c11 ./carm_1.c -o carm_1
user@debian:~/test7$ ./carm_1
f1: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4 
f2: p[0]=-2106668384 p[1]=-2106668408 p[2]=32765 p[3]=2 p[4]=3 
f3: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4

But when i rewrote it:

#include <stdio.h>

void f1(){
    int *p[5];
    int i=0;
    m:
    p[i]=(int [1]){i};
    if(++i<5)goto m;
    printf("f1: ");
    for(i=0;i<5;++i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("\n");
    fflush(stdout);
}

void f2(){
    int *p[5];
    int i=-1;
    p[i]=(int [1]){++i};
    p[i]=(int [1]){++i};
    p[i]=(int [1]){++i};
    p[i]=(int [1]){++i};
    p[i]=(int [1]){++i};
    printf("f2: ");
    for(i=0;i<5;++i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("\n");
    fflush(stdout);
}

void f3(){
    int *p[5];
    int i;
    for(i=0;i<5;i++){
        p[i]=(int [1]){i};
    }
    printf("f3: ");
    for(i=0;i<5;++i)
        printf("p[%d]=%d ",i,*(p[i]));
    printf("\n");
    fflush(stdout);
}

int main(){ f1(); f2(); f3(); }

f2 function works fine:

user@debian:~/test7$ gcc -std=c11 ./carm_2.c -o carm_2
user@debian:~/test7$ ./carm_2
f1: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4 
f2: p[0]=0 p[1]=1 p[2]=2 p[3]=3 p[4]=4 
f3: p[0]=4 p[1]=4 p[2]=4 p[3]=4 p[4]=4

Why ? These two variants of f2 function differs in value returned by postfix/infix increment of i (in compound literal). In the first case it is temporary value. The result of postfix increment operator is not lvalue. And the result of prefix increment operator is also not lvalue (in according to page 226 of CARM). Please, help me to understand. (sorry for my english).


Solution

  • I don't think this is an issue about lvalues and temporaries; but rather an unrelated bug in H&S's example.

    In the statement p[i]=(int [1]){i++};, there is a tricky question as to whether there is a sequence point after the i++, which seems to hinge on whether i++ is a full expression. In C17 it is explicitly stated that an initializer that is not part of a compound literal is a full expression, which would seem to imply that the i++ is not a full expression and that there is no sequence point. In that case, the statement in question would be undefined behavior, as would the p[i]=(int [1]){++i}; in your version.

    However, C99 doesn't seem to have had the "not part of a compound literal" exception, so I'm not quite sure what the situation was there. But even if there is a sequence point after the side effect of i++, still as far as I know, the order of evaluation of the left and right sides of = is unspecified. So if the compiler chooses to evaluate the right side first (including its side effects), the statement becomes p[1] = (int [1]){0}; and leaves p[0] uninitialized, causing undefined behavior when it is dereferenced. By the same token, the last statement becomes p[5] = (int [1]){4} which is also UB since p is of length 5.

    For a compiler that consistently chooses that ordering, your code would work; but for a compiler that chose the other order, your code could become p[-1] = (int [1]){0} which is likewise UB. So I don't think your version is strictly correct either.

    H&S probably should not have tried to be so clever, and just written

    int i=0;
    p[i] = (int [1]){i};
    i++;
    p[i] = (int [1]){i};
    i++;
    p[i] = (int [1]){i};
    i++;
    p[i] = (int [1]){i};
    i++;
    p[i] = (int [1]){i};
    i++;
    

    Then the code would be correct and would do what they say: p[0], ..., p[4] contain five different pointers, all pointing to ints whose lifetimes continue through the printf loop, and such that *(p[0]) == 0, *(p[1]) == 1, etc.