Skip to content

Instantly share code, notes, and snippets.

@lasty
Last active May 2, 2017 06:39
Show Gist options
  • Save lasty/3e818cb67c66e60ca393 to your computer and use it in GitHub Desktop.
Save lasty/3e818cb67c66e60ca393 to your computer and use it in GitHub Desktop.
Variation on Observer pattern, using C++11 move/copy
/*
Variation on the Observer design pattern.
I just want to keep a list of items, and call methods on them. (Similar to observer's notify)
But the items will always be in a list (ie, automatic registration)
Instead of the items requiring to be inside pointers or pointer wrappers, this handles stack allocated
items too, and items inside std containers, using copy and move semantics.
Uses C++11 features, compile with gcc -std=c++11 or equivalent
*/
#include <algorithm>
#include <vector>
#include <utility>
#include <memory>
#include <stdexcept>
#include <iostream>
using std::cout;
using std::endl;
////////////////////////////////////////////////////////////////////////////////
//helper function for checking if an item is inside a std container
template <typename CONTAINER, typename ITEM>
bool in_list(const CONTAINER& container, const ITEM& item)
{
auto it = std::find(begin(container), end(container), item);
return (it != end(container));
}
//Asserts with file/line logging
bool Assert_Real(const char* what_txt, bool what, const char* file, int line)
{
if (not what)
{
cout << "ASSERT(" << what_txt <<") failed. ["<<file<<":"<<line<<"]" << endl;
//throw std::runtime_error("ASSERT failed");
}
return what;
}
#define ASSERT(what) Assert_Real(#what, (what), __FILE__, __LINE__)
//to trace constructors and destructors and other various stats
constexpr bool logging = true;
////////////////////////////////////////////////////////////////////////////////
//Forward declaration for item type
template <typename ITEM> class Item;
// List holds a vector of pointers to ITEM
// Items should derived from class Item below
template <typename ITEM>
class List
{
private:
std::vector<ITEM*> thelist {};
public:
List() = default;
~List()
{
ASSERT(thelist.size() == 0);
if (logging) cout << "List contains "<< thelist.size() << " items at end" << endl;
for (auto &i : thelist)
{
i->ListGoneAway();
}
thelist.clear();
}
private:
friend class Item<ITEM>; //only Items should be able to use these functions
void Add(ITEM* item)
{
ASSERT(not in_list(thelist, item)); //don't allow to add the same item twice
thelist.push_back(item);
}
void Delete(ITEM* item)
{
ASSERT(in_list(thelist, item)); //make sure it actually in the list
thelist.erase( std::find(thelist.begin(), thelist.end(), item) );
}
// replaces one item with another (might be slightly faster than deleting and adding)
void Replace(ITEM* first, ITEM* second)
{
ASSERT(in_list(thelist, first));
ASSERT(not in_list(thelist, second));
auto it = std::find(thelist.begin(), thelist.end(), first);
*it = second;
}
public:
//allow access to the vector (for use with range-for loops)
auto begin() const -> decltype(thelist.begin()) { return thelist.begin(); }
auto end() const -> decltype(thelist.end()) { return thelist.end(); }
auto size() const -> decltype(thelist.size()) { return thelist.size(); }
};
// Item will manage itself being inside the list it is given
// Handles copying and moving to keep the list updated
// Also handles when the list gets destroyed before the items
// Template parameter is for the derived class, which removes need for virtual calls
template <typename ITEM>
class Item
{
private:
//pointer to the list we are currently in, or nullptr if we got moved or list went away
List<ITEM> * list_im_in = nullptr;
public:
Item() = delete; //don't allow items without lists
//TODO maybe make public List::Add/Delete and allow user to add to lists manually?
//Item() = default;
explicit Item(List<ITEM> &mylist)
: list_im_in(&mylist)
{
if (logging) cout << "[Item regular constructor]" << endl;
list_im_in->Add(static_cast<ITEM*>(this));
}
~Item()
{
if (logging) cout << "[Item destructor]" << endl;
if (list_im_in)
{
list_im_in->Delete(static_cast<ITEM*>(this));
}
}
Item(const Item& copy)
: list_im_in(copy.list_im_in)
{
if (logging) cout << "[Item copy constructor]" << endl;
list_im_in->Add(static_cast<ITEM*>(this));
}
Item(Item&& move)
: list_im_in(move.list_im_in)
{
if (logging) cout << "[Item move constructor]" << endl;
list_im_in->Replace(static_cast<ITEM*>(&move), static_cast<ITEM*>(this));
move.ListGoneAway();
}
Item& operator=(const Item& copyassign)
{
ASSERT(list_im_in == copyassign.list_im_in); //Don't allow items to move lists
if (logging) cout << "[Item operator= copy]" << endl;
//don't do anything, both objects are in the list already
return *this;
}
Item& operator=(Item && moveassign)
{
ASSERT(list_im_in == moveassign.list_im_in); //Don't allow items to move lists
if (logging) cout << "[Item operator= move]" << endl;
//don't do anything, both objects are in the list already
return *this;
}
//The list is signaling its impending destruciton, and dangling pointers are bad...
void ListGoneAway()
{
if (logging) cout << "[Item ListGoneAway]" << endl;
list_im_in = nullptr;
}
};
////////////////////////////////////////////////////////////////////////////////
//Classes for examples/tests below
// Derived class from item. All that is needed is to specify this new class in the template
// and pass it a list during construction
class Item2 : public Item<Item2>
{
private:
std::string name = "NOT_SET";
public:
Item2(List<Item2> &mylist, const std::string &name)
: Item<Item2>(mylist)
, name(name)
{
}
void Print() const
{
cout << "[" << this << "] Item2::Print() - '" << name << "'" << endl;
}
void DoStuff()
{
name = name + " do stuff";
}
};
// List containing Item2
class List2 : public List<Item2>
{
public:
void PrintAll() const
{
cout <<"List contains " << size() << " items:" << endl;
for (const auto i : *this)
{
i->DoStuff();
i->Print();
}
cout << endl;
}
void DoStuffAll()
{
for (auto &i : *this)
{
i->DoStuff();
}
}
};
// Composition instead of inheritance
class ContainsList3
{
private:
List<Item2> insidelist;
public:
operator List<Item2> & () { return insidelist; } //Or use a GetList() function
void Print() const
{
for (const auto i : insidelist)
{
i->Print();
}
}
void DoStuff() const
{
for (const auto i : insidelist)
{
i->DoStuff();
}
}
};
//Testing that Item4's can exist in Item2 lists
class Item4 : Item2
{
public:
using Item2::Item2;
};
//Factory pattern, works quite nicely
class FactoryContainerList
{
private:
List<Item2> factorylist;
public:
//Compiler should elide move constructor, and create inline from caller
Item4 MakeNewItem4(std::string name)
{
return Item4{factorylist, name};
}
void Print() const
{
for (const auto i : factorylist)
{
i->Print();
}
}
void DoStuff() const
{
cout << "\nTransforming " << factorylist.size() << " objects" << endl;
for (const auto i : factorylist)
{
i->DoStuff();
}
}
};
////////////////////////////////////////////////////////////////////////////////
//Unit tests
void TestObserver1()
{
cout << "\n\nTestObserver1() -- simple stack construction"<<endl;
List2 l;
Item2 i1{l, "i1"};
Item2 i2{l, "i2"};
l.PrintAll();
}
void TestObserver2()
{
cout << "\n\nTestObserver2() - items in vector, copy and move assign"<<endl;
List2 list;
std::vector<Item2> vec_items;
{
Item2 i1{list, "i1 pushed"};
vec_items.push_back(i1);
Item2 i2{list, "i2 insertet at front"};
vec_items.insert(vec_items.begin(), i2);
vec_items.emplace_back(list, "i3 emplaced");
ASSERT(vec_items.size() == 3);
ASSERT(list.size() == 5); //emplace should not create twice, 3 + 2 = 5
}
ASSERT(vec_items.size() == 3);
ASSERT(list.size() == 3);
list.PrintAll();
{
Item2 i1{list, "i1 copying"};
Item2 i2{list, "i2 moving"};
Item2 i3 = i1;
Item2 i4 = std::move(i2);
}
}
void TestObserver3()
{
cout << "\n\nTestObserver3() - items outlive the list"<<endl;
std::unique_ptr<List2> l { new List2() };
Item2 i1{*l, "i1"};
Item2 i2{*l, "i2"};
l->PrintAll();
l.reset();
}
void TestObserver4()
{
cout << "\n\nTestObserver4() - using list inside another class" << endl;
ContainsList3 l;
Item2 i1{l, "i1"};
Item2 i2{l, "i2"};
l.DoStuff();
l.Print();
}
void TestObserver5()
{
cout << "\n\nTestObserver5() - using factory pattern" << endl;
FactoryContainerList factory;
//these 2 lines should make no temporary items, and not use copy/move constructors
Item4 i4 = factory.MakeNewItem4("factory item 1");
Item4 i42 = factory.MakeNewItem4("factory item 2");
factory.Print();
factory.DoStuff();
factory.Print();
i4 = i42;
//items should have identical name strings now
factory.Print();
}
int main()
{
TestObserver1();
TestObserver2();
TestObserver3();
TestObserver4();
TestObserver5();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment