Last active
July 5, 2023 15:56
-
-
Save kallsyms/ea9cc1fe88a26f91711d2335feafaf31 to your computer and use it in GitHub Desktop.
Simple C++ flyweight implementation
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
// Simple flyweight pattern implementation. | |
#include <memory> | |
#include <mutex> | |
#include <unordered_set> | |
template <typename T> | |
class FlyweightObj | |
{ | |
// FlyweightObj takes two forms: | |
// 1) A raw pointer to the object, which is used for cheap lookups. | |
// 2) A weak_ptr to the object, which is used to keep an actual reference to the object for later "strengthening" into a shared_ptr. | |
// Only instances of the 2nd form can be inserted into the pool, while the 1st form is used for lookups. | |
// Why? The weak_ptr isn't valid in the shared_ptr deleter, so we have to separately keep the raw pointer and use it as the comparator to find the object to be deleted. | |
public: | |
explicit FlyweightObj(T *p) : raw_(p) {} | |
explicit FlyweightObj(std::shared_ptr<T> p) : raw_(p.get()), weak_(p) {} | |
bool operator==(const FlyweightObj<T> &other) const | |
{ | |
// Raw pointers will be equal in the shared_ptr deleter, so the deref won't occur. | |
// In all other cases, the raw pointer is guaranteed to be safe either 1) by construction or 2) since we have not encountered the shared_ptr deleter yet. | |
return raw_ == other.raw_ || *raw_ == *other.raw_; | |
} | |
std::shared_ptr<T> get() const | |
{ | |
return weak_.lock(); | |
} | |
// Use the raw pointer to hash to support "type 1" FlyweightObjs. | |
// N.B. This is not guaranteed safe on a "type 2" FlyweightObj, since the raw pointer is not guaranteed to be valid (again, in the shared_ptr deleter). | |
// However we don't need to hash the item already in the set at deletion time, just the "type 1" FlyweightObj we're using to find it. | |
std::size_t hash() const | |
{ | |
return std::hash<typename std::remove_cv<T>::type>{}(*raw_); | |
} | |
private: | |
T *raw_; | |
std::weak_ptr<T> weak_; | |
}; | |
template <typename T> | |
struct std::hash<FlyweightObj<T>> | |
{ | |
std::size_t operator()(const FlyweightObj<T> &o) const | |
{ | |
return o.hash(); | |
} | |
}; | |
template <typename T> | |
class FlyweightCore | |
{ | |
public: | |
// Returned shared_ptr is const so that the shared value cannot be unintentionally modified. | |
using ConstT = typename std::add_const<T>::type; | |
static std::shared_ptr<ConstT> insert(T &&value) | |
{ | |
std::lock_guard<std::mutex> guard(mtx_); | |
auto it = pool_.find(FlyweightObj<ConstT>(&value)); | |
if (it != pool_.end()) | |
{ | |
return it->get(); | |
} | |
else | |
{ | |
std::shared_ptr<ConstT> ptr(new ConstT{std::move(value)}, [](ConstT *v) | |
{ std::lock_guard<std::mutex> guard(mtx_); pool_.erase(FlyweightObj(v)); }); | |
pool_.insert(FlyweightObj<ConstT>(ptr)); | |
return ptr; | |
} | |
} | |
// Returns the number of unique values in the pool. | |
// Only for testing purposes. | |
static size_t size() | |
{ | |
return pool_.size(); | |
} | |
private: | |
// See https://stackoverflow.com/a/76439305 | |
// This can also be done with a using/typedef in this class, later initialized as | |
// typename FlyweightCore<T>::PoolType FlyweightCore<T>::pool_ = {}; | |
static inline std::unordered_set<FlyweightObj<ConstT>> pool_ = {}; | |
static inline std::mutex mtx_ = {}; | |
}; | |
// Wrapping class which holds a shared_ptr returned from FlyweightCore. | |
template <typename T> | |
class Flyweight | |
{ | |
using ConstT = typename FlyweightCore<T>::ConstT; | |
public: | |
Flyweight(T &&value) : value_(FlyweightCore<T>::insert(std::move(value))) {} | |
ConstT &operator*() const { return *value_; } | |
private: | |
std::shared_ptr<ConstT> value_; | |
}; | |
/* | |
* ---------------------------------------- | |
* Example | |
* ---------------------------------------- | |
*/ | |
#include <iostream> | |
#include <string> | |
class Foo | |
{ | |
public: | |
Foo(std::string &&name) : name_(std::move(name)) {} | |
Flyweight<std::string> name_; | |
}; | |
struct cred | |
{ | |
int uid; | |
int gid; | |
std::string username; | |
std::string groupname; | |
bool operator==(const cred &other) const | |
{ | |
return std::tie(uid, gid, username, groupname) == std::tie(other.uid, other.gid, other.username, other.groupname); | |
} | |
}; | |
template <> | |
struct std::hash<cred> | |
{ | |
std::size_t operator()(const cred &c) const | |
{ | |
return std::hash<int>()(c.uid) ^ std::hash<int>()(c.gid); | |
} | |
}; | |
class Foo2 | |
{ | |
public: | |
Foo2(cred &&cred) : cred_(std::move(cred)) {} | |
Flyweight<cred> cred_; | |
}; | |
int main() | |
{ | |
{ | |
std::cerr << "sizeof(FlyweightObj<std::string>) = " << sizeof(FlyweightObj<std::string>) << std::endl; | |
Foo foo1("foo"); | |
Foo foo2("foo"); | |
Foo foo3("bar"); | |
std::cout << &(*foo1.name_) << " " << &(*foo2.name_) << " " << &(*foo3.name_) << std::endl; | |
std::cout << FlyweightCore<std::string>::size() << std::endl; | |
} | |
std::cout << FlyweightCore<std::string>::size() << std::endl; | |
{ | |
std::cerr << "sizeof(cred) = " << sizeof(cred) << std::endl; | |
std::cerr << "sizeof(FlyweightObj<cred>) = " << sizeof(FlyweightObj<cred>) << std::endl; | |
std::cerr << "sizeof(Foo2) = " << sizeof(Foo2) << std::endl; | |
Foo2 foo1({0, 0, "root", "root"}); | |
Foo2 foo2({0, 0, "root", "root"}); | |
Foo2 foo3({1000, 1000, "user", "wheel"}); | |
std::cout << &(*foo1.cred_) << " " << &(*foo2.cred_) << " " << &(*foo3.cred_) << std::endl; | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment