Compound literal lifetime and if blocks
Asked Answered
W

1

25

This is a theoretical question, I know how to do this unambiguously, but I got curious and dug into the standard and I need a second pair of standards lawyer eyes.

Let's start with two structs and one init function:

struct foo {
    int a;
};
struct bar {
    struct foo *f;
};
struct bar *
init_bar(struct foo *f)
{
    struct bar *b = malloc(sizeof *b);
    if (!b)
        return NULL;
    b->f = f;
    return b;
}

We now have a sloppy programmer who doesn't check return values:

void
x(void)
{
    struct bar *b;

    b = init_bar(&((struct foo){ .a = 42 }));
    b->f->a++;
    free(b);
}

From my reading of the standard there's nothing wrong here other than potentially dereferencing a NULL pointer. Modifying struct foo through the pointer in struct bar should be legal because the lifetime of the compound literal sent into init_bar is the block where it's contained, which is the whole function x.

But now we have a more careful programmer:

void
y(void)
{
    struct bar *b;

    if ((b = init_bar(&((struct foo){ .a = 42 }))) == NULL)
        err(1, "couldn't allocate b");
    b->f->a++;
    free(b);
}

Code does the same thing, right? So it should work too. But more careful reading of the C11 standard is leading me to believe that this leads to undefined behavior. (emphasis in quotes mine)

6.5.2.5 Compound literals

5 The value of the compound literal is that of an unnamed object initialized by the initializer list. If the compound literal occurs outside the body of a function, the object has static storage duration; otherwise, it has automatic storage duration associated with the enclosing block.

6.8.4 Selection statements

3 A selection statement is a block whose scope is a strict subset of the scope of its enclosing block. Each associated substatement is also a block whose scope is a strict subset of the scope of the selection statement.

Am I reading this right? Does the fact that the if is a block mean that the lifetime of the compound literal is just the if statement?

(In case anyone wonders about where this contrived example came from, in real code init_bar is actually pthread_create and the thread is joined before the function returns, but I didn't want to muddy the waters by involving threads).

Winni answered 19/1, 2016 at 15:27 Comment(1)
@Winni Please not that my answer had some incorrect statements(no pun intended), and I fixed them. The outcome of the answer is still the same. See revisions: stackoverflow.com/posts/34880981/revisionsEveryplace
E
8

The second part of the Standard you quoted (6.8.4 Selection statements) says this. In code:

{//scope 1

    if( ... )//scope 2
    {

    }//end scope 2

}//end scope 1

Scope 2 is entirely inside scope 1. Note that a selection statement in this case is the entire if statement, not just the brackets:

if( ... ){ ... }

Anything defined in that statement is in scope 2. Therefore, as shown in your third example, the lifetime of the compound literal, which is declared in scope 2, ends at the closing if bracket (end scope 2), so that example will cause undefined behavior if the function returns non-NULL (or NULL if err() doesn't terminate the program).

(Note that I used brackets in the if statement, even though the third example doesn't use them. That part of the example is equivalent to this (6.8.2 Compound statement):

if ((b = init_bar(&((struct foo){ .a = 42 }))) == NULL)
{
    err(1, "couldn't allocate b");
}
Everyplace answered 19/1, 2016 at 15:44 Comment(5)
Yeah, I guess it is that simple. This is quite surprising though. A little gotcha I've never thought about before. Not that it happens often, but I've been tempted to call pthread_create this way a few times.Winni
So this is yet another scenario where it's necessary for code to use operators like && or ?: to do things which should "normally" be done with if but can't [e.g. fix the above as (b = init_bar(&((struct foo) { .a = 42 }))) && err(1, "couldn't allocate b");]? Or else don't bother with C11's mis-designed compound literals?Deer
@Deer This can very well be done with an if statement and blaming compound literals is shifting the blaim away from the programmer. In my opinion calling function inside the if statement is code smell. The correct version would be struct bar* b = b = init_bar( &(struct foo){ .a = 42 } ); if( b ){...Everyplace
@2501: I used that particular example because that's what the answer above used, but a more relevant use would be situations where code will need to use an object identified by a pointer if non-null, or make a default object if it was null. If code using compound literals would need to, at an outer scope, say struct foo DefaultObject; and then later say if (!p) { DefaultObject = (struct foo) { .a = computedValue }; p = &DefaultObject;} that doesn't really offer much benefit versus if (!p) { DefaultObject.a = computedValue; p = &DefaultObject;}, and in cases where...Deer
...everything in the default object is constant it would be inferior to having DefaultObject be static. I could see usefulness to having a syntax which would guarantee that the lifetime of such an object would last until execution leaves the syntactically-containing function or the compound literal is re-executed in the same syntactic context, whichever happens first, and I could see usefulness to also having a syntax that would allow the object's lifetime to be like that of a structure returned by a function. Extending only to an enclosing block, however, makes things brittle.Deer

© 2022 - 2024 — McMap. All rights reserved.