4.9 KiB
title | date | draft | description | tags | categories | series | favorite | ||||
---|---|---|---|---|---|---|---|---|---|---|---|
Generic Flyweight in C++ | 2020-07-16T14:28:52+02:00 | false | A no-boilerplate flyweight pattern |
|
|
|
false |
The flyweight is a well-known GoF design pattern.
It's intent is to minimize memory usage by reducing the number of instantiations of a given object.
I will show you how to implement a robust flyweight in C++, as well as a way to make it templatable for easy use with no boiler-plate.
Flyweight
The flyweight pattern minimizes memory usage by sharing a maximum amount of memory.
The classic example, as outlined on Wikipedia, is the representation of glyphs in a word processor. Most characters will be instantiated multiple times, it would lead to a ludicrous amount of memory usage to instantiate an object with its full metadata for each character onscreen.
What you can do instead is to instantiate the object once when you first encounter it, and then each time you need an identical object, you just refer to the first copy that you already had.
Implementation
Most tutorials that I have seen online use an std::vector
with a small amount
of objects, and each flyweight holds on to an index inside the vector
.
class GlyphMetadata { /* implementation */ };
class FlyweightImpl {
static inline std::vector<GlyphMetadata> instances_{};
public:
static size_t glyphIndex(char c) {
size_t ret = 0;
// Linear scan to find the glyph's index if we already have it
for (const auto& g : instances_)
if (g.char() == c)
return ret;
else
++ret;
// We didn't find it, add it at the end
instances_.emplace_back(c);
return ret; // Return the newly added element's index
}
// Getters etc...
};
class Glyph {
const size_t index_;
public:
Glyph(char c) : index_(FlyweightImpl::glyphIndex(c)) // Reference the index
{}
/* implementation, using the index to reference metadata */
};
// Etc...
However, this is not a robust solution, a large amount of objects will lead to
longer checks for equality as you scan the whole length of the array. You
cannot keep the vector
sorted to do binary searches and insertion, because
the flyweights rely on their index inside the vector being stable.
Instead, I'd recommend you use an std::set
for the following reasons:
- its semantic implies an ordering relationship, without duplication
- it has an (asymptotically) efficient insertion.
- it has stable iterators/pointers on insertion: a flyweight can just refer to
a pointer to the object contained inside the
std::set
.
That last bullet point is the reason why I'd recommend using a set
instead of
a sorted vector
.
Here's the same example using this technique:
// Same GlyphMetadata class
// No need for a FlyweightImpl class
class Glyph {
static inline std::set<GlyphMetadata> instances_{};
GlyphMetadata* meta_;
public:
Glyph(char c) : meta_(&(*instances_.emplace(c))) {}
};
The little &(*instances_.emplace(c))
does all the work for us:
instances_.emplace(c)
creates the corresponding metadata only if it isn't in the set already.- We get an iterator back to the inserted element from this operation
- We dereference it (
*<IT>
) to get aGlyphMetadata&
- We take the address of that reference for our flyweight (
&(<REF>)
).
Templating
This scheme with an std::set
is easily templatable: indeed we can imagine a
class Unique<T>
which enables us to flyweight any T
.
// Templated on both the type T and the comparison functor used for total order
// Most of the time, 'std::less' is good enough
template <typename T, typename Cmp = std::less<T>>
class Unique {
static inline std::set<T, Cmp> instances_{};
T* instance_;
// Construct our Unique from a given T
Unique(T value) : instances_(&(*instances_.emplace(std::move(value)))) {}
// Other methods, e.g: implicit conversion to T&, copy assignment, etc...
};
You can then create new flyweight by simply inheriting from the Unique
class:
// A flyweight string, e.g: when we have lots of duplication
class LightStrings : public Unique<std::string> {
// Flyweight pattern for free from inherited Unique
// Implementation...
};
Conclusion
This is an easy, generic flyweight implementation without any boilerplate. On the next post I'll show you how to use the same scheme with a polymorphic base class in our flyweight.
I first discovered this pattern while working on EPITA's Tiger Compiler, a full-featured compiler written in modern C++, whose goal is to help teach C++ techniques to students and illustrate the use of design patterns in a big code base.