2

Take this code:

#include <stdio.h>

struct S { int x; int y; };

struct S foo(int a, int b) {
    struct S s = { a, b };
    return s;
}

int main() {
    int a;

    a = foo(2, 4).x;
    printf("%d\n", a);
    return 0;
}

It works as intended. What I'm concerned about is the lifetime of the returned struct object. I know the standard talks about temporary lifetime for structs that contain arrays, but in this case there is no array in the structure.

So I guess that as soon as foo() has ended, its return value should be dead, right? But apparently we can still access the x member. Why?

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
Pep
  • 625
  • 4
  • 19
  • 2
    You are returning a copy, that copy's lifetime continues at the calling site – UnholySheep Aug 25 '20 at 09:01
  • 1
    @interjay: Lifetime does have to do with whether the structure contains arrays. C 2018 6.2.4 8 specifically defines a temporary lifetime only for non-lvalue expressions of structures or unions that contain a member with array type: “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*.” – Eric Postpischil Aug 25 '20 at 10:07

4 Answers4

3

"I know the standard talks about temporary lifetime for structs that contain arrays, but in this case there is no array in the structure."

You mean this paragraph:

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.

36) The address of such an object is taken implicitly when an array member is accessed.

Source: ISO/IEC 9899:2018 (C18), §6.2.4/8

The temporary lifetime was explicitly invented in the context of structures and unions which contain array members because since array to pointer decay, accessing an array member by its name would give you a pointer to the first element of the array, which invoked undefined behavior in earlier C standard before this paragraph was added in C11.

Chris Dodd explained it a little better here.

So tempory lifetime (as meant in the standard) isn't relevant for structures with non-array members.

"So I guess that as soon as foo() has ended, its return value should be dead, right?"

No. foo() returns a copy of an object of struct S and not a reference to a local struct S object. Note that the return type of foo() is struct S, not struct S * (a pointer to struct S).

"But apparently we can still access the x member. Why?"

Because you return a copy to the caller. You attempt to access the member x of this copy and not the struct S object s inside of foo().

  • I assume it's a way to make sure the contents of the array are allocated while the expression is being evaluated. I mean, when I get that copy from the returned structure, I have all the information needed in that copy. However, if the returned structure has an array, the only thing that's copied in that copy is the pointer to the first element of the array. There should be a mechanism to make sure the contents of the array keep allocated while evaluating the expression, and this temporary lifetime should do exactly this. Am I right? – Pep Aug 25 '20 at 10:44
  • @PepeDeTicher No. It's a little bit different. A full copy of the entire structure is returned regardless whether the structure contains array member(s) or not when returned by copy/value. The copy contains the whole array too. Not just a pointer to the array member of a structure in `foo()`. -- The "*temporary lifetime*" definition is basically only a rule that allows you to use the pointer gained when you access the array member by its name. It is a little unclear worded and brings some good stuff for confusion but practically it is just a paragraph to allow you to use the gained pointer. – RobertS supports Monica Cellio Aug 25 '20 at 11:04
  • @PepeDeTicher "*There should be a mechanism to make sure the contents of the array keep allocated while evaluating the expression, and this temporary lifetime should do exactly this.*" - Yes, this paragraph describes that the array should keep allocated until the expression is evaluated but the array member is entirely copied into the copy of the returned value. It describes that the returned copy of the structure with the array member(s) in it should keep allocated until the expression is evaluated and done. – RobertS supports Monica Cellio Aug 25 '20 at 11:24
1

The call to foo(2, 4) returns a copy of the variable s inside the function.

This returned (and temporary) copy will have a lifetime to the end of the full expression, which is the assignment a = foo(2, 4).x.

This means that the assignment to a is done before the lifetime of the temporary structure ends, which means that the code you show is fine and valid.

You can read more about lifetime in e.g. this reference.

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • There is no temporary lifetime here because there is no object. C defines a temporary object for structures containing arrays because accessing an array in the structure requires an array-to-pointer conversion. It does not define a temporary lifetime for structures without arrays. The structure returned by this function is just a value, not an lvalue; there is no associated object. The fact an implementation may create a copy in memory is just an implementation detail; it does not constitute an object in the C model. – Eric Postpischil Aug 25 '20 at 10:24
1

The structure returned by foo is just a value (also called an rvalue). It is not an object and does not have any lifetime.

Consider a function int foo(void) { return 3; }. This returns an int value of 3, and we would not expect to be able to take its address, as in printf("%p", (void *) &foo());. The 3 is just a value used in the computer with no associated storage.

