Skip to content

Instantly share code, notes, and snippets.

@milleniumbug
Last active March 20, 2018 20:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milleniumbug/38224655327844a94a95e915023a4ebf to your computer and use it in GitHub Desktop.
Save milleniumbug/38224655327844a94a95e915023a4ebf to your computer and use it in GitHub Desktop.
Rule of Zero introduction
#include <vector>
#include <iostream>
// nice little class A
class A
{
B b;
int a;
std::vector<int> allTheInts;
int doThisThing() { return b.stuff() + a; }
};
// OMG, it turns out B is actually a polymorphic base class and we need to store instances of class D which is derived from B too
// Our first attempt:
class A
{
B* b;
int a;
std::vector<int> allTheInts;
public:
A() { b = new D(); }
int doThisThing() { return b->stuff() + a; }
};
// nice, we can store derived instances.
// oh wait, the memory is leaking, let's fix this
class A
{
B* b;
int a;
std::vector<int> allTheInts;
public:
A() { b = new D(); }
~A() { delete b; }
int doThisThing() { return b->stuff() + a; }
};
// It's all ok, except when people do this
int main()
{
A a;
A b;
a = b;
}
// b is now a member which needs special treatment from the copy constructors. I'll call it a "resource" for the rest of this code
// X - Rule of Three violated
// X - Rule of Five violated
// X - Rule of Zero violated
// In the case we never want copy the object, it's simple to just disable these operations:
class A
{
B* b;
int a;
std::vector<int> allTheInts;
public:
A() { b = new D(); }
~A() { delete b; }
int doThisThing() { return b->stuff() + a; }
// = delete; is a C++11 feature
// C++03 equivalent would be declaring these as private
A& operator=(const A& other) = delete;
A(const A& other) = delete;
// it's not necessary to disable move operations if we disable copy operations
// but I prefer to do this for clarity's sake:
A& operator=(A&& other) = delete;
A(A&& other) = delete;
};
// We've got rid of the bug potential
// V - Rule of Three satisfied
// V - Rule of Five satisfied
// X - Rule of Zero violated
// Unfortunately, "brick types" are problematic to manage
// - no returning from functions (until C++17)
// - no passing by value
// - can't store in a std::vector
// In the case, however, that we want to provide these operations, they must be implemented
class A
{
B* b;
int a;
std::vector<int> allTheInts;
public:
A() { b = new D(); }
~A() { delete b; }
A(const A& other) :
b(other.b->clone()),
a(other.a),
allTheInts(other.allTheInts)
{
}
A& operator=(const A& other)
{
this->allTheInts = other.allTheInts;
// NOTE THE ORDER OF THESE TWO STATEMENTS!
auto tmp = other.b->clone(); // (1)
delete this->b; // (2)
// clone must be done before delete'ing in case it throws
// otherwise we risk double deletion
this->b = tmp;
this->a = other.a;
return *this;
}
int doThisThing() { return b->stuff() + a; }
};
// We added a clone() function to the B class hierarchy which creates independent instances.
// V - Rule of Three satisfied
// X - Rule of Five not satisfied
// X - Rule of Zero violated
// This implementation is suboptimal since it doesn't use move semantics
// let's fix this
class A
{
B* b;
int a;
std::vector<int> allTheInts;
public:
A() { b = new D(); }
~A() { delete b; }
A(const A& other) :
b(other.b->clone()),
a(other.a),
allTheInts(other.allTheInts)
{
}
// If we don't declare this `noexcept`
// std::vector won't use move semantics, it will copy instead
A(A&& other) noexcept :
b(other.b),
a(other.a),
allTheInts(std::move(other.allTheInts))
{
this->b = other.b;
other.b = nullptr;
}
A& operator=(A&& other) noexcept
{
this->b = other.b;
other.b = nullptr;
this->a = other.a;
this->allTheInts = std::move(other.allTheInts);
return *this;
}
A& operator=(const A& other)
{
this->allTheInts = other.allTheInts;
auto tmp = other.b->clone();
delete this->b;
this->b = tmp;
this->a = other.a;
return *this;
}
int doThisThing() { return b->stuff() + a; }
};
// Current state:
// V - Rule of Three satisfied
// V - Rule of Five satisfied
// X - Rule of Zero violated
// Hey, we need to add a member
class A
{
B* b;
int a;
std::vector<int> allTheInts;
long c;
public:
A() { b = new D(); }
~A() { delete b; }
A(const A& other) :
b(other.b->clone()),
a(other.a),
allTheInts(other.allTheInts),
c(other.c)
{
}
A(A&& other) noexcept :
b(other.b),
a(other.a),
allTheInts(std::move(other.allTheInts)),
c(other.c)
{
this->b = other.b;
other.b = nullptr;
}
A& operator=(const A& other)
{
this->allTheInts = other.allTheInts;
auto tmp = other.b->clone();
delete this->b;
this->b = tmp;
this->a = other.a;
return *this;
}
A& operator=(A&& other) noexcept
{
this->b = other.b;
other.b = nullptr;
this->a = other.a;
this->allTheInts = std::move(other.allTheInts);
this->c = other.c;
return *this;
}
int doThisThing() { return b->stuff() + a; }
};
// Thanks to DRY violation, we forgot to copy in the copy assignment operator
// It's because we're repeating ourselves like some kind of caveman
// Let's refactor responsibility for managing a resource outside of our class:
// see: Single Responsiblity Principle, DRY, Rule of Zero
class ResourceB
{
B* b;
ResourceB(B* b) : b(b) {}
public:
B& get() { return *b; }
ResourceB(const ResourceB& other) :
b(other.b->clone())
{
}
ResourceB& operator=(const ResourceB& other)
{
auto tmp = other.b->clone();
delete this->b;
this->b = tmp;
}
ResourceB(ResourceB&& other) noexcept
{
b = other.b;
other.b = nullptr;
}
ResourceB& operator=(const ResourceB& other)
{
this->b = other.b->clone();
}
ResourceB& operator=(ResourceB&& other) noexcept
{
// instead of writing
// this->b = std::move(other.b);
// other.b = nullptr;
this->b = std::exchange(other.b, nullptr);
}
template<typename T>
static ResourceB make()
{
return ResourceB(new T());
}
};
class A
{
ResourceB b;
int a;
std::vector<int> allTheInts;
long c;
A() : b(ResourceB::make<D>()) {}
int doThisThing() { return b.get().stuff() + a; }
};
// We removed duplication, and made the code of our class work by making sure there is no member that needs special treatment
// There is zero declared members of the Big Five, and yet our class works properly, hence Rule of Zero
// V - Rule of Three satisfied
// V - Rule of Five satisfied
// V - Rule of Zero satisfied
// Hey, it turns out standard library already has these "ownership policy" types!
// With std::unique_ptr, we can get 60% of our class done and we didn't need to write any extra line of code:
#include <memory>
class A
{
std::unique_ptr<B> b;
int a;
std::vector<int> allTheInts;
long c;
A() : b(std::make_unique<D>()) {}
int doThisThing() { return b->stuff() + a; }
};
// it's 60% because copy assignment operator and copy constructor are disabled because the resource handling policy of `std::unique_ptr` is: only one instance manages a resource
// trying to copy instances of A will issue a compiler error, but destruction and moving are safe
// There's a proposed `clone_ptr` class which will work like this:
struct BCloner
{
B* operator()(const B* other)
{
return other->clone();
}
};
class A
{
clone_ptr<B, BCloner> b;
int a;
std::vector<int> allTheInts;
long c;
A() : b(make_cloneable<D>()) {}
int doThisThing() { return b->stuff() + a; }
};
// This would also allow for copy operations.
// Generally:
// if you to release something manually, you're doing it wrong:
//
// std::vector<T> replaces usages of new T[]
// std::string replaces usages of null-terminated strings
// std::unique_ptr<T> replaces usage of new/delete with single owner
// std::shared_ptr<T> replaces usage of new/delete with multiple owners
// std::unique_ptr<T, CustomDeleterFunctionObjectType> replaces usage of C's "typedef to a pointer to incomplete type" idiom
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment