I am learning the behavior of C++'s special member function, using a naive String
class as example. (The code is modified from this tutorial) Here is the implementation so-question.cpp
:
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
class String {
private:
uint32_t size;
char *buffer;
public:
String() {
cout << "Default No-Parameter Constructor" << endl;
this->size = 0;
this->buffer = nullptr;
}
String(const char *name) {
cout << "Parameterized Constructor" << endl;
this->size = std::strlen(name);
this->buffer = new char[this->size];
memcpy(this->buffer, name, this->size);
}
String(const String &other) {
cout << "Copy Construtor" << endl;
this->size = other.size;
this->buffer = new char[this->size];
memcpy(this->buffer, other.buffer, this->size);
}
String &operator=(const String &other) {
cout << "Copy Assignment Operator" << endl;
if (this != &other) {
// The buffer size of `this` is different from the `other`'s. So we need
// to delete `this` and reallocate memory for `this` with the size of
// `other`.
delete[] this->buffer;
this->size = other.size;
this->buffer = new char[this->size];
memcpy(this->buffer, other.buffer, this->size);
}
return *this;
}
String(String &&other) {
cout << "Move Constructor" << endl;
this->size = other.size;
/**
* In the copy constructor, we have to allocate memory.
* In the move constructor, we don't have to allocate any more memory. We
* just have to change a couple of variables so it's very efficient.
*/
this->buffer = other.buffer;
/**
* NOTE: Here, the destructor of "other" will deallocate "other.buffer" and
* we've stolen the buffer so we don't want that to happen. So to deal
* with this all we have to do is set "other.buffer" to point to
* nullptr. And it is safe to delete a nullptr.
*/
other.size = 0;
other.buffer = nullptr;
}
String &operator=(String &&other) {
cout << "Move Assignment Operator" << endl;
if (this != &other) {
this->size = other.size;
/**
* Here, the object `buffer` to which `this` points, needs to be freed up,
* because `this` is going to point to `other`'s memory. If it's not done,
* then we have a memory leak.
*/
delete[] this->buffer;
this->buffer = other.buffer;
other.size = 0;
other.buffer = nullptr;
}
return *this;
}
~String() {
cout << "Destructor: ";
// NOTE: a potential bug is here. If `this` has pointed to a nullptr and
// size is non-zero, then it causes error:
// Segmentation fault (core dumped)
for (uint32_t i = 0; i < this->size; ++i) {
cout << this->buffer[i];
}
cout << '\n';
delete[] this->buffer;
}
friend ostream &operator<<(ostream &out, const String &test);
};
ostream &operator<<(ostream &out, const String &test) {
out << "String: ";
for (uint32_t i = 0; i < test.size; ++i) {
out << test.buffer[i] << ' ';
}
return out;
}
String get_String() { return String(); }
The String
class can be tested with the following code:
int main() {
String test1;
test1 = get_String();
String test2{"test2"};
String test3{test2};
cout << "test1: " << test1 << endl;
test1 = test3;
cout << "test2: " << test2 << endl;
cout << "test3: " << test3 << endl;
cout << "test1: " << test1 << endl;
vector<String> vec;
vec.push_back(String("rvalue"));
vec.push_back(test1);
cout << "vec[0] " << vec[0] << endl;
cout << "vec[1] " << vec[1] << endl;
cout << '\n';
cout << "Test move-from object:\n";
test2 = std::move(test1);
cout << "test2: " << test2 << endl;
cout << "test1: " << test1 << endl;
return 0;
}
And the output message is:
Default No-Parameter Constructor
Default No-Parameter Constructor
Move Assignment Operator
Destructor:
Parameterized Constructor
Copy Construtor
test1: String:
Copy Assignment Operator
test2: String: t e s t 2
test3: String: t e s t 2
test1: String: t e s t 2
Parameterized Constructor
Move Constructor
Destructor:
Copy Construtor
Copy Construtor
Destructor: rvalue
vec[0] String: r v a l u e
vec[1] String: t e s t 2
Test move-from object:
Move Assignment Operator
test2: String: t e s t 2
test1: String:
Destructor: rvalue
Destructor: test2
Destructor: test2
Destructor: test2
And the code can be run with the command g++ -Wall -std=c++17 -std=gnu++17 so-question.cpp && ./a.out
.
The result looks good and makes sense to me.
However, I have one question, is it true that in theory and in practice that when using move constructor and move assignment operator, the move-from object shall be invalid. I am asking this because I am just wondering what if I would like to have two objects points to the same memory location. For example,
String test1{"apple"};
String test2;
test2 = std::move(test1);
test1
becomes invalid after it's moved. But what if I want both test1
and test2
both points to the same string in memory apple
. (copy can't be used here because then it will not be the same memory) This can be done by changing the code in move assignment operator. But does it violate some software design pattern and should be avoided or there's a known method to address this behavior? Thank you.
Update: Fix the code to make it as minimum reproducible.
using
statements. I suspect the code containsusing namespace std;
but I can't be sure and that is something I would want to comment on. It is actually best to provide the code as files so that we might be able to test it ourselves. Right now the code is very hard to review. \$\endgroup\$