Hillsoft Blog

Exploring Maths and Computing

C++ Lifetime Quiz Part 1

This post is part of the C++ Lifetime Quiz series.

Many object-oriented languages have constructors, a special type of method for creating objects. These have a wide variety of uses including custom type conversion logic, calculating derived data the object needs to store, validating inputs, and resource acquisition.

The natural converse of a constructor is a destructor, which runs when the object’s lifetime ends instead of when it starts. This feature, however, is not included in most programming languages; garbage collection in particular is often a barrier to this as it makes it impossible to predict when an object will actually be destroyed. C++ is one of the few languages that do allow destructors to be written and in this series we will explore the wide range of edge cases that can occur.

Even if you’re not writing your own destructors, they are widely used throughout the standard library. Some examples include ‘data-owning’ classes such as std::string, std::vector, and std::map which all use the destructor to free the memory they were using. The std::unique_ptr class similarly uses its destructor to free the memory it points to and std::shared_ptr uses its destructor to decrement a reference counter and, if that counter hits 0, free the memory it points to. Destructors aren’t only used for memory management though; std::unique_lock is used in multi-threaded code, locking a std::mutex in its constructor and unlocking it in its destructor.

In all of the examples below, the output is hidden until you choose to reveal it. I suggest you treat this as a quiz and try to work out the example programs’ output by hand before revealing the correct answer.

To demonstrate clearly when constructors and destructors are being called, we will use the Speaker class defined below.


class Speaker {
public:
explicit Speaker(const char* word)
: word_(word), depth_(1) {
std::cout << "Constructing: "
<< depth_ << " "
<< word_ << std::endl;
}

~Speaker() {
std::cout << "Destructing: "
<< depth_ << " "
<< word_ << std::endl;
word_ = "DANGER Deleted";
}

void speak() const {
std::cout << "Speaking: "
<< depth_ << " "
<< word_ << std::endl;
}

protected:
const char* word_;
int depth_;
};

Local Storage

Let’s start with a simple example of this in action:


int main() {
Speaker s{"A"};
std::cout << "Running" << std::endl;
return 0;
}
Spoiler

Constructing: 1 A
Running
Destructing: 1 A

The s variable is valid until the end of main and so the destruction happens at the very end. The rule in general is that destruction of local variables happens when they go out of scope. Some examples to demonstrate this:


void foo() {
Speaker sfoo{"foo"};
}

int main() {
Speaker s1{"A"};
if (true) {
Speaker s2{"B"};
}
foo();
return 0;
}
Spoiler

Constructing: 1 A
Constructing: 1 B
Destructing: 1 B
Constructing: 1 foo
Destructing: 1 foo
Destructing: 1 A

In the previous examples, there has only ever been one variable that needed destructing at any moment. What happens if we have two variables created in the same scope?


int main() {
Speaker s1{"A"};
Speaker s2{"B"};
return 0;
}
Spoiler

Constructing: 1 A
Constructing: 1 B
Destructing: 1 B
Destructing: 1 A

The rule here is that they are always destroyed in the reverse of the creation order so, as s1 was created first, it is destroyed last. This rule also holds in other contexts which we will see later.

Global Storage

We can also have global variables and, as with local variables, their constructor is run at their creation and their destructor when they are destroyed.


Speaker sglobal{"Global"};
Speaker sglobal2{"Global2"};

int main() {
Speaker slocal{"Local"};
return 0;
}
Spoiler

Constructing: 1 Global
Constructing: 1 Global2
Constructing: 1 Local
Destructing: 1 Local
Destructing: 1 Global2
Destructing: 1 Global

As you might expect, global variables are constructed before the main method is called and destructed after the main method completes. This, perhaps surprisingly, means that parts of your program can run before main starts and after it finishes! The global variables are also constructed in the natural order (from the top of the file down) and are destructed in the reverse order to their creation.

Now, what happens if we have global variables defined across multiple files?


a.cpp:
Speaker sa{"A"};

main.cpp:
Speaker sb{"B"};

int main() {
return 0;
}
Spoiler

There are actually two possible outputs here:


Constructing: 1 A
Constructing: 1 B
Destructing: 1 B
Destructing: 1 A

or


Constructing: 1 B
Constructing: 1 A
Destructing: 1 A
Destructing: 1 B

As we saw in the previous example, the order in which global variables are constructed is well-defined within a file but, as this example shows, it is not defined across files. The standard simply gives us no way to decide which of these two global variables is constructed first. In practice, this makes defining singletons as global variables rather risky; we’ll see a better approach (static storage) later but, as with most features in C++, that also has its pitfalls. We are, however, still guaranteed that destruction will happen in the reverse order of creation.

Now, what if we define a global variable in a header instead? In my testing, this actually failed to link due to the symbol having multiple definitions. I’ve seen linkers in the past allow this and, in that case, you would actually get a completely separate global variable for each .cpp file that includes the header. The way around this is to use the extern keyword; this is similar to function declarations in headers in that it doesn’t actually create a global variable and instead only reserves a name and type. This variable must then be initialised in a single .cpp file.

Static Storage

Static storage is typically used to solve the same sorts of problems as global variables, but with a few key differences. The biggest one of these is that they are defined within a function, similarly to local storage, and can only be accessed directly from within the scope in which they are defined. We can clearly demonstrates the global-like behaviour of static variables with a simple example that counts how many times a method is called:


int getInt() {
static int i = 0;
i++;
return i;
}

int main() {
std::cout << getInt() << std::endl;
std::cout << getInt() << std::endl;
return 0;
}
Spoiler

1
2

If you tweak this example and try to access i in main, you will get an error, just as you would if it were a local variable.

Now let’s use an example with our Speaker object to see exactly when static variables are created and destroyed.


void foo() {
static Speaker s{"static"};
}

int main() {
std::cout << "Starting main" << std::endl;
foo();
foo();
std::cout << "Ending main" << std::endl;
}
Spoiler

Starting main
Constructing: 1 static
Ending main
Destructing: 1 static

We can see here that static variables are constructed when they are first used and, as with global variables, they are destroyed at the end of the program. Interestingly, static variables still follow the rule that they are destroyed in the reverse order to their creation. This means that the destruction order of this type of global variable can depend on the earlier program flow. Static variables are great if you have singletons that depend on each other in their constructors (as long as there are no circular dependencies) but this destruction order can be a problem if the singletons depend on each other in their destructors.

We may also wonder what happens if a static variable is used within another static variables constructor. Which is destroyed first?


const Speaker& getSpeakerSingleton() {
static Speaker s{"Singleton"};
return s;
}

class SpeakerUser {
public:
SpeakerUser() {
getSpeakerSingleton();
}

~SpeakerUser() {
std::cout << "Destructing SpeakerUser" << std::endl;
}
};

const SpeakerUser& getSpeakerUserSingleton() {
static SpeakerUser su;
return su;
}

int main() {
getSpeakerUserSingleton();
return 0;
}
Spoiler

Constructing: 1 Singleton
Destructing SpeakerUser
Destructing: 1 Singleton

This gives us our answer! The practical upshot of this is that if we use a static variable in our constructor, it can also be safely used in our destructor. However, if we don’t use a static variable in the constructor, what happens if we try using it in the destructor? What if it has already been destroyed?


const Speaker& getSpeakerSingleton() {
static Speaker s{"Singleton"};
return s;
}

class SpeakerUser {
public:
SpeakerUser() {
}

~SpeakerUser() {
std::cout << "Destructing SpeakerUser" << std::endl;
getSpeakerSingleton().speak();
}
};

const SpeakerUser& getSpeakerUserSingleton() {
static SpeakerUser su;
return su;
}

int main() {
getSpeakerUserSingleton();
return 0;
}
Spoiler

Destructing SpeakerUser
Constructing: 1 Singleton
Speaking: 1 Singleton
Destructing: 1 Singleton

The output here looks perfectly sensible, but we have actually done something vary dangerous; we have constructed a static variable after the main method exits! According to the standard, this is undefined behaviour; the wording is a mouthful but it states that the reverse construction order rule must hold. Therefore, because the Speaker is constructed after the SpeakerUser, the Speaker must be destroyed first but this isn’t possible as it doesn’t exist until after that point! In practice, this sort of program may work, but it is definitely better avoided.


