Rule of Three

The rule of three is a rule of thumb in C++ (prior to C++11) that claims that if a class defines any of the following then it should probably explicitly define all three:

Destructor – call the destructors of all the object’s class-type members

Copy constructor – construct all the object’s members from the corresponding members of the copy constructor’s argument, calling the copy constructors of the object’s class-type members, and doing a plain assignment of all non-class type (e.g., int or pointer) data members

Copy assignment operator – assign all the object’s members from the corresponding members of the assignment operator’s argument, calling the copy assignment operators of the object’s class-type members, and doing a plain assignment of all non-class type (e.g. int or pointer) data members.

Rule of Five

With the advent of C++11 the rule of three can be broadened to the rule of five.

C++11 implements move semantics, allowing destination objects to grab (or steal) data from temporary objects.

For the rule of five we have the following special members:

Example for Linked List

Taken from CS247.

Declaration (Node.h)

#ifndef NODE_H
#define NODE_H
 
#include <string>
 
struct Node {
  std::string data;
  Node* next;
 
  Node(const std::string& data, Node* next); // Ctor
 
  // Big 5
  Node(const Node& other); // Copy Ctor
  Node(Node&& other); // Move Ctor
  ~Node(); // Dtor
  Node& operator=(const Node& other); // Copy assignment operator
  Node& operator=(Node&& other); // Move assignment operator
};
#endif
  • Notice that const is everywhere except for the move operators, because by nature you will be moving it

Definition Node.cc

#include "Node.h"
#include <utility>
 
Node::Node(const std::string& data, Node* next): data{data}, next{next} {}
 
Node::Node(const Node& other): data{other.data}, next{other.next ? new Node{*(other.next)} : nullptr} {}
 
Node::Node(Node&& other): data{std::move(other.data)}, next{other.next} {
  other.next = nullptr;
}
 
Node::~Node() {
  delete next;
}
 
Node& Node::operator=(const Node& other) {
  Node tmp{other};
  std::swap(data, tmp.data);
  std::swap(next, tmp.next);
  return *this;
}
 
Node& Node::operator=(Node&& other) {
  std::swap(data, other.data);
  std::swap(next, other.next);
  return *this;
}

Usage main.cc

#include "Node.h"
 
Node getAlphabet() {
  return Node{"a", new Node{"b", new Node{"c", nullptr}}};
}
 
int main() {
  Node n{"1", new Node{"2", new Node{"3", nullptr}}};
  Node p{n}; // Copy ctor
  Node alphabet{getAlphabet()}; // Move ctor
  n = p; // Copy assignment operator
  n = getAlphabet(); // Move assignment operator
}
 

With Polymorphism, it gets a lot more complicated. See Polymorphic Big Five.

Rule of Zero

The basic idea here is that if all of the class’s fields are non-pointers from the C++ standard library and/or are C++ default types (int, bool, char, …), then the compiler will automatically generate The Big Three for you.

The compiler will always generate The Big Three for you if you don’t write them yourself.