Similarly, given struct S { int x, y; }, struct S foo(void) { return (struct S) { 3, 4 }; } returns a struct S value containing 3 and 4. Although we often think of structures as layouts of memory, the C standard treats this return value as just a value. It is a compound value, having multiple parts, but it is just a value with no associated storage. It is not an object in the C model.

Also similarly, given struct S { int x, y[1]; }, struct S foo(void) { return (struct S) { 3, { 4 } }; } returns a struct S value containing 3 and an array containing 4. Here the C standard painted itself into a corner. It wanted to support returning structures from functions, but, when you access an array, as in foo().y[0], the rule currently in C 2018 6.3.2.1 3 says the array is converted to a pointer to its first element. A pointer has to point to storage, so there has to be some object to point to. I suppose one solution might have been to say you cannot use arrays within such structure values individually. (You could use the return value by copying it into an object with struct S x = foo(); and then using x.) However, the solution the C committee adopted was to define a temporary lifetime for such structures. Their definition for that, in C 2018 6.2.4 8, defines a temporary lifetime only for structures and unions that contain an array member.

However, in your code, this is not a concern. Because your foo(2, 4) returns a value, you can use that value as you desire; foo(2, 4).x works because it takes the x member of the value. It does not need to worry about the lifetime of any object because there is no object involved.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • That's what I thought: a mechanism to make sure the contents of the array keep allocated while the expression is being evaluated, right? – Pep Aug 25 '20 at 10:46
  • @PepeDeTicher: Pretty much. I suspect what happened is support for returning structures was added in C implementations, and perhaps in the C standard, and compiler writers made it work, and folks realized there was a problem with arrays in such structures. It would not surprise me if the C standard did not cover this situation, and the compiler writers just made it work because it was sort of the natural thing to do, and the C committee then adopted the rule about temporary lifetime as a fix to patching this gap in the formal specification. – Eric Postpischil Aug 25 '20 at 10:49
  • @PepeDeTicher: That is largely guesswork from my experience in how the world works. Somebody who dives into the history of the standardization process and defect reports might be able to provide better information. – Eric Postpischil Aug 25 '20 at 10:50
  • It makes sense, anyways. – Pep Aug 25 '20 at 11:21
0

From the C Standard

4 A full expression is an expression that is not part of another expression or of a declarator. Each of the following is a full expression: an initializer that is not part of a compound literal; the expression in an expression statement; the controlling expression of a selection statement (if or switch); the controlling expression of a while or do statement; each of the (optional) expressions of a for statement; the (optional) expression in a return statement. There is a sequence point between the evaluation of a full expression and the evaluation of the next full expression to be evaluated.

The lifetime of a temporary object ends when the evaluation of the containing full expression ends.

So within this expression statement

a = foo(2, 4).x;

