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 management

    • Implements 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;
}
  • Pointer to string is simply copied

  • Both point to same string

../../../../../_images/pointer-member-compilerview.svg
  • First destructor frees memory

  • Second ⟶ double-free

../../../../../_images/pointer-member-doublefree.svg

Example: Rule Of Three - Explicit Resource Management, Done Almost Right

#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;
}
../../../../../_images/pointer-member-manual-copy.svg

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, prominently

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