rule of zero/three/five

discuss the rule of three/five in cpp.

When we start to define a class or a struct, if we do not follow the rule of three or rule of five, we might got some potential problems when the code becomes complicated.

For me, it seems that I would prefer to use the stack variable compared with the pointer to the class. Some times I may try to use SomeClass *a =new SomeClass, then I may forget to call the delete a sometimes. I would take some time to reorganize the code in order to avoid to use the class/struct pointer, unless it is necessary. I noticed that one reason that push me to use the poiter and the heap allocation to the class is that I may not need to follow the rule of three, for example, I could declare the dedicated struct or class instance in heap and acquire it by pointer, then I put these pointer into STL such as queue and map for further processing.

Although it works sometimes, but it is pretty like the c style. If the code gets comlicated, some memory problems appear and that make the code a little bit hard to maintain. Following the rule of three can avoid this issue and make the code looks more cleaning and tight.

Rule of thumb

Refer to this question to get some ideas. Just not define any of them of define all of them. If the implicitly defined copy constructor does the thing you want, just use the implicit implementation. So when you can use rule of zero, just use rule of zero. Otherwise, you might forget to add specific copy assignment operator which cause error.

rule of zero

Before considering rule of three and five, it is important to consider if you are oversimplify things. In most scenarios, we can use smart pointer in the class direactly to avoid the rule of three and five. Since most standard libraries follows rule of three and five, we can simplify the code by using them direactly and have the rule of zero class. It is rare to write a class that manages the underlying resources from scratch.

Typical case for considering the rule of five is to handle all kinds of specialised I/O things (or library related to the data management). Since these library may use a lot low level c code and we need to handle some low level operations manually.

This blog describes a lot of detials, and is informative.

rule of three

rule of three means that following three functions for struct or class must be provided: destructor, copy constructor, copy assignment operator. For copy constructor, it will be used when you init a struct/class instance by another class instance. For the copu assignmnet operator, it supports to the operation such as SomeClass a = instanceOfA. If one of these functions is used without first being declared by the programmer, it will be implicitly implemented by the compiler. Attention, the compiler generated version is based on the shadow copy, if there are some pointer value in the struct, you may need to set these special functions manually. Otherwise, it might cause the problem of the double free such like this case. there is one block of the memory but there are two points, therefore, it might cause the double free problems.

#include <iostream>
#include <vector>

struct foo{
std::string f1;
std::vector<int> v1;
foo(){
std::cout<<"foo is constructed"<<std::endl;
f1="construct test";
v1.push_back(666);
}

foo(const foo& f){
std::cout<<"copy constructor is called"<<std::endl;
f1="copy test";
v1.push_back(888);
}

foo& operator = (const foo &t) {
std::cout<<"assignment operator is called"<<std::endl;
this->f1="assignment test " + t.f1;
this->v1[0]=999;
return *this;
}

~foo(){std::cout<<"foo is destructed automatically when foo is outof scope"<<std::endl;}
};

void test (){
foo f = foo();

//defualt copy operator
foo g = f;
//check the value of the g
std::cout << "check the value g: "<< g.f1<< "," << g.v1[0] <<std::endl;

//copy constructor
foo i;
std::cout << "default constructor check the value i: "<< i.f1<< "," << i.v1[0] <<std::endl;

//assignment constructor
i = f;
std::cout << "after copy check the value i: "<< i.f1<< "," << i.v1[0] <<std::endl;

return;
}

int main(){
test();
std::cout<<"main finish"<<std::endl;
return 0;
}

//////output//////
foo is constructed
copy constructor is called
check the value g: copy test,888
foo is constructed
default constructor check the value i: construct test,666
assignment operator is called
after copy check the value i: assignment test construct test,999
foo is destructed automatically when foo is outof scope
foo is destructed automatically when foo is outof scope
foo is destructed automatically when foo is outof scope
main finish

according to this case, when the class is initializsed, the default constructor or the copy constructor is called. When two instances are initialized and one instance could get value from another instance, the assignment constructor will be called. The destructor will be called when the instance is out of the scope.

If you want the struct as the key or value of specific STL such as the map, you also need to declare other operator such as “<” operation.

rule of five

After the c++11, more rules are added into the class declaration, these rules are usually called rule of five.

This is a good reference. The main complicated part is the notation of the &&, two functions added here are move constructor and move assignment operator.

We need to figure out the lvalue and the rvalue before discussing more details. this article provides a good explanation.

