How it works...

In this recipe, we will learn how to make a class movable. To start, let's examine a basic class definition:

#include <iostream>

class the_answer
{
int m_answer{42};

public:

the_answer() = default;

public:

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

int main(void)
{
the_answer is;
return 0;
}

In the preceding example, we create a simple class with a private integer member that is initialized. We then define a default constructor and a destructor that outputs to stdout when an instance of the class is destroyed. By default, this class is movable, but the move operation mimics a copy (in other words, there is no difference between a move or a copy with this simple example).

To really make this class movable, we need to add both a move constructor and a move assignment operator as follows:

the_answer(the_answer &&other) noexcept;
the_answer &operator=(the_answer &&other) noexcept;

Once we add these two functions, we will be able to use the following to move our class from one instance to another:

instance2 = std::move(instance1);

To support this, in the preceding class, we will not only add the move constructor and assignment operator, but we will also implement a default constructor to provide a valid moved-from state to our example class, as follows:

#include <iostream>

class the_answer
{
int m_answer{};

public:

the_answer() = default;

explicit the_answer(int answer) :
m_answer{answer}
{ }

As shown, the class now has a default constructor and an explicit constructor that takes an integer argument. The default constructor initializes the integer memory variable, which represents our moved-from or invalid state:

public:

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

As shown in the preceding example, we output the value of our integer member variable when the class is destroyed, but in this case, we first check to make sure the integer variable is valid:

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

the_answer &operator=(the_answer &&other) noexcept
{
if (&other == this) {
return *this;
}

m_answer = std::exchange(other.m_answer, 0);
return *this;
}

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

Finally, we implement the move constructor and assignment operators. The move constructor simply calls the move assignment operator to prevent the need for duplication (as they perform the same action). The move assignment operator first checks to make sure that we are not moving to ourselves. This is because doing so would lead to corruption as the user would expect the class to still contain a valid integer but in fact, the internal integer would inadvertently be set to 0.

We then exchange the integer value and set the original to 0. This is because, once again, a move is not a copy. A move transfers the value from one instance to another. In this case, the instance being moved to starts as 0 and is given a valid integer, while the instance being moved from starts with a valid integer and is set to 0 after the move, resulting in only 1 instance containing a valid integer.

It should also be noted that we have to define the copy constructor and assignment operator. This is because, by default, if you provide a move constructor and assignment operator, C++ will automatically delete the copy constructor and assignment operator if they are not explicitly defined.

In this example, we will compare a move versus a copy, so we define the copy constructor and assignment operator to ensure they are not implicitly deleted. In general, it is best practice to define your destructor, the move constructor, and assignment operator as well as the copy constructor and assignment operator for every class you define. This ensures that the copy/move semantics for every class you write are explicit and intentional:

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

std::cout << ' ';

{
the_answer is{23};
the_answer is_42{42};
is = std::move(is_42);
}

return 0;
}

When the preceding code is executed, we get the following:

In our main function, we run two different tests:

  • The first test creates two instances of our class and copies the contents of one instance to the other.
  • The second test creates two instances of our class and then moves the contents of one instance to the other.

When this example is executed, we see the first test's output was written to twice. This is because the first instance of our class is given a copy of the second instance of our class, which has a valid integer value. The second test's output is only written to once because we are transferring the valid state of one instance to the other, resulting in only one instance having a valid state at any given moment.

There are some notable instances worth mentioning here:

  • Move constructors and assignment operators should never throw exceptions. Specifically, a move operation transfers the valid state of an instance of a type to another instance of that type. At no point should this operation fail as no state is being created or destroyed. It is simply being transferred. Also, it is oftentimes difficult to undo a move operation part of the way through the move. For these reasons, these functions should always be labeled as noexcept (refer to https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-move-noexcept).
  • Move constructors and assignment operators do not include const types in their function signature because the instance being moved from cannot be const since its internal state is being transferred, which implicitly assumes a write is occurring. More importantly, if you label a move constructor or assignment operator as const, it is possible that a copy would occur instead.
  • Unless you intend to create a copy, a move should be used instead, especially for large objects. Just like passing const T& as a function argument to prevent a copy from occurring, when a function is called, a move should be used in place of a copy when a resource is being moved into another variable instead of being copied.
  • The compiler will automatically generate move operations instead of copy operations when possible. For example, if you create an object in a function, configure the object, and then return the object, a move will be automatically performed by the compiler.

Now that you know how to make your classes movable, in the next recipe, we will learn what a move-only type is, and why you might want to use them in your applications.

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

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