The entire point of constexpr
is so that it can be evaluated at compile time. Since variables aren't actually instantiated at that point, they won't have an address.
That's aside from the point that this type of RNG, when done in a way that the addresses can be calculated at run-time, will likely have a very limited range, even probably even the same value when calling multiple times from the same function.
For the language lawyers, [basic.stc]
(C++20 in this case) has this to say (my emphasis):
When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values (6.7.2). Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior.
That means any use of the rng()
return value &c
(other than dereferencing or deallocation) is subject to the whims of the implementation, since the lifetime of c
is over.
Interestingly, while gcc
documents a few implementation-defined items, this does not appear to be one of them (clang
isn't any better, by the way). This is despite the fact it shows up in the standard as a specific item, any use of an invalid pointer other than to perform indirection or deallocate
, in the "Index of implementation-defined behavior".
It's interesting that a slight modification to code will result in different code being generated by gcc
:
void const * rng() noexcept { char c; return &c; }
// rng():
// push rbp ; Stack set-up.
// mov rbp, rsp
//
// mov eax, 0 ; Return zero, always.
//
// pop rbp ; Stack tear-down.
// ret
void const * rng() noexcept { char c; char *x = &c; return x; }
// rng():
// push rbp ; Stack set-up.
// mov rbp, rsp
//
// lea rax, [rbp-9] ; Get address of c.
// mov QWORD PTR [rbp-8], rax ; Put into x.
// mov rax, QWORD PTR [rbp-8] ; Return x.
// pop rbp ; Stack tear-down.
// ret
In the second definition, you don't get the warning about returning the address of a local variable, and you do get a non-zero value when run.
Unfortunately, that's only anecdotal evidence of what gcc
does, so cannot be guaranteed. You also get the same value every time you call the function from the same stack "level" in a single program run.
You'll also notice there the absence of constexpr
above. Adding that makes no difference to the outcome other than the fact you seem to need to initialise c
to some value. In other words, the trick that prompted this question doesn't always work even without using constexpr
(another reason to not rely on it).
The clang
compiler appears to handle the case without the temporary pointer (but with the same non-random results, of course):
void const * rng() noexcept { char c; return &c; }
// rng():
// push rbp ; Stack set-up.
// mov rbp, rsp
// lea rax, [rbp - 1] ; Return address.
// pop rbp ; Stack tear-down.
// ret
Bottom line: though an implementation should document it, it is still free to do whatever it wants. That is, after all, what "implementation-defined" means. It's just that the "defined" bit is missing :-)
That includes gcc
returning zero in the case where you attempt to return the address directly, and a non-zero value if you first put it into a pointer variable.
Re your comment:
I am not really looking for a rng, just got the idea for this question from the trick.
Even if you remove all my comments about the usefulness of this trick for random number generation, the fact that it doesn't work (and is not guaranteed to work) in all situations makes it non-portable and therefore potentially incorrect code.