Search code examples
c++c++11memory-fences

std::call_once and memory reordering


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.


Solution

  • 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.