std::call_once(3)

πŸ“¦ version 2024.07.05, in nixpkgs unstable, opensuse-leap 16.0

πŸ’… NAME πŸ’…

std::call_once β€” bestie, it's std::call_once, that's literally the whole name ✨ (yes the manpage really just names it after itself, iconic behavior honestly)

πŸ¦‹ SYNOPSIS πŸ¦‹

Okay so first things first, it lives in the header <mutex> 🫢

template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );

(since C++11) β€” yeah she's a modern girlie πŸ’…

What she does: executes the Callable object f exactly once, even if called concurrently from several threads. Like, one time only, no cap. πŸ”₯

🌸 In detail: 🌸

  • If, by the time std::call_once is called, flag indicates that f was already called, std::call_once returns right away (such a call to std::call_once is known as passive). She just peaces out, no notes. ✌️
  • Otherwise, std::call_once calls INVOKE(std::forward<Callable>(f), std::forward<Args>(args)...). Unlike the std::thread constructor or std::async, the arguments are not moved or copied because they do not need to be transferred to another thread of execution (such a call to std::call_once is known as active). Periodt, no unnecessary copying around here. πŸ‘‘
  • If that invocation throws an exception, it is propagated to the caller of std::call_once, and flag is not flipped so that another call will be attempted (such a call to std::call_once is known as exceptional). She said "try again bestie", how forgiving of her 😭
  • If that invocation returns normally (such a call to std::call_once is known as returning), flag is flipped, and all other calls to std::call_once with the same flag are guaranteed to be passive. Done and dusted βœ…

All active calls on the same flag form a single total order consisting of zero or more exceptional calls, followed by one returning call. The end of each active call synchronizes-with the next active call in that order. It's giving queued up in the group chat, everybody waits their turn πŸ’…

The return from the returning call synchronizes-with the returns from all passive calls on the same flag: this means that all concurrent calls to std::call_once are guaranteed to observe any side-effects made by the active call, with no additional synchronization. No extra work needed, she did it for you 🧚


πŸŽ€ PARAMETERS πŸŽ€

  • flag β€” an object, for which exactly one function gets executed. One flag, one function, that's the whole deal πŸ’–
  • f β€” Callable object to invoke. The main character of this whole operation ✨
  • args... β€” arguments to pass to the function. Her little entourage 🫢

⭐ RETURN VALUE ⭐

(none)

Like, literally nothing. She does the thing and leaves. πŸ’€

πŸ™ˆ EXCEPTIONS πŸ™ˆ

  • std::system_error if any condition prevents calls to std::call_once from executing as specified. Rude of the system, honestly.
  • Any exception thrown by f. Whatever she throws, you're catching it fr fr.

🌈 NOTES 🌈

If concurrent calls to std::call_once pass different functions f, it is unspecified which f will be called. Not me picking a favorite 😭 The selected function runs in the same thread as the std::call_once invocation it was passed to.

Initialization of function-local statics is guaranteed to occur only once even when called from multiple threads, and may be more efficient than the equivalent code using std::call_once. Slay, the compiler already had this handled πŸ’…

The POSIX equivalent of this function is pthread_once. Old school icon, we respect her πŸŽ€


πŸ”₯ EXAMPLE πŸ”₯

// Run this code

#include <iostream>
#include <mutex>
#include <thread>

std::once_flag flag1, flag2;

void simple_do_once()
{
    std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
}

void may_throw_function(bool do_throw)
{
    if (do_throw)
    {
        std::cout << "Throw: call_once will retry\n"; // this may appear more than once
        throw std::exception();
    }
    std::cout << "Did not throw, call_once will not attempt again\n"; // guaranteed once
}

void do_once(bool do_throw)
{
    try
    {
        std::call_once(flag2, may_throw_function, do_throw);
    }
    catch (...) {}
}

int main()
{
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once);
    std::thread st3(simple_do_once);
    std::thread st4(simple_do_once);
    st1.join();
    st2.join();
    st3.join();
    st4.join();

    std::thread t1(do_once, true);
    std::thread t2(do_once, true);
    std::thread t3(do_once, false);
    std::thread t4(do_once, true);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

πŸ“ Possible output: πŸ“

Simple example: called once
Throw: call_once will retry
Throw: call_once will retry
Throw: call_once will retry
Did not throw, call_once will not attempt again

Lowkey obsessed with how the "Throw:" line shows up three times but the last one is guaranteed exactly once. That's the whole personality of this function right there πŸ’œ


πŸ‘‘ Defect reports πŸ‘‘

The following behavior-changing defect reports were applied retroactively to previously published C++ standards. History was rewritten, and honestly? She ate. ✨

  • LWG 2080 β€” applied to C++11. Behavior as published: std::invalid_argument would be thrown if f is invalid, but the scenario where f is invalidated is not specified. Correct behavior: removed this error condition.
  • LWG 2442 β€” applied to C++11. Behavior as published: the arguments were copied and/or moved before invocation. Correct behavior: no copying/moving is performed.

πŸ¦‹ SEE ALSO πŸ¦‹

  • once_flag β€” helper object to ensure that call_once invokes the function only once (C++11) (class)
  • C documentation for call_once

and that's the whole page bestie, go forth and call things exactly once πŸ’–βœ¨πŸŽ€πŸ¦‹πŸŒΈπŸ’…πŸ«Ά

Source and date for the record, since we're being official: http://cppreference.com Β· 2024.06.10 Β· std::call_once(3) β€” mwah, bye! πŸ’œπŸ§š

πŸ“¦ where this page comes from