Home > other >  More questions on the C11 temporary lifetime rule and undefined behaviour
More questions on the C11 temporary lifetime rule and undefined behaviour

Time:02-23

I am struggling to find a definitive answer on yet another example possibly concerning the temporary lifetime rule from C11 / ISO/IEC 9899:2018, which I am quoting here again to facilitate searches:

A non-lvalue expression with structure or union type, where the structure or union contains a member with array type (including, recursively, members of all contained structures and unions) refers to an object with automatic storage duration and temporary lifetime. 36) Its lifetime begins when the expression is evaluated and its initial value is the value of the expression. Its lifetime ends when the evaluation of the containing full expression ends. Any attempt to modify an object with temporary lifetime results in undefined behavior. An object with temporary lifetime behaves as if it were declared with the type of its value for the purposes of effective type. Such an object need not have a unique address.

To begin with, I am not sure if the rule even applies. The example is:

#include <stdio.h>
#include <assert.h>

struct A {
    int b;
};

struct X {
    int x;
    struct A *a;
};

static void
foo(const char *n, struct X *x)
{
    assert(x != NULL);
    assert(x->a != NULL);
    printf("%s = {{ .x = %d, .a[0] = { .b = %d }}}\n", n, x->x, x->a->b);
}

#define FOO(x) foo(#x, x)

void
main(void) {
    struct X x1 = { .x = 11, .a = &(struct A){ .b = 21 }};
    struct X x2 = { .x = 12, .a = (struct A [2]){{ .b = 22 }, { .b = 23 }}};
    FOO(&x1);
    FOO(&x2);

    /* UB? */
    x1.a->b = 31;
    FOO(&x1);
    x2.a[0].b = 32;
    FOO(&x2);

    /* --- */

    struct X *p1 = &(struct X){ .x = 31, .a = &(struct A){ .b = 41 }};
    struct X *p2 = &(struct X){ .x = 32, .a = (struct A [2]){{ .b = 42 }, { .b = 43 }}};

    FOO(p1);
    FOO(p2);

    /* UB? */
    p1->a->b = 51;
    FOO(p1);
    p2->a[0].b = 52;
    FOO(p2);

    /* --- */

    struct A a[2] = {{ .b = 2 }, { .b = 3 }};
    struct X y = { .x = 1, .a = a};

    FOO(&y);

    /* should be legal */
    y.a->b = 4;
    FOO(&y);
}

My questions:

  • To start with the basics, I hope that x1, x2, p1 and p2 indisputably are lvalues and that the modification of y.a->b in the example is legal. Correct?
  • But what about the .a member in the x and p cases? Is it an lvalue when dereferenced?
  • With respect to p6 and p7, does the .a member have a variable length array type? If yes, is that based on the declaration or the initialization?
  • And, consequently, is the behavior of modifications of x1.a->b, x2.a->b, p1->a->b and p2->a->b well defined?

Thank you!

CodePudding user response:

To start with the basics, I hope that x1, x2, p1 and p2 indisputably are lvalues and that the modification of y.a->b in the example is legal. Correct?

Yes.

But what about the .a member in the x and p cases? Is it an lvalue when dereferenced?

Yes. The objects which x1.a, x2.a, p1->a, and p2->a point to are not temporaries. They are compound literals [6.5.2.5]. They are lvalues that are writable, and their lifetime is until exit from the enclosing block, just like an ordinary local variable with auto storage duration. They are alive at every point where you access them.

The passage you quoted from 6.2.4p8 is irrelevant to your code, which does not contain any objects with temporary lifetime. A temporary would be something like the value returned by a function with return type struct A. For instance:

struct A blah(void) {
    struct A ret = { 47, NULL };
    return ret;
}

void other(void) {
    printf("%d\n", blah().x); // valid, prints 47
    blah().x = 15;            // error, blah().x not an lvalue
    int *ptr = &(blah().x);   // error, blah().x not an lvalue
}

The object returned by blah() is not an lvalue, but you can get into a situation of taking the address of such an object when the struct contains an array member. This is why the language about lifetime and modification is there.

struct B {
    int arr[5];
};

struct B foobar(void) {
    struct B ret = { { 0,1,2,3,4 } };
    return ret;
}

void other(void) {
    printf("%d\n", foobar().arr[3]); // valid, prints 3;
    foobar().arr[2] = 7;             // compiles but is UB, temporary may not be modified
    int *ptr = foobar().arr   2;     // valid but useless
    // lifetime of object returned by `foobar()` ends
    printf("%d\n", *ptr);            // UB, lifetime has ended
}

With respect to p6 and p7, does the .a member have a variable length array type? If yes, is that based on the declaration or the initialization?

No. The .a member simply has a pointer type, struct A *. A struct member cannot have a variable length array type; that would be a variably modified type under 6.7.6p3, and that is forbidden for a struct or union member; see 6.7.2.1p9 and note 123.

A variable length array is something like:

void stuff(size_t n) {
    int vla[n];
}

(There is the slightly related concept of a flexible array member, 6.7.2.1p18, which is effectively an array member at the end of a struct, whose size is determined by the amount of additional space you dynamically allocate. But that's a separate question.)

And, consequently, is the behavior of modifications of x1.a->b, x2.a->b, p1->a->b and p2->a->b well defined?

Yes, absolutely.

  • Related