Search code examples
c++global-variablesstatic-initialization

Global variable initialization order


One issue of global variables is that initialization order is undefined across translation units, and we have some practices to avoid the global variables. But I still want to understand the initialization order of global variables across translation units, just for education purposes.

Suppose we have code like this:

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

my.cpp:

// global variables

ActionSet s(ActionType::addShard);

My questions are:

  1. Can I always get the exact value from the global "s" variable? s depends on ActionType::addShard which is defined in a different translation unit.
  2. If it is not guaranteed, how can I compile/link/run to get the wrong result? I heard that the order depends on the link stage.

==== To make the topic 2 discussed easier, here is my test code ====

//  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 s{X::x, X::y};

    void f1() {
        s.dump();
    }


    void f2() {
        const ActionSet s2{X::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)

Solution

  • You have a lot of code to go through, unfortunately, I'm unable to find out what ActionType actually is.

    As you indicated, using Global Variables is indeed a bad idea. Luckily, they added constexpr to the language. With constexpr you can create constants which are 'defined' at compile time without having runtime impact. So regardless of the order in which you Ctors get executed, it will produce the right result.

    On the bad side of things, std::initializer_list ain't constexpr (not even in C++20), std::bitset is.

    #include <bitset>
    
    struct ActionType {
        static constexpr std::bitset<4> addShard{0b0101};
    };
    

    With the code above, you made a constexpr variable that can safely be used to initialize a global variable. Similarly, you could create your next type as constexpr available:

    class ActionSet {
    public:
        constexpr ActionSet();
        ActionSet(std::initializer_list<int> ids);
        constexpr ActionSet(std::bitset<4> actions) : _actions{actions} {}
        void dump() const;
    
    private:
        std::bitset<4> _actions{0};
    };
    
    constexpr ActionSet s(ActionType::addShard);
    

    In short, as long as you are able to 'define' everything at compile time (including all the required code in headers), you can create constants based on other constants. Calling constant methods on them can be done at run time later on.

    From C++20 on, you should be able to write:

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

    This should allow you from using non-const methods within your program. It's unclear to me if this still allows you to use 's' within the constructor of the next constexpr variable.