Extern全局变量在标准库中的使用

huangapple go评论76阅读模式
英文:

Extern global variable usage in Standard Library

问题

  1. C++标准库中不鼓励使用全局变量。全局变量与外部链接的合理性是什么?

  2. 外部变量是仅声明而非定义吗?

  3. 如果__once_callable__once_call仅声明而未定义,那么它们在哪里定义?我在devtoolset-11的标准库头文件中没有找到它们的定义,没有extern修饰。它们是在源文件中定义的吗?

英文:
  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?

答案1

得分: 5

第1部分 - C++标准库中全局变量的原因

确实,作为一个一般性的经验法则,应该非常小心地使用全局变量。这是因为在一个大型程序中,几乎不可能跟踪哪些部分修改了变量,很快就会变得不可能理解和调试发生了什么(更多原因可以在这个问题的答案中找到)。

尽管如此,只要小心谨慎,全局变量是一个合法的工具,甚至可能具有一些优势,例如性能。基本上,只要确保修改变量的地方数量严格受限制,就可以使用全局变量。在这种情况下,该变量仅在标准库的内部使用,仅在mutex文件中访问,不应由任何其他代码访问,因此这是可以的。

至于为什么在这种特定情况下必须使用全局变量,那是因为实际上没有其他选择。此变量用于在此函数中包装可调用对象并将其传递给__gthread_once()函数(第907行)。__gthread_once()仅接受全局函数的指针作为参数(在这里是__once_proxy)。为了让__once_proxy()调用“可调用对象”,它必须存储在某个已知位置。并且没有任何参数,唯一的选择就是使用全局变量。

基本上,您可以分配变量的三种类型的内存:堆、栈和可执行文件的静态分配变量区域(.bss)。"全局"变量是分配在后者中的。全局变量的地址直接由链接器(或操作系统加载器)填充到可执行代码中。而栈或堆上的变量的地址取决于许多因素,要访问那里的特定内容,必须将其地址作为参数(指针)传递,但如上所述,__once_proxy()不接受任何参数,这就是为什么在这里堆栈和堆不是一个选择的原因。

第2部分 - extern变量(声明与定义)

关于您问题的第二部分 - 是的,extern只是声明一个变量。定义必须存在于某个地方。对于这些变量,它们的定义在mutex.cc中,正如@Jason在他的答案中指出的那样。

英文:

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.

答案2

得分: 1

Sure, here are the translated parts:

  1. They are defined in the corresponding source file mutex.cc:
  2. Is it true that extern variable is only declaration but not definition?
    • Yes, unless you use an initializer to initialize the variable. For example:

(Note: I've excluded the code portions as per your request.)

英文:

> 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)

答案3

得分: 0

Part 1:

正如其他人在他们的答案中指出的,全局变量通常是不鼓励使用的,尽管它们有它们的用途。

在我看来,最大的缺点以及使用全局变量容易出现错误的原因是,它的生命周期是隐式的。这意味着在程序的任何时刻很难确定它是否已经初始化。这对于读取和写入该全局变量都可能是问题。

另一方面,我认为const全局变量实际上很有用,如果你将它们设为constexpr会更好。编译器可以优化掉这样的变量(除非它们的地址被获取)。

自从C++17以来,你还可以用inline标记全局变量,这保证它们将被定义一次,可能会减小二进制文件的大小。请注意,只有在你的编译器不会优化掉变量时,这才有意义,尽管仍然建议将constexpr全局变量标记为inline

此外,补充vvv444的回答,全局变量通常驻留在二进制文件的.bss部分,由运行时(在调用main之前)初始化为零。然而,常量通常驻留在.rodata部分(如果它们没有被优化掉的话),这意味着它们的值是写入二进制文件本身的。请注意,只有在这样的常量的初始化器可以在编译时评估时,才能发生这种情况。

Part 2:

正如Jason所指出的,extern变量声明可以与初始化器相关联,从而使其成为定义,但在我看来,这是一个糟糕的代码坏味道。初始化应该尽可能接近声明,例如在mutex.cc中。无论如何,无论是clang还是gcc,在没有打开任何警告的情况下都会警告你有初始化器的extern声明。

Part 3:

正如Jason之前提到的,它们在mutex.cc中定义。

Summary:

总结一下我的想法,我在我的程序中成功地使用了全局变量,其中99%都是inline constexpr。剩下的1%是因为与给定代码示例中提到的类似的原因。我认为为你的对象和变量使用显式的生命周期管理将会导致更简单和更易于调试的代码。我通常会实现一种类似Context(可能是RAII)的类型,在main中实例化它,并将指针传递给它,这会使生命周期变得明确。

英文:

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.

huangapple
  • 本文由 发表于 2023年5月22日 11:00:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76302802.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定