Last active
March 20, 2018 20:03
-
-
Save milleniumbug/38224655327844a94a95e915023a4ebf to your computer and use it in GitHub Desktop.
Rule of Zero introduction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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