Last Update:
Lazy Initialisation in C++
Table of Contents
Lazy initialisation is one of those design patterns which is in use in almost all programming languages. Its goal is to move the object’s construction forward in time. It’s especially handy when the creation of the object is expensive, and you want to defer it as late as possible, or even skip entirely.
Keep reading and see how you can use this pattern with the C++ Standard Library.
Update: Read the next article about Lazy init and Multi-threading.
This article is a guest post from Mariusz Jaskółka:
Mariusz is a professional programmer for whose C++ is both his job and passion. He works for Novomatic Technologies Poland and writes texts to Polish blog cpp-polska. Besides programming he loves mushroom picking, mountain hikes and everything music-related. Check out his github and linkedin profiles.
Originally published in Polish at cpp-polska.pl
Problem description
Let’s make a real-life example. We have an RAII object that represents a file on the hard drive. We deliberately won’t use std::ifstream
class, as it allows late file opening so that using late initialisation pattern would be pointless.
Consider the following class:
class File
{
public:
File(std::string_view fileName)
{
std::cout << "Opening file " << fileName << std::endl;
}
~File()
{
std::cout << "Closing file" << std::endl;
}
File(const File&) = delete;
File(File&&) = default;
File& operator=(const File&) = delete;
File& operator=(File&&) = default;
void write(std::string_view str)
{
std::cout << "Writing to file: " << str << std::endl;
}
};
As we can see, the file is opened in the constructor, and that’s the only place we can do it.
We can use such class to save a configuration file:
class Config
{
File file;
public:
Config() : file{"config.txt"}
{
std::cout << "Config object created" << std::endl;
}
void addOption(std::string_view name, std::string_view value)
{
file.write(name);
file.write(" = ");
file.write(value);
file.write("\n");
}
};
Here’s a simple usage:
int main()
{
Config c;
std::cout << "Some operations..." << std::endl;
c.addOption("dark_mode", "true");
c.addOption("font", "DejaVu Sans Mono");
}
The problem with this implementation is that we presumably open the file long time before we really need to write to it. This may block other processes from manipulating this file, which is an undesirable side effect. We would instead open the file when the first call to addOption
function occurs.
We can achieve such behaviour in several ways. Let’s have a look.
The First Way - Uninitialized Raw Pointer:
Pointers seem to be the solution at first glance – they can point to some value or to “nothing” (nullptr
). Let’s go back to the example and then discuss why this is rather a bad idea.
class Config
{
File* file{nullptr};
public:
Config()
{
std::cout << "Config object created" << std::endl;
}
~Config()
{
delete file;
}
// ah... need to implement rule of 5...7 now!
void addOption(std::string_view name, std::string_view value)
{
if (!file)
file = new File{"config.txt"};
file->write(name);
file->write(" = ");
file->write(value);
file->write("\n");
}
};
In modern C++, holding allocated memory on the heap, under a raw pointer is considered to be a bad idea in most scenarios. First of all, mixing them with the exception mechanism can lead us to memory leaks. They also require manual memory freeing, which can be bypassed using the handy and lightweight RAII design pattern.
If we declared a destructor it also means we have to follow the Rule of Five and implement copy ctor, assignment op and move semantics for the class.
The Second Way – Smart Pointer
Having a smart pointer can free us from extra boilerplate code:
class Config
{
std::unique_ptr<File> file{};
public:
Config()
{
std::cout << "Config object created" << std::endl;
}
void addOption(std::string_view name, std::string_view value)
{
if (!file)
file = std::make_unique<File>("config.txt");
file->write(name);
file->write(" = ");
file->write(value);
file->write("\n");
}
};
Our problem is solved in a much more elegant way. Compared to the original implementation, this method has one drawback though - the object is allocated on the heap. Allocation on the heap requires a system call (syscall), and the number of system calls should be rather minimized. Using objects from under the pointer might also cause less possibility of program optimization compared to objects referenced from the stack. That can lead us to another solution …
The Third Way – std::optional
(C++17)
class Config
{
std::optional<File> file{};
public:
Config()
{
std::cout << "Config object created" << std::endl;
}
void addOption(std::string_view name, std::string_view value)
{
if (!file)
file.emplace("config.txt");
file->write(name);
file->write(" = ");
file->write(value);
file->write("\n");
}
};
We can notice that the above code doesn’t differ very much with the previous one. The unique_ptr
and optional
references are similar, but the implementation and purpose of those classes vary significantly. First of all, in the case of std::optional
our objects memory is on the stack.
It is worth mentioning that if you are not using C++17, but some older language version, you can use the Boost.Optional library, which implements the almost identical class.
(Smart) Pointers vs std::optional
unique_ptr
is – as the name implies – a wrapper around the raw pointer, whileoptional
object contains memory required to its reservation as a part of the object.- Default constructor of
unique_ptr
class just sets the underlying pointer tonullptr
, whileoptional
object allocation also allocates (on the stack) memory for an underlying object. - make_unique helper function does two things – it reserves memory required to object construction on the heap, and after that, it constructs an object using that memory. Its behaviour can be compared to the ordinary new operator. On the other hand, the member function
optional::emplace
, which could be considered as an equivalent, only calls object construction with the usage of on-stack preallocated memory – so it works like less known placement new operator.
The consequences of the above features are:
- Copy constructor of
unique_ptr
doesn’t exist. We can use another smart pointer –shared_ptr
so that we could copy the pointer, but it would still point one object on the heap. Theoptional
class, on the other hand, invokes deep copy of the underlying object when is copied itself. The situation is similar in the case of the operator=
. - Move constructor of
unique_ptr
class doesn’t invoke deep copy either. It just moves underlying object management to a different instance. Theoptional
class invokes underlying object move constructor. - Destructor of
unique_ptr
class not only destroys underlying object (calls destructor of it), but also frees memory occupied by it – so it works exactly likeoperator delete
.optional
’s destructor calls underlying object’s destructor, but it doesn’t have to free any memory – it will be available to next objects appearing on the stack.
Which Option Should I Use?
The use of the optional
class described earlier may not be the first that comes to mind those who use it. Instead, it is a class that expresses that an object is present or is not. Here we revealed the fact that the object does not yet exist, but it will probably be in the future. This is, however, perfectly valid usage of this class.
The answer to the question “which method should I use to express late initialisation?” isn’t that trivial though. I would advise beginners to use optional
by default (form std or boost). However, if we examine this issue in more detail, we can draw the following conclusions:
- Smart pointers should be used mainly when we want to postpone the reservation of some large amount of memory, e.g. intended for storing the contents of an image file.
std::optional
should be preferred when not the memory (its amount) is essential, but the reservation of other types of resources (such as file handles, network sockets, threads, processes). It is also worth using it when the construction of the object is not possible immediately but depends on some parameter whose value is not yet known. In addition, using this class will usually be more efficient - especially if we have, for example, a large vector of such objects and want to iterate over them.
We also cannot forget about the properties of the described classes, especially about how they are copied and moved.
Update: Read the next article about Lazy init and Multi-threading.
Back to you:
- Do you use some form of lazy initialisation?
- What techniques do you use to implement it?
- Maybe you have some good example?
Let us know in comments
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: