How it works...

As stated before, a move should not throw an exception to ensure strong exception guarantees (that is, the act of moving an object doesn't possibly corrupt the object), and in most cases, this is possible because a move (unlike a copy) doesn't create resources, it transfers them. The best way to ensure that your move constructors and move assignment operators do not throw is to only transfer member variables using std::move(), as in the following example:

m_answer = std::move(other.m_answer);

Assuming that the member variable you are moving doesn't throw, your class will not either. Using this simple technique will ensure that your move constructors and operators never throw. But what if this operation cannot be used? Let's explore this issue with the following example:

#include <vector>
#include <iostream>

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

public:

the_answer() = default;

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

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

In the preceding example, we create a class with a vector as the member variable. The vector can either be initialized as empty by default, or it can be initialized with a single element. On destruction, if the vector has a value, we output the value to stdout. We implement the move constructor and operator as follows:

public:

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

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

try {
m_answer.emplace(m_answer.begin(), other.m_answer.at(0));
other.m_answer.erase(other.m_answer.begin());
}
catch(...) {
std::cout << "failed to move ";
}

return *this;
}
};

As shown, the move operator is transferring the single element from one instance to the other (not the best way to implement a move, but this implementation can demonstrate the point without being overly complicated). If the vector is empty, this operation will throw, as in the following example:

int main(void)
{
{
the_answer is_42{};
the_answer is_what{};

is_what = std::move(is_42);
}

std::cout << ' ';

{
the_answer is_42{42};
the_answer is_what{};

is_what = std::move(is_42);
}

return 0;
}

Finally, we attempt to move an instance of this class in two different tests. In the first test, both instances are default constructed, which results in empty classes, while the second test constructs the vector with a single element, which results in a valid move. In this case, we were able to prevent the move from throwing, but it should be noted that the resulting classes did not actually perform the move, resulting in both objects not containing the state that was desired. This is why move constructors should never throw. Even if we didn't catch the exception, it would be extremely difficult to assert the state of the program after the throw occurred. Did the move occur? What state is each instance in? In most cases, this type of error should lead to std::terminate() being called as the program enters a corrupt state.

A copy is different because the original class is left intact. The copy is invalid and the programmer can handle this case gracefully, as the original state of the instance being copied is unaffected (hence we mark it const).

Since, however, the instance being moved from is writable, both instances are in a corrupt state and there isn't a good way to know how to handle the program moving forward, as we don't know whether the original instance was left in a state that can be properly handled.

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

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