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++11A 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_onceon that flag. Writes made by the initializer are visible to every thread that returns fromcall_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
#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
#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
#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
#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
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_oncewhen 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_flagand 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
mutablefor lazy initialization inconstmethods, as in the first example. - Prefer it over hand-written double-checked locking. A correct hand-written version needs
std::atomicwith acquire/release ordering; a plain pointer check is a data race and undefined behavior.call_onceencapsulates 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_oncewith 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::atomicand a different design. - Copying the enclosing object. Classes containing a
once_flaghave implicitly deleted copy and move operations, which surprises containers such asstd::vectorthat require movability. Wrap the flag instd::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_onceon 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) threwstd::system_erroror crashed. Link-pthread(orThreads::Threadsin CMake) explicitly. - Using it for repeated work.
call_onceis for a single transition from uninitialized to initialized. For recurring "run once per event" signalling, use astd::condition_variableorstd::atomicflag.
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.