Make interfaces easy to use correctly and hard to use incorrectly.” — Scott Meyers
Disclaimer
In Cpp there are a couple of patterns that are recommended ideas for handling resources. One of them is using move only classes. Move only classes mitigate double delete resources. For resources like TCP network connections that can be a problem. In this post I hope to show the utility of move only classes. This post is not meant to be a thorough explanation of move semantics. If you are looking for something like that read and absorb everything in this book
RAII (Resource acquisition is initialization) and move only/non copy.
https://en.cppreference.com/w/cpp/language/raii.html
Comparing Copy vs Move
Below is the code we will examine the differences between a normal copy and a move only cpp class. First we will start with the “normal” copy constructor cpp class, and run it with a super trivial use case. Second, we will swap the copy constructors with move constructors. We will then run it with a similar super trivial use case. Finally we will do our examination of the differences, and discuss.
// fake acquiring a resource by returning a random number between 0 and 200
int acquire() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(0, 200);
return dist(gen);
}
// Class to manage a resource
class resource {
public:
// constructor: acquire the resource
resource() : item(acquire()) {
std::cout << "C(i) item: " << item << std::endl;
};
// assignment operator assign rhs item to this item
resource &operator=(resource &rhs) noexcept {
std::cout << "= Item: this: " << this->item << " rhs: " << rhs.item
<< std::endl;
this->item = rhs.item;
return *this;
};
// copy constructor assign rhs item to this item.
// this could just call = but we want to seen anther print
resource(resource &rhs) noexcept {
std::cout << "Copy() Item: this: " << this->item << " rhs: " << rhs.item
<< std::endl;
this->item = rhs.item;
};
// destructor just prints the resource
~resource() { std::cout << "destroying: " << item << std::endl; };
private:
int item{-1}; // the resource
};
int main() {
auto r = resource();
auto r1 = r;
return 0;
};
Below is the main function, what do you think happens?
- Resource is created with the constructor?Wrong!
- resource is assigned to r?
- then it is destroyed?
Wrong!
- Resource is created with the constructor? correct
- Resource is assigned to r? correct
- the it is destroyed? correct, but something else is destroyed too!
This is what happens
C(i) item: 4
Copy() Item: this: -1 rhs: 4
destroying: 4
destroying: 4
The output from the main function shows that 4 is destroyed twice. This is problematic if 4 was a socket or a similar resource. We would only want to close it once and not many times over the lifetime of the program. This is where move only classes come in.
The move only version
swap out the = operator and the copy constructor for the follow
resource &operator=(resource &&rhs) noexcept {
std::cout << "= Item: this: " << this->item << " rhs: " << rhs.item << std::endl;
std::swap(this->item, rhs.item);
return *this;
};
resource(resource &&rhs) noexcept {
std::cout << "Copy() Item: this: " << this->item << " rhs: " << rhs.item << std::endl;
std::swap(this->item, rhs.item);
};
Also (=) delete the = operator and copy constructor
resource(resource &rhs) = delete; resource &operator=(resource &rhs) = delete;
Since the copy versions no longer exist the compiler will throw a fit and return an error.
MoveOnlyClass.cpp: In function ‘int main()’:
MoveOnlyClass.cpp:40:13: error: use of deleted function ‘constexpr resource::resource(const resource&)’
40 | auto r1 = r;
| ^
MoveOnlyClass.cpp:13:7: note: ‘constexpr resource::resource(const resource&)’ is implicitly declared as deleted because ‘resource’ declares a move constructor or move assignment operator
13 | class resource {
| ^~~~~~~~
Compiler errors oh no
The compiler does not automatically use the move version in this instances. It needs to be prompted to, so we need to change one line in main.we need to wrap r in the move function. this essentially asks the compiler to use our move assignment operator.
auto r1 =r;
to
auto r1 = std::move(r);
Look 180 was only destroyed once
C(i) item: 180 Copy() Item: this: -1 rhs: 180 destroying: 180 destroying: -1
but, the destructor is still called twice
We swapped out our copy constructor and assignment operator for move only version. In a move versions, resources are swapped, not assigned or copied. This enforces the “move” in move semantics.
Why do any of it
Move only classes are really useful for managing resources. They are beneficial when you want to acquire the resource only once and close it only once. Move only classes can be useful when working with resources like sockets, more precisely TCP connection. The problem this alleviates is the double deletion. In the copy version of the resource class. After assignment delete was called on the resource we acquired. Image instead of an random integer that member variable was a TCP connection, and the destructor closed the connection. We would have closed the connection but used it as the other variable. We would probably also assume the connection is still open.
Move only classes make resource manage management easy to do, and help mitigate the double delete problem. There are other alternatives to mitigating the double delete problem. One approach is using only references or pointers to the instantiation. Another option is to wrap it in a smart pointer. both of these require the user to have internal knowledge of the class implementation. The user essentially needs to use the class in a specific way or with a particular algorithm. Using Move only classes makes that specific way or particular algorithm intrinsic to the class its self. This makes class easy to use correctly and harder to use wrong. (queue: the Scott Meyers quote)
Leave a Reply