The Rule Of Three/Five/Zero¶
Rule Of Three¶
If you find yourself writing a destructor, you probably need to think about copy
⟶ Copy constructor, copy assignment operator
Rule Of Five¶
If you implement one of (best: all three)
Destructor
Copy constructor
Copy assignment operator
your class probably should have a move constructor and move assignment operator too. The compiler won’t create one.
Rule Of Zero¶
Best to not think about all this, and try hard to implement none of those five
⟶ Simple life!
Example: Rule Of Zero, Simplest¶
Member of
std::string
type. Dual copy/move initializers from it.std::string
does resource managementImplements copy
Implements move
As long as you leave everything go, life is easy
Compiler implements what’s possible ⟶ all five
⟶ Rule Of Zero
#include <iostream>
#include <string>
class Owner
{
public:
Owner() = default;
Owner(const std::string& value) // <-- copies value
: _member(value) {}
Owner(std::string&& value) // <-- moves value
: _member(std::move(value)) {}
/*empty*/ // <-- no copy, no move, no dtor
const std::string& member() const { return _member; }
private:
std::string _member;
};
int main()
{
Owner orig("howdy"); // <-- moves temporary std::string object into orig
{
Owner copy1 = orig; // <-- copy ctor
Owner copy2;
copy2 = orig; // <-- copy assignment operator
std::cout << "COPY >>>" << '\n';
std::cout << "orig: " << orig.member() << '\n';
std::cout << "copy1: " << copy1.member() << '\n';
std::cout << "copy2: " << copy2.member() << '\n';
}
{
std::cout << "MOVE >>>" << '\n';
Owner moved1 = std::move(orig); // <-- move ctor
std::cout << "orig: " << orig.member() << '\n';// <-- gone ...
std::cout << "moved1: " << moved1.member() << '\n'; // ... moved here
Owner moved2;
moved2 = std::move(moved1); // <-- move assignment operator
// ...
}
return 0;
}
Example: Rule Of Zero, Move-Only¶
Member of move-only type
std::unique_ptr
Compiler implements what’s possible ⟶ move only
#include <iostream>
#include <memory>
class Owner
{
public:
Owner() = default;
Owner(std::unique_ptr<int>&& value)
: _member(std::move(value)) {}
/*empty*/ // <-- no copy, no move, no dtor
const std::unique_ptr<int>& member() const { return _member; }
private:
std::unique_ptr<int> _member;
};
int main()
{
Owner orig(std::make_unique<int>(42));
// Foo copy = orig; // <-- no
Owner moved = std::move(orig); // <-- move ctor
return 0;
}
Example: Rule Of Three - Explicit Resource Management, Done Wrong¶
Pointer member
Must think of ownership
MyString
allocates⟶ Must deallocate
⟶ User-defined destructor
Compiler generated copy is buggy by default!
⟶ Rule Of Three
#include <iostream>
#include <cstring>
class MyString
{
public:
MyString()
{
_cstr = new char[1]{'\0'}; // <-- allocation
}
MyString(const char* cstr)
{
_cstr = new char[strlen(cstr) + 1];
strcpy(_cstr, cstr); // <-- allocation
}
~MyString()
{
delete[] _cstr; // <-- deallocation
}
private:
char* _cstr;
};
int main()
{
MyString orig("howdy");
MyString copy1 = orig; // <-- compiler generated copy ctor: WRONG!!
return 0;
}
|
|
Example: Rule Of Three - Explicit Resource Management, Done Almost Right¶
Copy constructor (btw. delegating work to an existing constructor)
Copy assignment operator
Slightly wrong ⟶ self assignment
Perfectly legal in C, realistic in certain corner cases
#include <iostream>
#include <cstring>
class MyString
{
public:
MyString()
{
_cstr = new char[1]{'\0'};
}
MyString(const char* cstr)
{
_cstr = new char[strlen(cstr) + 1];
strcpy(_cstr, cstr);
}
~MyString()
{
delete[] _cstr;
}
// copy
MyString(const MyString& from) // <-- copy constructor
: MyString(from._cstr) {} // <-- delegating constructor
MyString& operator=(const MyString& from) // <-- copy assignment operator
{
delete[] _cstr;
_cstr = new char[strlen(from._cstr) + 1];
strcpy(_cstr, from._cstr);
return *this;
}
private:
char* _cstr;
};
int main()
{
MyString orig("howdy");
MyString copy1 = orig; // <-- compiler generated copy ctor: RIGHT!!
copy1 = copy1; // <-- self assignment: WRONG!!
return 0;
}
Example: Rule Of Three - Explicit Resource Management, Done Right¶
Final tweak: add self assignment check
⟶ Pooh
#include <iostream>
#include <cstring>
class MyString
{
public:
MyString()
{
_cstr = new char[1]{'\0'};
}
MyString(const char* cstr)
{
_cstr = new char[strlen(cstr) + 1];
strcpy(_cstr, cstr);
}
~MyString()
{
delete[] _cstr;
}
MyString(const MyString& from)
: MyString(from._cstr) {}
MyString& operator=(const MyString& from)
{
if (this != &from) { // <-- self assignment check
delete[] _cstr;
_cstr = new char[strlen(from._cstr) + 1];
strcpy(_cstr, from._cstr);
}
return *this;
}
private:
char* _cstr;
};
int main()
{
MyString orig("howdy");
MyString copy1 = orig;
copy1 = copy1;
return 0;
}
Example: Rule Of Five - Explicit Resource Management, And Moving?¶
Move not implemented
⟶ RValues are copied rather than re-used/moved
Not a correctness, but a usability issue
“A question of being nice to your users”
E.g.
std::string
(as all types from the standard library, where applicable) have dual constructors
#include <iostream>
#include <cstring>
class MyString
{
public:
MyString()
{
_cstr = new char[1]{'\0'};
}
MyString(const char* cstr)
{
_cstr = new char[strlen(cstr) + 1];
strcpy(_cstr, cstr);
}
~MyString()
{
delete[] _cstr;
}
MyString(const MyString& from)
: MyString(from._cstr) {}
MyString& operator=(const MyString& from)
{
if (this != &from) {
delete[] _cstr;
_cstr = new char[strlen(from._cstr) + 1];
strcpy(_cstr, from._cstr);
}
return *this;
}
private:
char* _cstr;
};
int main()
{
MyString orig("howdy");
MyString copy1 = std::move(orig); // <-- calling copy ctor
return 0;
}
Example: Rule Of Five - Explicit Resource Management, And Moving!¶
Same old story: check for self-move cornercase
#include <iostream>
#include <cstring>
#include <utility>
class MyString
{
public:
MyString()
{
_cstr = new char[1]{'\0'};
}
MyString(const char* cstr)
{
_cstr = new char[strlen(cstr) + 1];
strcpy(_cstr, cstr);
}
~MyString()
{
delete[] _cstr;
}
MyString(const MyString& from)
: MyString(from._cstr) {}
MyString& operator=(const MyString& from)
{
if (this != &from) {
delete[] _cstr;
_cstr = new char[strlen(from._cstr) + 1];
strcpy(_cstr, from._cstr);
}
return *this;
}
// move
MyString(MyString&& from)
: _cstr(std::exchange(from._cstr, new char[1]{'\0'})) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
MyString& operator=(MyString&& from)
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
if (this != &from) { // <-- self move check
delete[] _cstr;
_cstr = std::exchange(from._cstr, new char[1]{'\0'});
}
return *this;
}
private:
char* _cstr;
};
int main()
{
MyString orig("howdy");
MyString copy1 = std::move(orig); // <-- calling move ctor
copy1 = std::move(copy1);
return 0;
}
Example: Move-Only Datatypes¶
Not everything is copyable
⟶
std::unique_ptr
, prominentlyFile descriptors? Copy could be implemented as dup()
… but that is not exactly the same
And not always applicable
#include <sys/timerfd.h>
#include <unistd.h>
#include <cassert>
#include <utility>
#include <iostream>
class PeriodicTimer{
public:
PeriodicTimer(timespec interval)
: _fd(timerfd_create(CLOCK_MONOTONIC, 0)),
_interval(interval)
{
assert(_fd>=0);
}
~PeriodicTimer()
{
close(_fd);
}
PeriodicTimer(const PeriodicTimer&) = delete;
PeriodicTimer& operator=(const PeriodicTimer&) = delete;
PeriodicTimer(PeriodicTimer&& from)
: _fd(std::exchange(from._fd, -1)),
_interval(std::exchange(from._interval, {0})) {}
PeriodicTimer& operator=(PeriodicTimer& from)
{
if (this != &from) {
_fd = std::exchange(from._fd, -1);
_interval = std::exchange(from._interval, {0});
}
return *this;
}
void start()
{
itimerspec spec = {
.it_interval = _interval,
.it_value = {0,1} // <-- initial expiration: (almost) now
};
int error = timerfd_settime(_fd, 0, &spec, nullptr);
assert(!error);
}
uint64_t /*n expirations*/ wait()
{
uint64_t num_expirations;
ssize_t n_bytes_read = read(_fd, &num_expirations, sizeof(num_expirations));
assert(n_bytes_read == sizeof(num_expirations));
return num_expirations;
}
private:
int _fd;
timespec _interval;
};
void do_something_with(PeriodicTimer&& timer)
{
auto my_timer = std::move(timer);
for (int i=0; i<3; i++) {
sleep(2);
uint64_t n_expirations = my_timer.wait();
std::cout << "timer expired " << n_expirations << " times" << std::endl;
}
}
int main()
{
PeriodicTimer timer({0,1000UL*1000UL}); // <-- millisecond
timer.start();
// auto another_timer = timer; // <-- no!
do_something_with(std::move(timer));
return 0;
}