1 Preface

Some time ago, I was looking at the code of an old product, which was a mixed C/C++ code, and the code was full of global variables and used extern references to external global variables. The problem was that since there were dependencies between classes, if all dependencies were passed in through the constructor method, it would lead to a complex construction of the whole object dependency graph. For older code, using global variables + extern references can be a simple and brutal way to insert new call relationships to be added, but it also brings code corruption.

In our division’s new C++ Fusion programming specification it is mentioned that

References to external function interfaces and variables by means of declarations are prohibited.

You can only use interfaces provided by other modules or files by including header files. The use of external function interface variables by declaration can easily lead to inconsistent declaration and definition when the external interface is changed. Also this implicit dependency can easily lead to architectural corruption.

Avoid the use of global variables (excerpt).

The use of global variables can lead to data coupling between business code and global variables. The initialization order of global variables as well as global constants in different compilation units is not strictly defined, and it is necessary to pay attention to whether their initialization has interdependencies when using them.

A complete application is composed of a set of objects that collaborate with each other, and the developer has to focus on how to make these objects work together to accomplish the required functionality with low coupling and high aggregation. If a framework comes out to help us create objects and manage the dependencies between these objects, then we only need to aggregate on the business logic. As a veteran of writing Java code at the same time, I understand that managing dependencies is one of the original design goals of Spring, and Java naturally has dynamic reflection capabilities that provide the basis for IoC framework implementation. But C++ doesn’t have the introspection that reflection can have on classes, so how do you implement a simple IoC framework?

2 Code Explanation

Spring has various ways of dependency injection, let’s implement a simple property dependency injection first. In C++ you want to achieve the following effect.

1
2
3
4
5
6
class A {};

class B {
private:
    INJECT_PTR(A, m_pA); // INJECT_PTR is a macro that automatically injects a pointer to A for B.
};

Obviously it is not realistic to achieve automatic injection of C++ objects without the help of some other auxiliary facilities. Referring to the Spring concept, the objects that can be injected must be beans, which are managed in the IoC framework and are called managed objects. So to achieve the above effect, two core issues have to be solved.

  • how objects are automatically managed by the framework
  • how to discover the dependencies between objects and automatically inject the required objects

Three core classes are abstracted.

  • IObject: is an interface that abstracts the object initialization (Init) and cleanup (Destroy) methods, equivalent to Spring’s @PostConstruct and @PreDestroy capabilities.
  • ManagedObject: is a base class , all the object types that can be managed by the framework to inherit it , it also implements the IObject interface.
  • Container: is an object container that manages all instances of ManagedObject objects , support for calling all objects Init and Destroy methods to complete the object initialization and cleanup.

Constraints.

  • Subclasses that inherit from ManagedObject must have a default parameterless constructor method that supports calling parameterless constructors to create objects.
  • Dependencies can exist between objects of ManagedObject, but they are not constructed with sequential dependencies.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class IObject {
public:
    virtual void Init() = 0;
    virtual void Destroy() = 0;
    virtual ~IObject() = default;
};

template<typename T>
class ManagedObject : public IObject {
protected:
    // The purpose of providing automatic object creation is achieved through static object initialization
    static struct AutoInit {
        inline void DoNothing() const {} // This will be discussed later
        explicit AutoInit();
    } m_init;

    // Inject the callback function type of the member variable and register it with m_valInjectFuncs in the constructor method.
    struct InjectFunction {
        InjectFunction(ManagedObject *obj, const std::function<void()> &func)
        { obj->m_valInjectFuncs.emplace_back(func); }
    };
public:
    void Init() final; // Call the callback function of m_valInjectFuncs first, then call OnInit
    void Destroy() final;
    virtual void OnInit() {}; // Initialization by subclass override
    virtual void OnDestroy() {};
private:
    std::vector<std::function<void()>> m_valInjectFuncs; // Store the callback function for injecting member variables
};

class Constainer {
public:
    static Constainer &instance(); // Single instance
    void AddObject(const char *name, IObject *obj); // Registration management object
    IObject *GetObject(const char *name) const; // Get managed objects
    void InitAllObjects(); // Initialize all objects and call the Init method of all objects
    void DestroyAllOjects();  // Clean up all objects, call the Destroy method of all objects, and release all object memory
private:
    std::unorder_map<std::string, IObject *> m_objs;
};

How to solve the first problem? This solution uses the feature of C++ that initialization of static member variables precedes the main method. Then you can have a static member object in ManagedObject, create generation in the static member object constructor for the class that inherits it, and then add the object pointer to Container management. If you want to know the specific type of the subclass, you need to use a template to give the subclass type to the static member object constructor method at compile time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T>
ManagedObject<T>::AutoInit::AutoInit()
{
    T *p = new T(); // Get the subclass type and call the parameterless constructor
    const auto name = typeid(T).name();
    Container::Instance().AddOject(name, p); // Register to container
}

template<typename T>
ManagedObject<T>::AutoInit ManagedObject<T>::m_init; // Static object instantiation

