There is no leak here. In general an object owned by a std::unique_ptr is never leaked, as long as .release() is not called on the owning std::unique_ptr.
Your program does however have undefined behavior because the lifetimes are not correct. If you store a reference or pointer to the owned object you take over some responsibility to assure correct object lifetimes.
give_a_number() returns, by-value, a std::unique_ptr owning the int object. This std::unique_ptr is therefore materialized as a temporary object in the statement
int& ret = *give_a_number();
You don't move this temporary into any persistent std::unique_ptr instance. So when the temporary object is destroyed at the end of the statement, the int it owns is destroyed as well. Now your reference is dangling.
You then use the dangling reference in return ret;, causing undefined behavior.
You can store a reference to the owned object, but if you do so, you must assure that the std::unique_ptr instance owning the reference out-lives the int reference to avoid lifetime issues. E.g. the following are fine:
int main(){
return *give_a_number(); // `std::unique_ptr` temporary outlives return value initialization
}
int main(){
auto ptr = give_a_number(); // `ptr` lives until `main` returns
int& ret = *ptr;
return ret;
}
Usually it is safer to just store the smart pointer in an automatic variable as above and obtain the object by dereferencing with *ptr, where needed. The std::unique_ptr and the object it owns then live until the end of the block, if it is not moved from.
But it is perfectly correct to e.g. pass *ptr to a function by-reference. Since ptr outlives such a function call, there will not be any problem.
This also works if you pass *give_a_number() directly as argument to a function, because the std::unique_ptr temporary will outlive such a call as well. But in that case the std::unique_ptr and the object it owns will live only until the end of the statement (full-expression), not the end of the block.