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