How it works...

A move-only class is a class that can be moved but cannot be copied. To explore this type of class, let's wrap std::unique_ptr, which itself is a move-only class, in the following example:

class the_answer
{
std::unique_ptr<int> m_answer;

public:

explicit the_answer(int answer) :
m_answer{std::make_unique<int>(answer)}
{ }

~the_answer()
{
if (m_answer) {
std::cout << "The answer is: " << *m_answer << ' ';
}
}

public:

the_answer(the_answer &&other) noexcept
{
*this = std::move(other);
}

the_answer &operator=(the_answer &&other) noexcept
{
m_answer = std::move(other.m_answer);
return *this;
}
};

The preceding class stores std::unique_ptr as a member variable and, on construction, instantiates the memory variable with an integer value. On destruction, the class checks to make sure std::unique_ptr is valid and if so, outputs the value to stdout.

At first glance, we might wonder why we must check for validity as std::unique_ptr is always constructed. The reason std::unique_ptr could become invalid is during a move. Since we are creating a move-only class (and not a non-copyable, non-movable class), we implement the move constructor and move assignment operator, which moves std::unique_ptr. std::unique_ptr, on moving, will transfer the contents of its internal pointer from one class to another, resulting in the class being moved from storing an invalid pointer (that is, nullptr). In other words, even though this class cannot be null-constructed, it can still store nullptr if it is moved, as in the following example:

int main(void)
{
the_answer is_42{42};
the_answer is = std::move(is_42);

return 0;
}

As shown in the preceding example, only one class outputs to stdout as only one instance is valid. Like std::unique_ptr, a move-only class ensures that you always have a 1:1 relationship between the total number of resources being created and the total number of actual instantiations occurring.

It should be noted that since we are using std::unique_ptr, our class becomes a move-only class whether we like it or not. For example, attempting to add a copy constructor or copy assignment operator to enable the ability to copy will result in a compilation error:

the_answer(const the_answer &) = default;
the_answer &operator=(const the_answer &) = default;

In other words, every class that includes a move-only class as a member also becomes a move-only class itself. Although this might seem undesirable, you must first ask yourself: do you really need a class to be copyable? The likely answer is no. In fact, in most cases, even before C++11, most—if not all—of the classes that we work with should be move-only. The ability of a class to be copied when it should be moved can lead to wasted resources, corruption, and so on, which is one of the reasons move semantics were added to the specification. Move semantics allow us to define how we want the resources we allocate to be handled, and it provides us with a way to enforce the desired semantics at compile time. 

You might wonder how the preceding example would be converted to allow for copying. The following example leverages a shared pointer to accomplish this:

#include <memory>
#include <iostream>

class the_answer
{
std::shared_ptr<int> m_answer;

public:

the_answer() = default;

explicit the_answer(int answer) :
m_answer{std::make_shared<int>(answer)}
{ }

~the_answer()
{
if (m_answer) {
std::cout << "The answer is: " << *m_answer << ' ';
}
}

auto use_count()
{ return m_answer.use_count(); }

The preceding class uses std::shared_ptr instead of std::unique_ptr. Under the hood, std::shared_ptr keeps track of the number of copies that are made and only deletes the pointer it stored when the total number of copies is 0. In fact, you can query the total number of copies using the use_count() function.

Next, we define the move constructor, move assignment operator, copy constructor, and copy assignment operator, as follows:

public:

the_answer(the_answer &&other) noexcept
{
*this = std::move(other);
}

the_answer &operator=(the_answer &&other) noexcept
{
m_answer = std::move(other.m_answer);
return *this;
}

the_answer(const the_answer &other)
{
*this = other;
}

the_answer &operator=(const the_answer &other)
{
m_answer = other.m_answer;
return *this;
}
};

These definitions could have also been written using the = default syntax as these implementations are the same thing. Finally, we test this class using the following:

int main(void)
{
{
the_answer is_42{42};
the_answer is = is_42;
std::cout << "count: " << is.use_count() << ' ';
}

std::cout << ' ';

{
the_answer is_42{42};
the_answer is = std::move(is_42);
std::cout << "count: " << is.use_count() << ' ';
}

return 0;
}

If we execute the preceding code, we get the following:

In the preceding tests, we first create a copy of our class and output the total number of copies to see that two copies were in fact created. The second test performs std::move() instead of a copy, which results in only one copy being created as expected.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset