Skip to content
C++
Library
since C++11
Intermediate

std::call_once and std::once_flag

Thread-safe one-time initialization in C++11 with std::call_once and std::once_flag, including exception semantics, lazy init patterns, and comparison with function-local statics.

std::call_oncesince C++11

A function template in <mutex> that, given a std::once_flag, guarantees a callable is successfully executed exactly once across all threads that invoke it with that flag, with all other callers blocking until the winning execution completes.

Overview

std::call_once and std::once_flag (both C++11, header <mutex>) are the standard primitives for one-time initialization in multithreaded code. They replace hand-rolled double-checked locking, which is easy to get wrong without correct atomic memory ordering.

The contract has three parts:

  • Exactly one successful execution. Among all calls sharing a once_flag, one callable runs to completion; the rest return without invoking theirs.
  • Synchronization. The completion of the successful execution synchronizes-with the return of every other call_once on that flag. Writes made by the initializer are visible to every thread that returns from call_once.
  • Exception retry. If the callable exits via an exception, the call is not counted. The exception propagates to the caller, the flag stays unset, and another thread (or a later call) may try again. This is called an exceptional execution.

A once_flag is default-constructible, non-copyable, and non-movable. Its default constructor is constexpr, so a namespace-scope once_flag is constant-initialized and there is no static-initialization-order problem for the flag itself.

Syntax

cpp
#include <mutex>

namespace std {
    struct once_flag {
        constexpr once_flag() noexcept;          // C++11
        once_flag(const once_flag&) = delete;
        once_flag& operator=(const once_flag&) = delete;
    };

    template <class Callable, class... Args>
    void call_once(once_flag& flag, Callable&& f, Args&&... args);   // C++11
}

Arguments are decay-copied and invoked as if by std::invoke (the std::invoke wording applies since C++17; in C++11/14 the equivalent INVOKE was specified). Member-function pointers work: pass the object pointer as the first argument.

Behavior was clarified in C++17/C++20 wording (the return of the callable is ignored, and std::system_error is thrown if the call cannot be carried out). Early libstdc++ implementations on some platforms depended on pthread_once and required linking with -pthread; always link the thread library explicitly.

Examples

Lazy member initialization

cpp
#include <mutex>
#include <string>
#include <fstream>
#include <sstream>

class Config {
public:
    const std::string& text() const {
        std::call_once(loaded_, [this] { load(); });   // C++11
        return text_;
    }

private:
    void load() const {
        std::ifstream f("app.conf");
        if (!f) throw std::runtime_error("config missing"); // flag stays unset
        std::ostringstream ss;
        ss << f.rdbuf();
        text_ = ss.str();
    }

    mutable std::once_flag loaded_;
    mutable std::string text_;
};

Because once_flag is neither copyable nor movable, Config is also neither copyable nor movable. If you need value semantics, hold the state behind a std::unique_ptr or implement custom copy operations that default-construct a fresh flag.

Per-resource initialization shared across worker threads

cpp
#include <mutex>
#include <thread>
#include <vector>
#include <iostream>

std::once_flag g_logInit;

void init_logging() {
    std::cout << "init on thread " << std::this_thread::get_id() << '\n';
    // open sinks, install handlers...
}

void worker(int id) {
    std::call_once(g_logInit, init_logging);
    // Safe: logging is fully initialized here, on every thread.
    std::cout << "worker " << id << " running\n";
}

int main() {
    std::vector<std::thread> ts;
    for (int i = 0; i < 4; ++i) ts.emplace_back(worker, i);
    for (auto& t : ts) t.join();
}

Compile with -pthread on Linux.

Retry after failure

cpp
#include <mutex>
#include <stdexcept>
#include <atomic>

std::once_flag flag;
std::atomic<int> attempts{0};

void flaky_init() {
    if (++attempts < 3) throw std::runtime_error("transient");
    // succeeds on the third attempt
}

void ensure_init() {
    for (;;) {
        try {
            std::call_once(flag, flaky_init);
            return;
        } catch (const std::runtime_error&) {
            // flag still unset; loop and try again
        }
    }
}

Member function and arguments

cpp
struct Pool {
    void start(std::size_t n) { /* spawn n threads */ }
    std::once_flag started;
};

void use(Pool& p) {
    std::call_once(p.started, &Pool::start, &p, 8u);   // C++11
}

Best Practices

  • Prefer function-local statics for process-wide singletons. Since C++11, "magic statics" guarantee thread-safe initialization of a block-scope static, and compilers typically implement it with the same machinery. It is shorter and cannot be misused. Use call_once when the initialization is per object, depends on runtime arguments, is conditional, or must be retryable.
  • Keep the flag next to the data it guards. Declare once_flag and the protected data as adjacent members so the relationship is obvious.
  • Make initialization idempotent on failure. Because a thrown exception leaves the flag unset, the callable should leave no half-built state behind, or it should clean up before throwing.
  • Keep the callable short and non-blocking where possible. Every concurrent caller blocks until it finishes.
  • Use mutable for lazy initialization in const methods, as in the first example.
  • Prefer it over hand-written double-checked locking. A correct hand-written version needs std::atomic with acquire/release ordering; a plain pointer check is a data race and undefined behavior. call_once encapsulates that correctly. On the fast path, mainstream implementations only perform an atomic load, so the cost is comparable.

Common Pitfalls

  • Recursive invocation on the same flag. Calling call_once with the same flag from inside its own callable is undefined behavior in practice (deadlock). The same hazard exists for recursively initializing a function-local static.
  • Different flags, shared state. Two flags guarding the same data do not exclude each other. One flag per piece of state.
  • Reusing or resetting a flag. There is no reset. If you need re-initialization, use a mutex plus state, or std::atomic and a different design.
  • Copying the enclosing object. Classes containing a once_flag have implicitly deleted copy and move operations, which surprises containers such as std::vector that require movability. Wrap the flag in std::unique_ptr<std::once_flag> if necessary.
  • Assuming exceptions disable the flag. They do not. A thrown exception only means this attempt didn't count. Under persistent failure, every caller will retry and throw.
  • Deadlock via lock ordering. If the callable acquires a mutex that another thread holds while it is blocked in call_once on the same flag, you have a classic lock-order deadlock. Treat the flag like a mutex in your ordering analysis.
  • Old toolchains. Some pre-C++11-complete implementations (notably older libstdc++ with certain platforms or missing -pthread) threw std::system_error or crashed. Link -pthread (or Threads::Threads in CMake) explicitly.
  • Using it for repeated work. call_once is for a single transition from uninitialized to initialized. For recurring "run once per event" signalling, use a std::condition_variable or std::atomic flag.

See Also

  • Atomic operations: building lock-free initialization and the memory orderings that double-checked locking requires.
  • Condition variables: signalling readiness instead of one-shot initialization.
  • async and future: computing a value once and sharing it via std::shared_future.