const Speaker& getSpeakerSingleton() {
static Speaker s{"Singleton"};
return s;
}

class SpeakerUser {
public:
SpeakerUser() {
}

~SpeakerUser() {
std::cout << "Destructing SpeakerUser" << std::endl;
getSpeakerSingleton().speak();
}
};

const SpeakerUser& getSpeakerUserSingleton() {
static SpeakerUser su;
return su;
}

int main() {
getSpeakerUserSingleton();
getSpeakerSingleton();
return 0;
}
Spoiler

Constructing: 1 Singleton
Destructing: 1 Singleton
Destructing SpeakerUser
Speaking: 1 DANGER Deleted

In this example, we see another, more dangerous, misuse of static variables. The Speaker is constructed after the SpeakerUser so it must be destroyed first. Because static variables are only ever constructed once, this means that the SpeakerUser tries to access a deleted variable, something that falls very comfortably into the realms of undefined behaviour that should be avoided.

Our final concern here may be about how the destruction order of static and global variables interact.


const Speaker& getSpeakerSingleton() {
static Speaker s{"Singleton"};
return s;
}

class SpeakerUser {
public:
SpeakerUser() {
getSpeakerSingleton();
}

~SpeakerUser() {
std::cout << "Destructing SpeakerUser" << std::endl;
}
};

Speaker s1{"A"};
SpeakerUser su;
Speaker s2{"B"};

int main() {
return 0;
}
Spoiler

Constructing: 1 A
Constructing: 1 Singleton
Constructing: 1 B
Destructing: 1 B
Destructing SpeakerUser
Destructing: 1 Singleton
Destructing: 1 A

This example shows that they are interleaved with each other, using the reverse construction order tie-breaking rule.

Dynamic Storage

The last type of storage is dynamic storage. This covers any object created by new.


int main() {
Speaker* sp = new Speaker{"A"};
return 0;
}
Spoiler

Constructing: 1 A

This example neatly shows what makes dynamic storage special (and dangerous); the destructor doesn’t get called automatically! These objects will exist in memory and not have their destructors called until we explicit delete them. Forgetting to do this can cause hard to debug memory leaks, so it’s almost always better to use the standard library’s smart pointer classes instead.


int main() {
Speaker* sp = new Speaker{"A"};
delete sp;
return 0;
}
Spoiler

Constructing: 1 A
Destructing: 1 A

We could consider one more example for dynamic storage. Because we call the destructor manually, we could try calling it twice.


int main() {
Speaker* sp = new Speaker{"A"};
delete sp;
delete sp;
return 0;
}
Spoiler

Constructing: 1 A
Destructing: 1 A
free(): double free detected in tcache 2
** Program crashes **

Ah, better to not do that.

Another thing to note with dynamic storage variables is that the object is not tied to the name at all; sp is a unrecognised command emph to a Speaker, not a Speaker itself. We can see this in the following example.


int main() {
Speaker* sp = new Speaker{"A"};
Speaker* sp2 = sp;
sp->speak();
sp2->speak();
delete sp2;
sp->speak();
return 0;
}
Spoiler

Constructing: 1 A
Speaking: 1 A
Speaking: 1 A
Destructing: 1 A
Speaking: 6979600

Even though we have two variables, sp and sp2, there is only ever one Speaker. This means that calling speak on sp after deleting sp2 is undefined behaviour, explaining the garbled output we get here.

Member Variables

Let’s add a barebones class that has a Speaker as a member variable:


class SpeakerHolder {
public:
SpeakerHolder(const char* word)
: s(word) {
s.speak();
}

~SpeakerHolder() {
s.speak();
}

protected:
Speaker s;
};

And a minimal example using it; the question is whether s is constructed and destructed before or after SpeakerHolder.


int main() {
SpeakerHolder sh{"A"};
}
Spoiler

Constructing: 1 A
Speaking: 1 A
Speaking: 1 A
Destructing: 1 A

The answer is that we get the behaviour we probably want, the member variable s is totally safe to use in both the constructor and destructor of the SpeakerHolder.

Posted 3rd of September 2023
Filed under [series] C++ Lifetime Quiz, Practical Programming, C++



If you enjoyed this post, please follow us on Facebook for future updates!