2
  1. Global Variables are usually discouraged. What is the rational behind Global Variables with external linkage in C++ Standard Library?

  2. Is it true that extern variable is only declaration but not definition?

An example is the gcc implementation of std::call_once in mutex.h Thread local Global Variables with external linkage are declared:

  extern __thread void* __once_callable;
  extern __thread void (*__once_call)();

in

  /// @cond undocumented
# ifdef _GLIBCXX_HAVE_TLS
  // If TLS is available use thread-local state for the type-erased callable
  // that is being run by std::call_once in the current thread.
  extern __thread void* __once_callable;
  extern __thread void (*__once_call)();

  // RAII type to set up state for pthread_once call.
  struct once_flag::_Prepare_execution
  {
    template<typename _Callable>
      explicit
      _Prepare_execution(_Callable& __c)
      {
    // Store address in thread-local pointer:
    __once_callable = std::__addressof(__c);
    // Trampoline function to invoke the closure via thread-local pointer:
    __once_call = [] { (*static_cast<_Callable*>(__once_callable))(); };
      }

    ~_Prepare_execution()
    {
      // PR libstdc++/82481
      __once_callable = nullptr;
      __once_call = nullptr;
    }

    _Prepare_execution(const _Prepare_execution&) = delete;
    _Prepare_execution& operator=(const _Prepare_execution&) = delete;
  };
# else
  // Without TLS use a global std::mutex and store the callable in a
  // global std::function.
  extern function<void()> __once_functor;

  extern void
  __set_once_functor_lock_ptr(unique_lock<mutex>*);

  extern mutex&
  __get_once_mutex();

  // RAII type to set up state for pthread_once call.
  struct once_flag::_Prepare_execution
  {
    template<typename _Callable>
      explicit
      _Prepare_execution(_Callable& __c)
      {
    // Store the callable in the global std::function
    __once_functor = __c;
    __set_once_functor_lock_ptr(&_M_functor_lock);
      }

    ~_Prepare_execution()
    {
      if (_M_functor_lock)
    __set_once_functor_lock_ptr(nullptr);
    }

  private:
    // XXX This deadlocks if used recursively (PR 97949)
    unique_lock<mutex> _M_functor_lock{__get_once_mutex()};

    _Prepare_execution(const _Prepare_execution&) = delete;
    _Prepare_execution& operator=(const _Prepare_execution&) = delete;
  };
# endif
  1. If __once_callable & __once_call are declared but not defined, where are they defined? I didn't find definitions of them in Standard Library header files in devtoolset-11 without extern. Are they defined in source files?
vvv444
  • 2,764
  • 1
  • 14
  • 25
cpp
  • 265
  • 1
  • 6
  • *"Is it true that extern variable is only declaration but not definition?..."* Yes unless you use initializer to initialize it. – Jason May 22 '23 at 03:05
  • 2
    One question per post. Someone might know answer to some part of your question but not to others. So you should mostly ask one question per post. – Jason May 24 '23 at 06:04
  • "Global Variables are usually discouraged" - The keyword is "*usually*". C++ allows *many* things that are not good practice in day-to-day code, but that do have their uses in rare circumstances. Examples include `setjmp`/`longjmp`, `goto`, global variables, `delete this;`, etc. – Jesper Juhl May 29 '23 at 01:42

3 Answers3

5

Part 1 - Rational behind global variables in the C++ standard library

That's true that as a general rule of thumb, global variables should be used with extreme care. That's because in a large program it's almost impossible to track which parts of the program modify the variable and it quickly becomes impossible to understand and debug what's happening (more reasons can be found in answers to this question).

That said, global variables are a legitimate tool that can be used as long as care is taken. These even might have some advantages, for example in performance. Basically, it's OK to use global variable if you ensure that the number of places modifying the variable is strictly limited. In this case, the variable is internal to the standard library, only accessed in the mutex file and isn't supposed to be accessed by any other code, so that's why it is fine.

As to why they had to use a global variable in this specific case, it's because, frankly, there was no other choice. This variable is used to wrap the callable in this function and pass it to the __gthread_once() function (line 907). __gthread_once() only accepts a pointer to global function without any arguments as parameter (here __once_proxy). For __once_proxy() to invoke the "callable", it had to be stored at some known place. And without any arguments, the only option is using a global variable.

Basically, there are three kinds of memory where you can allocate variables: heap, stack and statically allocated variables area of the executable (.bss). The "global" variables are allocated in the later. The addresses of global variables are filled directly into the executable code by the linker (or OS loader). While the addresses of variables on stack or heap depend on a lot of factors and to access something specific there, its address has to be passed as a parameter (pointer), but as mentioned __once_proxy() receives no parameters, that's why stack and heap weren't an option here.

Part 2 - extern variables (declaration vs definition)

Regarding the second part of your question - yes extern only declares a variable. A definition has to exist in some place. For these variables it's in mutex.cc as @Jason pointed in his answer.

vvv444
  • 2,764
  • 1
  • 14
  • 25
1

If __once_callable & __once_call are declared but not defined, where are they defined?

They are defined in the corresponding source file mutex.cc:

#include <mutex>

#ifdef _GLIBCXX_HAS_GTHREADS

namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION

#ifdef _GLIBCXX_HAVE_TLS
 __thread void* __once_callable;   //these are definitions 
 __thread void (*__once_call)();   //these are definitions

Is it true that extern variable is only declaration but not definition?

Yes, unless you use an initializer to initialize the variable. For example:

extern int i; //this is declaration that is not a definition
extern int j = 0; //this is definition(and so a declaration also)
Jason
  • 36,170
  • 5
  • 26
  • 60
0

Part 1

As others have pointed out in their answers, global variables are generally discouraged, although they have their uses.

In my opinion the greatest disadvantage and the reason why using a global is so bugprone, is that its lifetime is implicit. Meaning it is hard to know for sure at any point during the program, if it has been initialized or not. This can be problem for both reading and writing said global variable.

On the other hand, I think const global variables are actually useful, even better if you make them constexpr. Compilers are allowed to optimize away such variables (unless their address is taken).

Since C++17 you can also mark global variables with inline which guarantees that they will be defined exactly once, possibly reducing binary size. Note that this is only meaningful if your compiler does not optimize the variable away, although it is still recommended to mark constexpr globals inline.

To add to vvv444's answer, global variables usually reside in the .bss section of the binary which is zero initialized by the runtime (before main is called). Constants however usually reside in the .rodata section (if they aren't optimized away), which means their value is written into the binary itself. Note that this can only happen, if the initializer of such constant can be evaluated at compile-time.

Part 2

As Jason pointed out, an extern variable declaration can have an initializer associated with it, making it a definition, but in my opinion this is a huge code smell. Initialization should happen as close to the declaration as possible. In this case in mutex.cc. Both clang and gcc warn you about extern declarations with initializers without turning on any warnings whatsoever.

Part 3

As Jason mentioned before, they are defined in mutex.cc.

Summary

To summarize my thoughts, I have used global variables successfully in my programs, BUT 99% of them were inline constexpr. The remaining 1% occurred because of similar reasons to the one mentioned in the given code sample. I think that using explicit lifetime management for your objects and variables will lead to less complicated and more debuggable code. I usually implement some kind of Context (possibly RAII) type, which I instantiate in main and pass a pointer to it around, this makes the lifetime explicit.

Quasar6
  • 68
  • 5