the returned object of the structure type is alive and the value of its data member x is assigned to the variable a that is declared in main and has the block scope of main.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
  • 1
    There is no temporary lifetime (or “lifetime of a temporary object”) here because temporary lifetimes are defined (in C 2018 6.2.4 8) only for non-lvalue expressions of structures or unions that contain a member with array type. – Eric Postpischil Aug 25 '20 at 10:08
  • @EricPostpischil There is a temporary object returned from the function. If for example you will try to apply the address of operator & to the returned object then the compiler will issue a message that you may not take an address of a temporary object. – Vlad from Moscow Aug 25 '20 at 10:11
  • Clang issues an error message that you may not take the address of an **rvalue**. If other compilers say something about a temporary object, they are defective. Of course, neither the compiler message from Clang nor from other compilers is definitive. The C standard is. The C standard does not define a temporary lifetime for a structure without an array member. The entity returned by `foo` is merely a value, not an lvalue, and has no associated object. You cannot take its address for the same reason you cannot take the address of the return value of a function that returns an `int`. – Eric Postpischil Aug 25 '20 at 10:14
  • @Vlad The thing here is the classification as explicit "*temporary lifetime/object*" as defined in the standard at §6.2.4/8 which only applies to structures and unions with array members. Since the structure has no array member, no such temporary lifetime/object occurs here in the official kind of meaning. – RobertS supports Monica Cellio Aug 25 '20 at 10:15
  • @EricPostpischil There is created a temporary object. Try the following program #include struct A { int x; int y; }; struct A foo( void ) { struct A a = { 0, 0 }; return a; } int main(void) { printf( "%p\n", ( void * )&foo() ); return 0; } You will get a message like this "error: lvalue required as unary ‘&’ operand printf( "%p\n", ( void * )&foo() );" So in the memory there is created a temporary object that is initialized by the returned value of the function. Its lifetime ends when the full expression ends. – Vlad from Moscow Aug 25 '20 at 10:18
  • The error message says you cannot do that; it does not say an object exists. And, again, what compilers say does not matter. What the C standard says does. The C standard says there is a temporary lifetime for structures and unions containing array members. It does not say there is a temporary lifetime for structures and unions without array members. – Eric Postpischil Aug 25 '20 at 10:21
  • @EricPostpischil If the object does not exist you can not assign it to other object moreover when the object has a structure type. The assignment operator is defined for objects of structure types. – Vlad from Moscow Aug 25 '20 at 10:25
  • @EricPostpischil In the C Standard there is a definition of the term object "region of data storage in the execution environment, the contents of which can represent values" – Vlad from Moscow Aug 25 '20 at 10:26
  • @EricPostpischil And the returned type of functions specifies what type has the returned object. So functions if they do not declared with the return type void return objects of the specified type . They are temporary objects and their lifetime ends in the full expression where such an object is used. – Vlad from Moscow Aug 25 '20 at 10:31
  • “If the object does not exist you can not assign it to other object” is false; given `int x`, we can assign `x = 3` even though 3 is not an object. Similarly, given `struct S x`, we can assign `x = foo(2, 4)` even though `foo(2, 4)` is not an object. The right side of an assignment operator may be a value; it is not required to be an lvalue. – Eric Postpischil Aug 25 '20 at 10:39
  • “So functions if they do not declared with the return type void return objects of the specified type” is false. `int foo(void) { return 3; }` returns a value, not an object. – Eric Postpischil Aug 25 '20 at 10:40
  • @EricPostpischil To assign a structure with an expression the expression shall have the type of the structure. An object of a structure shall be stored somewhere it is not stored in the air. – Vlad from Moscow Aug 25 '20 at 10:42
  • @VladfromMoscow: Re “the type of the structure”: The fact that something has a type does not mean it is an object. `3.5` has a type, but it is just a value, not an object. Re “An object of a structure shall be stored somewhere it is not stored in the air”: This is irrelevant. A returned structure value may be stored in registers. Or it may be removed in part by optimization. If it is conveyed in the stack or other memory, that is merely an implementation detail. The C standard does not define it to be an object. – Eric Postpischil Aug 25 '20 at 10:44
  • @EricPostpischil There is a note in the standard that "When referenced, an object may be interpreted as having a particular type;" Literals as for example string literals are also objects though they are named as literals. Any returned by a function value shall be stored somewhere that is in memory. So it is an extent of memory storing a value that is it is an object. – Vlad from Moscow Aug 25 '20 at 10:49
  • @EricPostpischil To apply the member access operator as in this expression foo(2, 4).x; you need an object of the structure type. Otherwise it is unclear to which entity the operator is applied. – Vlad from Moscow Aug 25 '20 at 10:53
  • Re “When referenced, an object may be interpreted as having a particular type”: This says nothing about values being objects. Re “Literals as for example string literals are also objects though they are named as literals”: String literals are entities in source code, not a running program. They result in the creation of objects because C 2018 6.4.5 6 says a string literal in source text causes creation of an array of static storage duration. So the standard explicitly says this object is created. Stop flailing; the standard is clear here. – Eric Postpischil Aug 25 '20 at 10:53
  • @EricPostpischil I have not said that it shall be a lvalue. I said that an object of the structure type shall exist to apply to it the member access operator. – Vlad from Moscow Aug 25 '20 at 10:56
  • Re “To apply the member access operator as in this expression foo(2, 4).x; you need an object of the structure type”: No, you do not. C 2018 6.5.2.3 does not say the operand of `.` must be an lvalue, and 6.5.2.3 3 explicitly allows for it to be not an lvalue. Admittedly 6.5.2.3 3 does say that “A postfix expression followed by the . operator and an identifier designates a member of a structure or union object,” but the following sentence explicitly allows for it to be not an lvalue, so this is clearly a defect in the standard. – Eric Postpischil Aug 25 '20 at 10:58
  • Re “I have not said that it shall be a lvalue. I said that an object of the structure type shall exist to apply to it the member access operator”: C 2018 6.5.2.3 does not specify any such constraint. – Eric Postpischil Aug 25 '20 at 11:00
  • @EricPostpischil There is an example with a phrase "If f is a function returning a structure or union, and x is a member of that structure or union, f().x is a valid postfix expression but is not an lvalue." So this operator accesess a memory that stores the returned structure. The returned structure is an object. It is not a type. It is an entity that stores the returned object. So it is an object: a memory that contains a vaklue of the specified type. – Vlad from Moscow Aug 25 '20 at 11:01
  • @EricPostpischil It is an interesting question. I will try to ask it at ISO. – Vlad from Moscow Aug 25 '20 at 11:03