C++ call_once | 'Call Once' Complete Guide

C++ call_once | 'Call Once' Complete Guide

이 글의 핵심

std::call_once is a C++11 function that guarantees a function executes exactly once, even when called from multiple threads. Used with std::once_flag to implement thread-safe initialization.

What is call_once?

std::call_once is a C++11 function that guarantees a function executes exactly once, even when called from multiple threads. Used with std::once_flag to implement thread-safe initialization.

#include <mutex>

std::once_flag flag;

void init() {
    std::cout << "Initialization" << std::endl;
}

void func() {
    std::call_once(flag, init);  // Only once
}

Why needed?:

  • Thread-safe initialization: Safe even with concurrent calls from multiple threads
  • Performance: Fast check after initialization (no double-checked locking needed)
  • Exception safety: Can retry on initialization failure
  • Simplicity: No complex synchronization code needed
// ❌ Manual synchronization: complex and error-prone
std::mutex mtx;
bool initialized = false;

void init() {
    std::lock_guard<std::mutex> lock(mtx);
    if (!initialized) {
        // Initialize
        initialized = true;
    }
}

// ✅ call_once: simple and safe
std::once_flag flag;

void init() {
    std::call_once(flag, []() {
        // Initialize (only once)
    });
}

How call_once works:

call_once internally uses atomic operations to execute the function only on first call, returning immediately on subsequent calls.

// Conceptual operation
std::once_flag flag;

void call_once(std::once_flag& flag, Callable&& func) {
    // Atomically check state
    if (flag.already_called()) {
        return;  // Already called, fast return
    }
    
    // First call: acquire lock
    lock();
    if (!flag.already_called()) {
        func();  // Execute function
        flag.mark_called();
    }
    unlock();
}

once_flag characteristics:

  • Not copyable: once_flag cannot be copied
  • Not movable: once_flag cannot be moved
  • State persistence: Once called, permanently remains in “called” state
std::once_flag flag1;
// std::once_flag flag2 = flag1;  // Error: not copyable
// std::once_flag flag3 = std::move(flag1);  // Error: not movable

Basic Usage

Here is the initialize implementation:

std::once_flag initFlag;
bool initialized = false;

void initialize() {
    std::cout << "Initializing..." << std::endl;
    initialized = true;
}

void process() {
    std::call_once(initFlag, initialize);
    // Only first call executes initialize
}

Practical Examples

Example 1: Singleton

Here is the getInstance implementation:

class Singleton {
    static std::once_flag initFlag;
    static Singleton* instance;
    
    Singleton() {
        std::cout << "Singleton created" << std::endl;
    }
    
public:
    static Singleton& getInstance() {
        std::call_once(initFlag, []() {
            instance = new Singleton();
        });
        return *instance;
    }
};

std::once_flag Singleton::initFlag;
Singleton* Singleton::instance = nullptr;

Example 2: Resource Initialization

Here is the getConnection implementation:

class Database {
    static std::once_flag connFlag;
    static Connection* conn;
    
public:
    static Connection& getConnection() {
        std::call_once(connFlag, []() {
            conn = new Connection("localhost");
            std::cout << "DB connected" << std::endl;
        });
        return *conn;
    }
};

Example 3: Configuration Loading

Here is the loadConfig implementation:

std::once_flag configFlag;
Config config;

void loadConfig() {
    std::cout << "Loading config" << std::endl;
    config = Config::load("config.json");
}

Config& getConfig() {
    std::call_once(configFlag, loadConfig);
    return config;
}

Example 4: Lambda Usage

Here is the func implementation:

std::once_flag flag;
int value = 0;

void func() {
    std::call_once(flag, [&value]() {
        value = expensiveComputation();
        std::cout << "Computation complete: " << value << std::endl;
    });
    
    std::cout << "Value: " << value << std::endl;
}

Exception Handling

Here is the init implementation:

std::once_flag flag;

void init() {
    throw std::runtime_error("Initialization failed");
}

void func() {
    try {
        std::call_once(flag, init);
    } catch (...) {
        // On exception, flag resets
        // Can retry on next call_once
    }
}

Common Issues

Issue 1: Multiple once_flags

Here is the init1 implementation:

std::once_flag flag1, flag2;

void init1() { std::cout << "Init 1" << std::endl; }
void init2() { std::cout << "Init 2" << std::endl; }

