devxlogo

Managing Objects’ Construction Order

Managing Objects’ Construction Order

n standard C++, the initialization order of objects with static storage type that are declared in separate translation units is unspecified. If, for instance, a constructor of object A takes as an argument a value returned from a member function of object B, B must be constructed before A. However, if A and B are defined in separately compiled files, there’s no guarantee that B will be constructed first.


How can you ensure that objects defined in separately compiled files are constructed in a desired order?


Avoid global objects. Use special accessor functions instead.

Advice on Code Organization
A properly organized C++ program consists of multiple source files and corresponding header files. A compilation unit, or unit for short, is a pair of a source file and its matching header file. Each unit may be compiled separately. Wouldn’t it be better to group multiple classes in one unit? No, it wouldn’t. In real world projects, each class may be maintained by a different developer, team or vendor. Splitting projects into separate compilation units has another advantage?reducing compilation time. Many compilers nowadays use incremental builds, meaning they compile only units that have changed since the last compilation.

Problem Analysis
Imagine an application that uses a log file class to record its activity for performance tuning, security purposes etc., In addition to the log file class, you also need an administrator class that sets the log file’s path, controls users’ authorizations etc. The first unit is implemented as follows:

// logfile.h//----------#ifndef LOGFILE_H#define LOGFILE_H#include #include class Logfile{public: Logfile(const std::string & path); //private: std::ofstream log; std::string name;};#endif// logfile.cpp//------------ #include "logfile.h"Logfile::Logfile(const std::string & path) :name(path){ log.open(name.c_str()); bool success=log; //..}#include "admin.h"extern Admin admin; //admin is defined elsewhere//but needed here to construct the lfLogfile lf(admin.getfilename()); 

The application uses a global object called lf (defined in logfile.cpp). The reason for this is that lf should start logging before main() starts. The argument passed to lf’s constructor is a path stored as an environment variable which class Admin retrieves. Obviously, admin must be constructed before lf. Class Admin is implemented in a separate unit like this:

// admin.h//---------#if !defined(ADMIN_H)#define ADMIN_H#include class Admin{public: Admin(); const std::string& getfilename() const;  //private: std::string filename;};#endif// admin.cpp//-------------#include "admin.h"Admin::Admin(){ const char * pname=std::getenv("LOGFILE_PATH"); if(pname) //did getenv find a match?  filename=pname; else //assign a default filename {  filename = "activities.log"; }}const std::string& Admin::getfilename() const{ return filename;}Admin admin;//global; must be constructed before lf

The application’s body is implemented as a separate source file called main.cpp:

// main.cpp//-----------#include "logfile.h"extern Logfile lf; //declaration; defined in Logfile.cppint main(){ //...application code return 0;}

This example, like most short and didactic examples, is a bit artificial. However, the point is to show a real problem that occurs frequently in real projects.

Dealing with the Construction Order Dependency
Will this application run as expected? Maybe. Everything depends on the order in which the objects admin and lf are constructed. If admin is constructed first, lf’s constructor can call admin.getfilename() safely. If, however, your linker chooses the reverse construction order (and it’s at liberty to do so!) all hell breaks loose. The call to admin.getfilename() would cause undefined behavior in this case. As a workaround, you can move the definitions of lf and admin into the same translation unit:

#include "admin.h"#include "logfile.h"Admin admin;Logfile lf(admin.getfilename());int main(){ //application code  return 0;}

While this tweak ensures that admin is constructed before lf, it’s still problematic. The implementer of main.cpp must know the exact order in which the objects should be constructed and declare them accordingly. In real projects, there could be hundreds of objects in dozens of translation units. Grouping all objects in a single translation unit is rarely an option. Besides, future maintainers of main.cpp might mistakenly change the order of definitions, not knowing that by doing so they’re wreaking havoc. To conclude, this isn’t a plausible solution.

Getting Rid of Construction Order Dependency
Instead of forcing all users to know the precise construction order, you want a mechanism of controlling it. Fortunately, such a mechanism is readily available and is quite simple: replace every global object with a local static object inside an accessor function. For example, instead of the global admin object, use the following accessor function:

//getadmin.cpp#include "admin.h"Admin& getadmin(){//create a local static object when this function is //called for the first time static Admin admin; return admin;}

Similarly, replace the global lf with an accessor function. Notice that in the following function, the construction of lf is guaranteed to succeed because the call to getadmin() always returns a reference to a valid Admin object:

//getlogfile.cpp#include "amdmin.h"#include "logfile.h"Logfile& getlogfile(){ static Logfile lf(getadmin().getfilename());//safe return lf;}

This technique neatly solves the order dependency problem. When you call an accessor function for the first time it constructs the local static object and returns a reference to it which you can use safely. This way, you actually determine the construction order of each object! You can call an accessor function from every translation unit. Moreover, subsequent calls return a reference to the same object so you don’t need global objects anymore.

Final Refinements
The use of accessor functions solved the construction order dependency but there’s still one problem left. How do you ensure that lf is constructed before main() starts? The solution is using a global reference to Logfile that is initialized by the proper accessor function:

#include "getlogfile.h"Logfile & ref=getlogfile(); //initialized before main()int main(){ //.. return 0;}
devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist