0

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)
zhihuifan
  • 1,093
  • 2
  • 16
  • 30
  • Just reverse the order of the object file, you'll get the error: `clang++ my.o action_type.o` – llllllllll Dec 18 '18 at 07:08
  • There's no guarantees to get the right result or the wrong result. You might find that link order works reliably one one compiler, but another there might be no way to predict what will happen. – john Dec 18 '18 at 07:10
  • @liliscent I tried per your suggestion, looks it doesn't work. I have paste my code above. could you point anything is wrong? – zhihuifan Dec 18 '18 at 07:28
  • The initialisation order is unspecified. There is no reliable way to get either the correct or an incorrect result. – molbdnilo Dec 18 '18 at 07:36
  • 1
    @zhihuifan You should make a *minimal* example to test the error. Just two files with fewer than 10 LOC is enough. As for your code, you use global `int` to test, that's wrong. Usually int is just written inside data section, they don't need runtime initialization. – llllllllll Dec 18 '18 at 07:41
  • @liliscent OK, I get your point. Looks compiler do too many things which are horrible to deep dive.. – zhihuifan Dec 18 '18 at 08:05
  • @liliscent just some more questions about this topic: actually the "unspecified" behaviors is decided after the link stage? which means for the same binary, the "unspecified" behavior should be consistent? if we ignore the dynamic library loading. – zhihuifan Dec 18 '18 at 08:50
  • 1
    @zhihuifan "unspecified" is only meaningful to the C++ standard, the actual code eventually runs on a machine, so the compiler and the system runtime lib should have a way to determine every detail. For example, clang on mac juxtaposes all initialization entries of `.o` files to the mach-o section `__mod_init_func`, then the runtime image loader runs those pointer one by one. gcc linux has a similar `.init_array` section (I'm not familiar with MSVC). But all of these are platform/compiler dependent, I'd suggest you to *not* write code that depends on these implementation detail. – llllllllll Dec 18 '18 at 09:37
  • sure @liliscent, I will not depends on these implementation. I just want to understand it to analysis an issue just happen yesterday. thank you for your explanation. – zhihuifan Dec 18 '18 at 09:47

1 Answers1

0

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.

JVApen
  • 11,008
  • 5
  • 31
  • 67