void func() {
    std::call_once(flag1, init1);
    std::call_once(flag2, init2);
}

Issue 2: Passing Arguments

Here is the init implementation:

std::once_flag flag;

void init(int x, const std::string& s) {
    std::cout << x << ", " << s << std::endl;
}

void func() {
    std::call_once(flag, init, 42, "Hello");
}

Issue 3: Member Functions

Here is the init implementation:

class MyClass {
    std::once_flag flag;
    
    void init() {
        std::cout << "Initialization" << std::endl;
    }
    
public:
    void process() {
        std::call_once(flag, &MyClass::init, this);
    }
};

Issue 4: Exception Retry

Here is the init implementation:

std::once_flag flag;
int attempt = 0;

void init() {
    attempt++;
    if (attempt < 3) {
        throw std::runtime_error("Retry");
    }
    std::cout << "Success" << std::endl;
}

void func() {
    try {
        std::call_once(flag, init);
    } catch (...) {
        // Retry on next call
    }
}

Static Local Variable Alternative

The following example demonstrates the concept in cpp:

// call_once
std::once_flag flag;
Resource* resource = nullptr;

Resource& getResource() {
    std::call_once(flag, []() {
        resource = new Resource();
    });
    return *resource;
}

// Static local variable (C++11, simpler)
Resource& getResource() {
    static Resource resource;  // Thread-safe
    return resource;
}

Production Patterns

Pattern 1: Lazy Initialization Wrapper

Here is the LazyInit implementation:

template<typename T>
class LazyInit {
    std::once_flag flag_;
    std::unique_ptr<T> instance_;
    
public:
    template<typename....Args>
    T& get(Args&&....args) {
        std::call_once(flag_, [this, &args...]() {
            instance_ = std::make_unique<T>(std::forward<Args>(args)...);
        });
        return *instance_;
    }
};

// Usage
LazyInit<Database> db;
db.get("localhost", 5432).query("SELECT * FROM users");

Pattern 2: Initialization Chain

Here is the initConfig implementation:

class Application {
    std::once_flag configFlag_;
    std::once_flag dbFlag_;
    std::once_flag cacheFlag_;
    
    void initConfig() {
        std::cout << "Loading config\n";
        // Config initialization
    }
    
    void initDatabase() {
        std::call_once(configFlag_, [this]() { initConfig(); });
        std::cout << "Connecting DB\n";
        // DB initialization
    }
    
    void initCache() {
        std::call_once(dbFlag_, [this]() { initDatabase(); });
        std::cout << "Initializing cache\n";
        // Cache initialization
    }
    
public:
    void start() {
        std::call_once(cacheFlag_, [this]() { initCache(); });
        std::cout << "Application started\n";
    }
};

Pattern 3: Retryable Initialization

Here is the tryInit implementation:

class RetryableInit {
    std::once_flag flag_;
    int maxRetries_ = 3;
    int attempts_ = 0;
    
    void tryInit() {
        attempts_++;
        if (attempts_ < maxRetries_) {
            throw std::runtime_error("Init failed, retrying");
        }
        std::cout << "Init successful\n";
    }
    
public:
    bool initialize() {
        try {
            std::call_once(flag_, [this]() { tryInit(); });
            return true;
        } catch (const std::exception& e) {
            std::cerr << e.what() << '\n';
            return false;
        }
    }
};

// Usage
RetryableInit init;
while (!init.initialize()) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
}

Why Use call_once for Singleton Initialization

When you want once-only expensive initialization at global/function level, manually using mutex + bool flag makes double-checked locking difficult to implement correctly and vulnerable to compiler optimizations. std::call_once provides standard-guaranteed once-only execution, commonly used for singleton lazy initialization.

However, since C++11, Meyers Singleton using static local objects is often simpler (see next section). call_once shines when you need multi-step initialization ordering, once-only calls with arguments, or retry on failure - cases where static alone is ambiguous.

Multithreading

Understanding with everyday analogy: Concurrency is like cooking multiple dishes simultaneously in a kitchen. One chef (single thread) boils soup, turns down heat, chops vegetables in between, then checks soup again. Parallelism is multiple chefs (multi-thread) each making different dishes simultaneously.

How safety applies to the standard

std::call_once(flag, f, args...) ensures f executes only once on successful completion, even when called simultaneously from all threads. While one thread executes f, other threads block until completion.

If exception occurs during initialization, the standard allows retry on next call_once (implementation resets once_flag to failed state). Useful for wrapping network/file initialization that can fail.

Comparison with static Local Variables (Production Choice)

Since C++11, block-scope static local variable initialization is guaranteed to happen only once without data races.

// Example
Foo& instance() {
    static Foo f;  // Initialized once on first call (thread-safe)
    return f;
}
SituationRecommendation
Can create type T in place, only need default/argument constructorstatic local variable is shorter and more readable
Initialization requires complex steps, multiple function calls, retry after exceptioncall_once
Dynamic library loading, plugin registration - “once-only” but awkward to express with staticcall_once or framework’s initialization API

Enhanced Production Patterns

  • Library initialization: Place once_flag at namespace level and wrap register_codec(), load_config() calls with call_once for app-wide once-only execution.
  • Lazy-loaded DLL/so: If handle acquisition can fail, implement retry policy with exception handling loop and call_once.
  • Testing: Since once_flag cannot be reset, if unit tests need “re-initialize per test”, better to move to separate fixture or non-static object.

Realistic Performance Expectations

After successful execution, call_once calls are typically handled through very lightweight atomic path. Exact numbers vary by platform/compiler, but still avoid calling call_once every iteration in hot loop. For “once-only”, move call point outside loop or to initialization phase.

Ideal structure: heavy work only on first call, then fast exit with same flag thereafter.

FAQ

Q1: What is call_once?

A: C++11 function that guarantees function executes exactly once, even when called simultaneously from multiple threads.

std::once_flag flag;

void init() {
    std::cout << "Initialization\n";
}

// Even called from multiple threads, init() executes only once
std::thread t1([]() { std::call_once(flag, init); });
std::thread t2([]() { std::call_once(flag, init); });

Q2: When should I use it?

A:

  • Singleton pattern: Create instance only once
  • Resource initialization: DB connection, file opening, etc.
  • Configuration loading: Read config file only once
  • Lazy initialization: Initialize only once when needed
// Singleton
static Logger& getLogger() {
    static std::once_flag flag;
    static Logger* instance = nullptr;
    std::call_once(flag, []() {
        instance = new Logger();
    });
    return *instance;
}

Q3: How does exception handling work?

A: On exception, once_flag resets allowing retry on next call.

Here is the init implementation:

std::once_flag flag;
int attempt = 0;

void init() {
    attempt++;
    if (attempt < 3) {
        throw std::runtime_error("Retry");
    }
    std::cout << "Success\n";
}

// Multiple calls will retry
for (int i = 0; i < 5; ++i) {
    try {
        std::call_once(flag, init);
    } catch (...) {
        std::cout << "Failed, retrying\n";
    }
}

Q4: Difference from static local variables?

A: Since C++11, static local variables are thread-safe, so simpler in most cases.

The following example demonstrates the concept in cpp:

// call_once: explicit
std::once_flag flag;
Resource* resource = nullptr;

Resource& getResource() {
    std::call_once(flag, []() {
        resource = new Resource();
    });
    return *resource;
}

// Static local variable: simpler (C++11+ thread-safe)
Resource& getResource() {
    static Resource resource;  // Automatically initialized once
    return resource;
}

Use call_once when:

  • Initialization logic is complex
  • Exception retry needed
  • Want explicit control over initialization timing

Q5: How is performance?

A: Very fast after first call. Internally uses atomic operations for fast checking.

// First call: execute initialization (slow)
std::call_once(flag, expensiveInit);

// Subsequent calls: atomic check only (very fast, ~1-2ns)
std::call_once(flag, expensiveInit);

Q6: Can I call member functions?

A: Yes. Pass member function pointer and this.

Here is the init implementation:

class MyClass {
    std::once_flag flag_;
    
    void init() {
        std::cout << "Initialization\n";
    }
    
public:
    void process() {
        std::call_once(flag_, &MyClass::init, this);
    }
};

Q7: Can I pass arguments?

A: Yes. call_once supports variadic arguments.

Here is the init implementation:

std::once_flag flag;

void init(int x, const std::string& s) {
    std::cout << x << ", " << s << '\n';
}

void func() {
    std::call_once(flag, init, 42, "Hello");
}

Q8: Learning resources for call_once?

A:

Related topics: Singleton Pattern, Thread Basics, Mutex.

One-line summary: std::call_once is a thread-safe initialization mechanism that guarantees function execution exactly once across multiple threads.