...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Most of the time, flyweight
default configuration is just good
enough and the user need not care about further tuning of her flyweight
instantiations; however, when the necessity for more control over Boost.Flyweight
behavior arises, comprehensive mechanisms are provided to select, configure and
even extend the following implementation aspects:
flyweight
objects refer to.
The flyweight
class template features a "smart" specification
interface by which the configuration aspects can be provided as optional template arguments
in whatever order the user pleases. For instance, a tagged flyweight
of std::string
s with a set-based factory and
no tracking can be specified like this:
flyweight<std::string, tag<label_t>, set_factory<>, no_tracking >
or like this:
flyweight<std::string, no_tracking, tag<label_t>, set_factory<> >
or in any other order; only std::string
is required to occupy
the first place in the specification.
The example code shown at the introductory section
uses the
"boost/flyweight.hpp"
convenience header, which simply includes the headers for the class template
flyweight
and its default configuration components:
#include <boost/flyweight/flyweight.hpp> // class template flyweight #include <boost/flyweight/hashed_factory.hpp> // hashed flyweight factory #include <boost/flyweight/static_holder.hpp> // regular factory instantiation #include <boost/flyweight/simple_locking.hpp> // simple locking policy #include <boost/flyweight/refcounted.hpp> // refcounting tracking policy
When using components other than these, their specific headers must be explicitly included.
Consider the following two types:
typedef flyweight<std::string> name_t; typedef flyweight<std::string> ip_address_t;
Although technically both types are identical, this is so by virtue of
coincidence, as there is no sensible relation between names and IP addresses.
Internally, the fact that name_t
and ip_address_t
are the same flyweight type causes values of both classes to be stored together
in the same flyweight factory, although their respective ranges
are not expected to overlap. Tagging can be used to turn these
into really different types:
struct name_tag{}; typedef flyweight<std::string,tag<name_tag> > name_t; struct ip_address_tag{}; typedef flyweight<std::string,tag<ip_address_tag> > ip_address_t;
Now, name_t
and ip_address_t
are different
flyweight classes having separate factories each. Tags are a purely syntactic
device: any type can be used for tagging inside the tag
construct, though good style recommends using tag classes with
descriptive names which are local to the context where the flyweight type
is being defined.
flyweight
uses a type of internal component called
factory whose purpose is to store and retrieve the different values
flyweight objects refer to at a given time. By default, a factory based on
a hashed container is used, so that flyweight<T>
is
actually equivalent to
flyweight<T,hashed_factory<> >
where hashed_factory
is a so-called factory specifier.
Boost.Flyweight provides several predefined factory specifiers, which not
only let the user select the specific type of factory used, but also
accept their own template arguments to customize each factory.
A given flyweight
instantiation has associated
flyweight::key_type
and flyweight::value_type
types (which are equal in the case
of regular flyweights or different if key-value
flyweights
are used). Also, there is an internal Entry
type which
corresponds to the type of the objects actually stored in the factory:
Entry
contains the shared value_type
objects
of flyweight
as well a some internal bookkeeping information;
also, Entry
is implicitly convertible to
const key_type&
, so that factories can rely on
key_type
to look up Entrie
s. Since
Entry
is internal to the implementation of flyweight
,
it cannot be directly referred to by the user in the configuration of
factories. Instead, the proxy
placeholder
type boost::mpl::_1
can be used.
hashed_factory
Header:"boost/flyweight/hashed_factory.hpp"
Syntax:hashed_factory<[Hash[,Pred[,Allocator]]]>
This specifier, which Boost.Flyweight takes by default, controls the usage of a
factory internally based in a hash container. Values are determined to be
equivalent by means of the
Binary
Predicate
Pred
, and indexed into the factory container
using Hash
, which is assumed to be a hash function,
i.e. a
Unary
Function
assigning to each value a hash identifier of
type std::size_t
. The Allocator
parameter is
used by the factory container for its memory allocation needs. The default
types for these parameters are such that the expression
flyweight<T,hashed_factory<> >
is equivalent to
flyweight< T, hashed_factory< boost::hash<key_value>, std::equal_to<key_value>, std::allocator<boost::mpl::_1> > >
where key_type
is the key type of the flyweight and
boost::mpl::_1
, as explained above, stands for the
internal Entry
type of the elements stored in the factory.
Suppose we would like to configure hashed_factory
for
a std::string
flyweight with
a special hash predicate special_hash
and a custom allocator
custom_allocator
; this would be specified as follows:
flyweight< std::string, hashed_factory< special_hash<std::string>, std::equal_to<key_value>, custom_allocator<boost::mpl::_1> > >
set_factory
Header:"boost/flyweight/set_factory.hpp"
Syntax:set_factory<[Compare[,Allocator]]>
set_factory
resorts to an std::set
-like ordered
container for the implementation of the flyweight factory.
Compare
must be a
Strict
Weak Ordering
on the value type flyweight
is
acting upon; as is customary with STL ordered containers, two values
are considered equivalent if none is less than the other according to Pred
.
Allocator
is an allocator type passed along to the factory
internal container for its memory-related tasks. When default parameters are
used, the expression
flyweight<T,set_factory<> >
is equivalent to
flyweight< T, set_factory<std::less<key_type>,std::allocator<boost::mpl::_1> > >
Usual tradeoffs arising in the comparison of ordered and hashed containers
also apply when choosing between set_factory
and
hashed_factory
:
so, set-based lookup and insertion of values are generally slower than those based on hashing,
but the latter can be affected by pathological worst-case scenarios with very
poor performance.
assoc_container_factory
Header:"boost/flyweight/assoc_container_factory.hpp"
Syntax:assoc_container_factory<ContainerSpecifier>
This specifier can be seen as a generalization of
hashed_factory
and set_factory
where the user
supplies the exact type of container on which the factory is based.
The way in which the container is specified might seem at first a little
daunting to those unfamiliar with the
Boost MPL Library:
ContainerSpecifier
must be an
MPL Lambda
Expression
such that, when invoked with the
types Entry
and key_type
explained above, it produces the type of
a container of Entry
elements satisfying the following
requirements:
Unique
Associative Container
where equivalence of Entry
s
is determined by the key_type
values the entries are convertible
to .
Let us see what a container specifier looks like with an example. Suppose we have our own ordered container like the following:
template< typename Elem, typename Compare=std::less<Elem>, typename Allocator=std::allocator<Elem> > class ultrafast_set { ... };
Then ultrafast_set
can be plugged into
assoc_container_factory
like this:
typedef flyweight< std::string, assoc_container_factory< // MPL lambda expression follows ultrafast_set<mpl::_1,std::less<std::string> > > > flyweight_string;
As has been explained, mpl::_1
is a so-called MPL
placeholder standing as a "slot" to be replaced with
Entry
by the internal machinery of Boost.Flyweight.
Note that we have not
relied on the default argument of ultrafast_set
for
Compare
and instead we have provided a fixed
instantiation for std::string
: this is so because
requirements state that the type with which ContainerSpecifier
will be filled in internally is convertible to const key_type&
(here const std::string&
), and it is based on
key_type
that lookup and equivalence of entries
should be determined. On the other hand,
the default argument for the Allocator
parameter works
just fine, as is more apparent if we write it down explicitly:
typedef flyweight< std::string, assoc_container_factory< ultrafast_set< mpl::_1, std::less<std::string>, std::allocator<mpl::_1> > > > flyweight_string;
Each flyweight type, that is, each distinct instantiation of the class
template flyweight
, is associated with exactly one factory
object. In most cases, how this factory object is created is of little
importance to the user of Boost.Flyweight, but there are special
circumstances where control of this aspect is necessary. An internal
component called holder is in charge of instantiating the
factory class and some other internal information; this component is
stipulated by means of a holder specifier, static_holder
being the default one.
static_holder
Header:"boost/flyweight/static_holder.hpp"
Syntax:static_holder
This the default holder specifier of Boost.Flyweight, and produces holders where the unique factory lives as a local static variable of the program.
intermodule_holder
Header:"boost/flyweight/intermodule_holder.hpp"
Syntax:intermodule_holder
In most C++ environments, static variables do not mix well with dynamically loaded modules in the sense that instances of the same static variable can be duplicated across different modules, even though by definition the variable should be unique. In many cases, this duplication goes unnoticed if the modules do not communicate between each other using the affected types, but consider this case where such communication does happen:
// module 1 typedef flyweight<std::string> flyweight_string; // produce_string is exported so that it can be dynamically // linked flyweight_string produce_string() { return flyweight_string("boost"); }
// main program typedef flyweight<std::string> flyweight_string; int main() { ... // import module 1 flyweight_string str1=produce_string(); flyweight_string str2("boost"); assert(str1==str2); }
In many environments, this program results in an assertion
failure because the flyweight factory object used
by flyweight_string
as seen within module 1 is
not the same factory object as seen within the main program: hence
the value representations internally pointed to by str1
and str2
will differ and will be mistakenly
considered as not equal. Many other problems might arise
due to factory duplication, including undefined behavior.
intermodule_holder
specifies a factory holder which
is capable of avoiding the duplication problem and ensuring that
all modules of a program are using the same factory instance.
To fix the example above, it suffices to redefine
flyweight_string
in both modules as:
typedef flyweight<std::string,intermodule_holder> flyweight_string;
intermodule_holder
is considerably more onerous than
static_holder
in terms of compilation times and
introduces a non-negligible overhead at program start-up, so its use
should be reserved to the situations where it is really necessary.
The internal factory associated to each flyweight
type is a shared resource and as such access to it must be properly
synchronized in multithreaded environments. A locking policy
specifies the synchronization mechanisms to be used for this purpose.
simple_locking
Header:"boost/flyweight/simple_locking.hpp"
Syntax:simple_locking
This is the default locking policy. It specifies the simplest native synchronization primitives provided by the operating system, whenever available.
no_locking
Header:"boost/flyweight/no_locking.hpp"
Syntax:no_locking
No synchronization is enforced so that irrestricted internal access
to the implementation shared resources is allowed.
Selecting no_locking
results in somewhat faster execution than
the default simple_locking
, but it renders the type
thread-unsafe, which can have catastrophic consequences.
This policy should not be used except in single-threaded environments or
when there is an absolute guarantee that the particular flyweight
type will not be used in a concurrent scenario.
A tracking policy controls the lifetimes of the flyweight
objects and can act based on this information. For instance, a suitable
tracking mechanism can determine when a given value stored in the factory
can be safely erased because it is no longer referenced by any
flyweight
; this is precisely what the default tracking policy,
refcounted
, does.
refcounted
Header:"boost/flyweight/refcounted.hpp"
Syntax:refcounted
This tracking policy determines that values stored in the factory be
equipped with reference counting mechanisms so that a factory entry is
erased when the last flyweight
object associated to it
is destroyed.
no_tracking
Header:"boost/flyweight/no_tracking.hpp"
Syntax:no_tracking
No flyweight tracking is done when this policy is selected, which implies
that the values stored in the factory remain in it until program termination.
As compared with refcounted
, no_tracking
presents
advantages and drawbacks. The benefits are:
no_tracking
include:
Revised April 24th 2019
© Copyright 2006-2019 Joaquín M López Muñoz. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)