8

Mind the code

... { int* p = new int(0); std::unique_ptr<int> q(p); ... // make use of 'p' } ... 

In the code above, the unique pointer q is used solely to free p, when time comes. Q is not used by itself.
Since q is never used below the line where it is declared, it can seemingly be released immediately after being declared, thus making use of p "use after free".
The question is q guaranteed to live on until going out of the current scope, or the compiler's optimizer is free to free it before ?

4
  • 3
    The compiler can do any optimization as long as the observable behavior isn't changed - that's the as-if rule. new and delete operators aren't observable behavior since C++14, IIRC. Commented Apr 14, 2020 at 12:17
  • Assuming optimizations are turned on, the compiler probably will do an SSA optimization. It could evaporate q entirely, and inline the destructor in the assembly equivalent of a "finally" block after the last use of q or p. To know for sure, you'll need to look at the optimized assembly of your code with your compiler on your platform. Commented Apr 14, 2020 at 12:33
  • It makes a huge difference if the compiler destructs p after last mention of q or last mention of p :) Of course I could look into the assembly code, but I'm looking for stable behavior over different (including newer) compiler versions. Commented Apr 14, 2020 at 12:37
  • @L.F.: in fact, it is the new expression which can be elided and so can change observable behavior. Commented Apr 14, 2020 at 12:47

2 Answers 2

5

With the as-if rule, compiler is allowed to do any optimization as long as observable behavior is identical.

Freeing immediately q/p would not be allowed, as then you will use dangling pointer.

Though it can call destructor before end of scope:

{ int* p = new int(0); std::unique_ptr<int> q(p); ... // make use of 'p' ... // No longer use of p (and q) ... // Ok, can delete p/q now (as long there are no observable behaviors changes) ... } 

As operator new/delete might be changed globally, compiler would generally not have enough information (linker has though), so consider they have (potentially) observable behaviors (as any external functions).

c++14 allows some elisions/optimisation of new expression, so

{ delete new int(42); int* p1 = new int(0); int* p2 = new int(0); std::unique_ptr<int> q2(p2); std::unique_ptr<int> q1(p1); ... // make use of 'p1'/p2 ... } 

Can be "replaced" by

{ // delete new int(42); // optimized out std::unique_ptr<int[]> qs{new int [] {0, 0}}; // only one allocation instead of 2 int* p1 = q->get(); int* p2 = q->get() + 1; ... // make use of 'p1'/p2 ... } 
Sign up to request clarification or add additional context in comments.

Comments

0

I've realized the answer to my own question:
In the code

{ int* p = new int(0); std::unique_ptr<int> q(p); ... // HERE } 

the destructor of q is guaranteed to be called upon scope exit (HERE).
While the compiler is allowed to allocate and free registers for variables as it pleases, the destructor is guaranteed to be called in a particular place - scope exit.
How do I know ?
Because this is what enables C++ scope guard. Oftentimes the scope guard is used to release a mutex on scope exit - this is something that needs to be guaranteed.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.