To solve the second problem again, construct a temporary InjectFunction object in the macro INJECT_PTR, and in its constructor method, get the required object pointer from the container and assign it to a member variable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#define STR_CONCAT_(a, b) a##b
#define STR_CONCAT(a, b) STR_CONCAT_(a, b)
#define INJECT_PTR(typeName, valName) typeName *valName;\
const InjectFunction STR_CONCAT(inject_, __LINE__) = {\
    this, [this]() {\
        this->m_init.DoNothing();\
        auto obj = Container::Instance().GetObject(typeid(typeName).name());\
        this->valName = dynamic_cast<typeName *>(obj);\
    }\
}; \

The logic of the above macro.

  • Adds an inject_{__LINE__} member variable to the class, of type InjectFunction, with the variable name with the line number where it is located, and no conflicts.
  • Uses the C++11 way of initializing variables, calling the constructor method of InjectFunction, passing in this, and a lambda expression
  • DoNothing() in the lambda expression to solve the problem that template member variables must be called to take effect.
  • then get the registered object pointer according to the type, and assign a value to the variable valName to be injected

Complete use of the following list.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class A : public ManagedObject<A> {};

class B : public ManagedObject<B> {
private: 
    INJECT_PTR(A, m_pA);
public:
    void OnInit() overide { std::cout << static_cast<void*>(m_pA) << std::endl; }
};

class C : public ManagedObject<C> {
private: 
    INJECT_PTR(A, m_pA);
    INJECT_PTR(B, m_pB);
public:
    void OnInit() overide
    { 
        std::cout << static_cast<void*>(m_pA) << std::endl; 
        std::cout << static_cast<void*>(m_pB) << std::endl;
    }
};

// static variables precede main method
// The static variable m_init of [A, B, C] is instantiated, and the creation of the A, B, C objects is completed and registered with Container, so the A, B, C objects are actually single instances.

int main(int argc, const char **argv) 
{
    Container::Instance()::InitAllObjects();
    // InitAllObjects() --> call Init of [A, B, C] --> call m_valInjectFuncs callback method of [B, C] --> dependency injection assignment
    Container::Instance()::DestroyAllOjects();
}

3 static variable life cycle

C++ static member variables, like normal static variables, allocate memory in a global data area in a memory partition and do not release it until the end of the program. This means that static member variables do not allocate memory with the creation of an object and do not release memory with the destruction of an object. In contrast, ordinary member variables allocate memory at object creation and release memory at object destruction.

C also has static variables, but the life cycle differs slightly from that of C++, with different initialization.

Global variables Static variables of a file domain Static member variables of a class Static local variables
C Compile-time initialization Compile-time initialization N/A Compile-time initialization
C++ before main execution before main execution before main execution initialized on first execution of related code

Going back to the scenario in the code explanation, classes A, B, and C in the example can also be placed in different h/cpp files and will be automatically constructed and registered with the container before main execution as long as the compile and link unit is added. If the constructor method of any of the classes is time consuming, it will cause the program to take longer to start.

Some notes.

  • C++ static members belong to the class scope, but not to the class object and cannot be initialized in the class constructor
  • Static member variables must be initialized and can only be done outside the class body
  • Static local variables defined in a member function of a class are shared by all objects of the class when this member function is called
  • The destruction of static variables is managed by atexit(), which is destructed one by one at the end of the program, in the reverse direction of the construction order. For static variables in the same compilation unit, the construction order is the same as the declaration order, and for different compilation units, the construction order is variable.

Is main execution before compile-time or run-time? To quote the C++ standard (C++11 N3690 3.6.2).

Global variables, static variables in file domains, and static member variables of classes are allocated memory and initialized during static initialization prior to main execution; static local variables (generally static variables within functions) are allocated memory and initialized at first use. Variables here include objects of built-in data types and custom types.

What is static initialization? Continuing with the C++ standard.

At the level of the language, the initialization of global variables can be divided into two stages as follows.

  • static initialization: static initialization refers to the use of constants to initialize variables, mainly including zero initialization and const initialization, static initialization is done during program loading, for simple types (built-in types, POD, etc.), from the concrete implementation For simple types (built-in types, PODs, etc.), the variables of zero initialization are stored in the bss segment, while the variables of const initialization are placed in the data segment, and the initialization is done when the program is loaded, which is basically the same as the initialization of global variables in C.
  • dynamic initialization: dynamic initialization refers to initialization that requires a function call to complete, for example, int a = foo(), or initialization of complex types (classes) (requiring a constructor call). The initialization of these variables is done before the execution of the main function by calling the corresponding code at runtime (except for static local variables)

To summarize.

  • For built-in types that have been manually initialized in code, the variable and its initialization value are stored in the data segment of the executable, which does a const initialization on it at runtime
  • If the variable is not initialized manually, it is placed in the bss segment of the executable, and zero initialization is done at runtime.
  • For custom types, the variables are placed in the bss segment and dynamically initialized at runtime
Initialize manually Not initialized manually
Built-In Type data segment, const initialization bss segment, Dynamic,zero initialization
Custom Type bss segment, Dynamic initialization bss segment, Dynamic initialization

4 Conclusion

In this paper, we have used the mechanism of static member variable initialization in C++ to implement a simple framework for automatic object registration and dependency injection management. Through the code design application, then the mechanism for static member variables is organized to strengthen the mastery of the underlying fundamentals of C++. Only when you open your brain and delve into the underlying details in the actual work, you will find C++ more and more interesting.