Given the code from here:
class lazy_init
{
mutable std::once_flag flag;
mutable std::unique_ptr<expensive_data> data;
void do_init() const
{
data.reset(new expensive_data);
}
public:
expensive_data const& get_data() const
{
std::call_once(flag,&lazy_init::do_init,this);
return *data;
}
};
And I saw a few variants of the same pattern elsewhere also. So my question is: why this code is considered save? and why compiler can't just read data before calling std::call_once and ends up with an incorrect data? e.g
tmp = data.get();
std::call_once(flag,&lazy_init::do_init,this);
return *tmp;
I mean I have found nothing about any barriers which would prevent that.
Programming in C++ would be essentially impossible if the compiler was allowed to produce code that matched what you describe.
This is stated in §1.9/14 Program Execution (n3290):
Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated.
Your return
statement is sequenced after the preceding full expression. The compiler has to output code as if all side-effects of that preceding statement has been completely evaluated before it evaluates the return statement.
Your example doesn't respect that rule, since it evaluates *data
before taking into account the side-effects of the std::call_once(...)
full expression.
Additionally, std::call_once
has this in its description (§30.4.4.2/2 and 3):
2/ Effects: An execution of call_once that does not call its func is a passive execution. An execution of call_once that calls its func is an active execution. An active execution shall call
INVOKE (DECAY_- COPY ( std::forward<Callable>(func)), DECAY_COPY (std::forward<Args>(args))...)
. If such a call to func throws an exception the execution is exceptional, otherwise it is returning. An exceptional execution shall propagate the exception to the caller of call_once. Among all executions of call_once for any given once_flag: at most one shall be a returning execution; if there is a returning execution, it shall be the last active execution; and there are passive executions only if there is a returning execution. [ Note: passive executions allow other threads to reliably observe the results produced by the earlier returning execution. — end note ]3/ Synchronization: For any given once_flag: all active executions occur in a total order; completion of an active execution synchronizes with the start of the next one in this total order; and the returning execution synchronizes with the return from all passive executions.
So the standard mandates synchronization to fit your use-case.