So the && represents the rvalue in cpp. and the last two rules are necessary if we use the move semantics. The original become unavalible after using the move related semantics.

This article provides a good explanation about why we need to use the rvalue and the move semantics. The main reason is to avoid the performance overhead, since the object is constructed for the orignal case when we execute the copy operation. Basically, there are two opeject, you assign values of one object two another, then both of them are deleted at last. But essentially, when you execute operations such as put a instance into a vector, you may want to provide a new view from the data structure’s view. The original instance might not useful anymore. In that case, in the move constructor, you just need to transfer the ownership of the data from the original instance to the new instance to avoid the extra data copy operation. Then the original instance become invalid. But the two destructors are also need to be called when the program is out of the scope.

Attention, the object that is moved out from the container might still hold the relevent resources, it is necessary to destroy objects that are moved out from the container when design the move constructor. refer to this answer, or refer to this commit to see how to correct a move constructor, originally, there is potential issue of the memory leak, but after the updates, the original object is deleted firstly to make the container empty, then the move operations are executed.

practical examples

this is the example for rule of three and rule of five, inpractical code, just remember there are thee types of constructor, two types of assignment operator and one destructor. When all of these things are considered, we could guarantee the class/struct is a secure one.

class foo {
public:
// default constructor
foo() { std::cout << "foo constructor is called" << std::endl; }
// copy constructor
foo(const foo& other) {
std::cout << "foo copy constructor is called" << std::endl;
}
// move constructor
foo(foo&& other) {
std::cout << "foo move constructor is called" << std::endl;
}

// destructor
~foo() { std::cout << "foo destructor is called" << std::endl; }

// assignment
foo& operator=(const foo& t) {
std::cout << "foo assignment operator is called" << std::endl;
return *this;
}

// move assignment
foo& operator=(foo&& other) {
std::cout << "foo move assignment operator is called" << std::endl;
return *this;
}
};

init the smart ptr

It is worth noting that when we use the shared pointer, we may use the make_shared function, this function will construct a new element by new operation, if we already create an class instance, the copy constructor will be called when we use the make_shared.

This is an exmaple:

int main() {
std::shared_ptr<foo> footest1;
std::cout << "test1" << std::endl;
std::shared_ptr<foo> footest2(nullptr);
std::cout << "test2" << std::endl;
std::shared_ptr<foo> footest3(new foo());
std::cout << "test3" << std::endl;

foo f;
std::shared_ptr<foo> footest4 = std::make_shared<foo>(f);
std::cout << "test4" << std::endl;
//c++14 feature
std::unique_ptr<foo> footest5 = std::make_unique<foo>(f);
std::cout << "test5" << std::endl;

foo* fptr = &f;
std::cout << "test6" << std::endl;

return 0;
}

the output is sth like this:

test1
test2
foo constructor is called
test3
foo constructor is called
foo copy constructor is called
test4
foo copy constructor is called
test5
test6
foo destructor is called
foo destructor is called
foo destructor is called
foo destructor is called

we could find out both the make_unique and the make_shared operation calles the copy constructor of original class.

This is normal in most of the cases, but for some cases, the copy constructor may be labeled by the delete key word such as this:

// copy constructor
foo(const foo& other) = delete;

in this case, there are issues if we init the shared pointer by make_shared since the copy constructor will be called.

foo.cpp:39:40: note: in instantiation of function template specialization 'std::__1::make_shared<foo, foo &>' requested here
std::shared_ptr<foo> footest4 = std::make_shared<foo>(f);
^
foo.cpp:8:3: note: 'foo' has been explicitly marked deleted here
foo(const foo& other) = delete;

The good way here is to init the object when we init the smart pointer by this way:

std::shared_ptr<foo> footest3(new foo());
std::cout << "test3" << std::endl;

in this case, the constructor of the foo class is only called once.

This strategy can be used for init the class that contains the shared ptr that point to foo, such like this:

class bar {
public:
std::shared_ptr<foo> m_foo;
bar() : m_foo(new foo){};
~bar() = default;
};

by this way, the copy constructor of the class foo is not called.

It might be important to go over some basic information about the cpp class at this point, in this article, about the Member initialization in constructors, if the type of the member is a class, such as the exmaple in our case, the constructor of this member can only be accessed in the list of the member initilization. If we do nothing here, the default constructor of the class member will be called when it jump into the body of the constructor. In our case, if we do nothing at the member initilization list, the shared pointer will be constructed by default constructor, which is a nullptr. After this, if we want to give it a valid value, we may need to use the make shared, but this will reigger the copy constructor which is deleted. Another posible solution is to use the reset that replaces the managed object:

class bar {
public:
std::shared_ptr<foo> m_foo;
bar() {
m_foo.reset(new foo);
};
~bar() = default;
};

anyway, it is important to know that the shared pointer will hold a managed object, and that is important distinction compared with the raw pointer. The init of the smart pointer can be views as sum of the following steps 1. declare raw pointer 2.create class instance 3.assign instance to the pointer. The initilization of the shared pointer may trigger the copy constructor, and it is important to use the proper way to init it to avoid the unnecessary copy operation.

key of the map

a commonly use case scenario is to customize an class and make it as the key of the map or set, what function are necessary in this case?
this is an example:

class foo {
public:
char a[16];
// default constructor
foo() { std::cout << "foo constructor is called" << std::endl; }

// destructor
~foo() { std::cout << "foo destructor is called" << std::endl; }

// copy constructor
foo(const foo& other) = default;

// less-than operator
bool operator<(const foo& ob) const { return a < ob.a; }

// move constructor
foo(foo&& other) = delete;

// assignment operator
foo& operator=(const foo& t) = delete;

// move assignment operator
foo& operator=(foo&& other) = delete;
};

int main() {
std::map<foo, int> custMap;
foo f1;
custMap[f1] = 0;
return 0;
}

the default constructor and destructor is necessary since we need to create the object, another necessary one is the copy constructor, this is because we need to copy the instance into the map when we execute the custMap[f1] = 0, another important thing is the less-than operator, this is required by the implementation of the map, it need to know the sequence between differnt objects.

Let’s check the unordered_map, according to this answer, we need to define the hash function and customize the equality operator. The hash function can be wrapped by the functor and this function can be the third parameter for the unordered map, this is an workable example:

class foo {
public:
char a[16];
// default constructor
foo() { std::cout << "foo constructor is called" << std::endl; }

// destructor
~foo() { std::cout << "foo destructor is called" << std::endl; }

// copy constructor
foo(const foo& other) = default;

// less-than operator
// bool operator<(const foo& ob) const { return a < ob.a; }

// equal operator
bool operator==(const foo& ob) const { return memcmp(a, ob.a, 16); }

// move constructor
foo(foo&& other) = delete;

// assignment operator
foo& operator=(const foo& t) = delete;

// move assignment operator
foo& operator=(foo&& other) = delete;
};

struct KeyHasher {
std::size_t operator()(const foo& f) const {
using std::hash;
using std::string;

return (hash<string>()(f.a));
}
};

int main() {
std::unordered_map<foo, int, KeyHasher> custMap;
foo f1;
custMap[f1] = 0;
return 0;
}

in this example, we define another functor that wrap the hash function.

swap operation

when it is ok to call the swap operation?

if check the cppreference here, the move assignment operator and the move constructor of specific type should be defined.

Just pay attention here, if the delete the coresponding functions, there are compiling error when we use the swap.

foo.cpp:44:3: error: no matching function for call to 'swap'
std::swap(f1,f2);
^~~~~~~~~
/Library/Developer/CommandLineTools/usr/include/c++/v1/type_traits:4501:1: note: candidate template ignored: requirement 'is_move_constructible<foo>::value' was not satisfied [with _Tp = foo]
swap(_Tp& __x, _Tp& __y) _NOEXCEPT_(is_nothrow_move_constructible<_Tp>::value &&

It only works when we set :

// move constructor
foo(foo&& other) = default;
// move assignment operator
foo& operator=(foo&& other) = default;

we could see that another instance is created by swap:

int main() {
foo f1;
foo f2;
std::swap(f1,f2);
return 0;
}

//output
foo constructor is called
foo constructor is called
foo destructor is called
foo destructor is called
foo destructor is called

references

A good blog about rule of zero/three/five
https://www.sonarsource.com/blog/the-rules-of-three-five-and-zero/

why to use the rvalue
https://www.educative.io/edpresso/what-is-a-move-constructor-in-cpp

problem of double free (use reference instead of copy)

http://www.cplusplus.com/forum/general/29640/

assignment operator vs copy constructor

https://www.geeksforgeeks.org/copy-constructor-vs-assignment-operator-in-c/

the rule of zero, five, or maybe six.

https://www.modernescpp.com/index.php/c-core-guidelines-constructors-assignments-and-desctructors

推荐文章