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;
}