Does the C standard keep the return struct of a function on the stack if I keep a pointer to a value inside it?
No, not if the struct is a NON l-value, meaning you have not stored it into a variable after it was returned from the function.
In the shown example, does the C standard know to keep the return value of get_x on the stack until the end of the calling stack frame because I'm peeking inside it with a pointer?
No. Read the C standard reference in @dbush's answer.
The problem isn't the get_x()
function--that's all fine. Rather, in the erroneous code in the original question and in Example 1 below, the problem is simply the fact that the returned-by-value struct X
(returned by get_x()
) is NOT an l-value (assigned to a variable), so it is ephemeral, meaning its storage duration ends once the int *p = get_x(argc+50).arr;
line is evaluated. Therefore, the *p
in return *p
is undefined behavior since it accesses memory for a struct X
which was never stored into an l-value and therefore no longer exists. Examples 2 and 3 below, however, solve this problem and exhibit no undefined behavior, and are valid.
Example 1 (from the question; is undefined behavior):
Therefore, this is NOT legal:
int *p = get_x(argc+50).arr;
return *p;
See these warnings output by the clang 11.0.1 LLVM C++ compiler: https://godbolt.org/z/PajThdsxz :
<source>:15:14: warning: temporary whose address is used as
value of local variable 'p' will be destroyed at the end of
the full-expression [-Wdangling]
int *p = get_x(argc+50).arr;
^~~~~~~~~~~~~~
1 warning generated.
ASM generation compiler returned: 0
<source>:15:14: warning: temporary whose address is used as
value of local variable 'p' will be destroyed at the end of
the full-expression [-Wdangling]
int *p = get_x(argc+50).arr;
^~~~~~~~~~~~~~
1 warning generated.
Execution build compiler returned: 0
Program returned: 51
When using the clang 11.0.1 C compiler, however, no such warnings exist: https://godbolt.org/z/Y3zdszMvG. I don't know why.
Example 2 (ok):
But this is fine:
int p = get_x(argc+50).arr[0];
return p;
Example 3 (ok):
...and this is fine too:
struct X x = get_x(argc+50);
int *p = x.arr;
return *p;
Interestingly enough though,the compiled assembly generated by all 3 versions above is exactly identical (only when compiled in C++), indicating that while the first may be undefined, it works just as well as the other two for this particular compiler when compiled in C++. Here is the C++ assembly output:
get_x(int): # @get_x(int)
mov eax, edi
ret
main: # @main
push rax
add edi, 50
call get_x(int)
pop rcx
ret
However, the C-compiler-generated assembly is different for all 3 cases, and significantly longer than the C++-compiler-generated assembly. See the last godbolt link just above to see for yourself.
It looks like the clang C++ compiler is significantly smarter than the clang C compiler.