全局变量初始化顺序

Posted

技术标签:

【中文标题】全局变量初始化顺序【英文标题】:Global variable initialization order 【发布时间】:2018-12-18 06:53:38 【问题描述】:

全局变量的一个问题是跨翻译单元的初始化顺序未定义,我们有一些实践来避免全局变量。但我仍然想了解跨翻译单元的全局变量的初始化顺序,仅用于教育目的。

假设我们有这样的代码:

action_type.h

struct ActionType 
    static const ActionType addShard;  // struct static variables

action_type.cpp

ActionType ActionType::addShard(addShardValue); 

action_set.h

ActionSet(ActionType s);

我的.cpp:

// global variables

ActionSet s(ActionType::addShard);

我的问题是:

    我能否始终从全局“s”变量中获取准确的值? s 取决于在不同翻译单元中定义的 ActionType::addShard。 如果不能保证,如何编译/链接/运行得到错误的结果?我听说顺序取决于链接阶段。

====为了让话题2更容易讨论,这里是我的测试代码====

//  cat action.h 

#ifndef ACTION_H
#define ACTION_H
#include <iostream>
#include <bitset>
namespace m 
    class ActionSet 
    public:
        ActionSet();
        ActionSet(std::initializer_list<int> ids);
        void dump() const;

    private:
        std::bitset<4> _actions;
    ;

#endif /* ACTION_H */

// action.cpp

#include "action.h"
#include <iostream>

namespace m 
ActionSet::ActionSet(): _actions(0) 
    std::cout << "from default" << std::endl;

ActionSet::ActionSet(std::initializer_list<int> ids) 
    std::cout << "from init list.." << std::endl;
    for(auto id : ids) 
        _actions.set(id, true);
    


void ActionSet::dump() const 
    for(int i=0; i<4; i++) 
        std::cout << _actions[i] << ",";
    
    std::cout << std::endl;



// const.h

#ifndef CONST_H
#define CONST_H
namespace m 
struct X 
    static int x;
    static int y;
;


#endif /* CONST_H */

// const.cpp

#include "const.h"

namespace m 
    int X::x = 0;
    int X::y = 2;
;

// f.h  

#ifndef F_H
#define F_H

#include "action.h"
#include <iostream>

namespace m 
 void f1();
void f2();


#endif /* F_H */

// f.cpp
#include "f.h"
#include "const.h"

namespace m 
    const ActionSet sX::x, X::y;

    void f1() 
        s.dump();
    


    void f2() 
        const ActionSet s2X::x, X::y;
        s2.dump();
    
;

// action.h 

#ifndef ACTION_H
#define ACTION_H
#include <iostream>
#include <bitset>
namespace m 
    class ActionSet 
    public:
        ActionSet();
        ActionSet(std::initializer_list<int> ids);
        void dump() const;

    private:
        std::bitset<4> _actions;
    ;

#endif /* ACTION_H */

// action.cpp

#include "action.h"
#include <iostream>

namespace m 
ActionSet::ActionSet(): _actions(0) 
    std::cout << "from default" << std::endl;

ActionSet::ActionSet(std::initializer_list<int> ids) 
    std::cout << "from init list.." << std::endl;
    for(auto id : ids) 
        _actions.set(id, true);
    


void ActionSet::dump() const 
    for(int i=0; i<4; i++) 
        std::cout << _actions[i] << ",";
    
    std::cout << std::endl;



// main.cpp

#include "f.h"


int main(int argc, char *argv[])

    m::f1();
    m::f2();
    return 0;


// CMakeLists.txt

cmake_minimum_required(VERSION 2.6)
project(project_name)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED on)
set(CMAKE_CXX_EXTENSIONS off)
set(CMAKE_EXPORT_COMPILE_COMMANDS on)
set( CMAKE_VERBOSE_MAKEFILE on )

add_executable(main const.cpp main.cpp f.cpp action.cpp)
add_executable(main2 main.cpp f.cpp action.cpp const.cpp)

【问题讨论】:

把目标文件的顺序倒过来,会报错:clang++ my.o action_type.o 无法保证得到正确的结果或错误的结果。您可能会发现链接顺序在一个编译器中可靠地工作,但另一个编译器可能无法预测会发生什么。 @liliscent 我按照您的建议进行了尝试,看起来它不起作用。我在上面粘贴了我的代码。你能指出有什么问题吗? @zhihuifan 你应该做一个minimal的例子来测试错误。只需两个少于 10 LOC 的文件就足够了。至于您的代码,您使用全局int 进行测试,这是错误的。通常 int 只是写在数据段里面,它们不需要运行时初始化。 @zhihuifan "unspecified" 只对 C++ 标准有意义,实际代码最终在机器上运行,所以编译器和系统运行时库应该有办法确定每个细节。例如,mac 上的 clang 将 .o 文件的所有初始化条目并列到 mach-o 部分 __mod_init_func,然后运行时映像加载器将这些指针一一运行。 gcc linux 有一个类似的.init_array 部分(我不熟悉MSVC)。但所有这些都依赖于平台/编译器,我建议你不要编写依赖于这些实现细节的代码。 【参考方案1】:

不幸的是,我无法找出 ActionType 究竟是什么。

正如您所指出的,使用全局变量确实是个坏主意。幸运的是,他们在语言中添加了constexpr。 使用constexpr,您可以创建在编译时“定义”的常量,而不会影响运行时。所以不管你的Ctors被执行的顺序是什么,它都会产生正确的结果。

不好的一面是,std::initializer_list 不是 constexpr(甚至在 C++20 中也不是),std::bitset 是。

#include <bitset>

struct ActionType 
    static constexpr std::bitset<4> addShard0b0101;
;

使用上面的代码,您创建了一个constexpr 变量,可以安全地用于初始化全局变量。同样,您可以将下一个类型创建为 constexpr available:

class ActionSet 
public:
    constexpr ActionSet();
    ActionSet(std::initializer_list<int> ids);
    constexpr ActionSet(std::bitset<4> actions) : _actionsactions 
    void dump() const;

private:
    std::bitset<4> _actions0;
;

constexpr ActionSet s(ActionType::addShard);

简而言之,只要您能够在编译时“定义”所有内容(包括头文件中的所有必需代码),您就可以根据其他常量创建常量。稍后可以在运行时对其调用常量方法。

从 C++20 开始,你应该可以写:

[[constinit]] ActionSet s(ActionType::addShard);

这应该允许您在程序中使用非常量方法。我不清楚这是否仍然允许您在下一个 constexpr 变量的构造函数中使用“s”。

【讨论】:

以上是关于全局变量初始化顺序的主要内容,如果未能解决你的问题,请参考以下文章

dynamic initializer和全局变量

java中的全局变量和静态变量是在编译时分配内存还是在加载时分配内存??

Java静态方法,静态变量,初始化顺序

调整static变量初始化顺序的一个办法

go 函数

JavaScript 变量和函数提升问题总结