...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
forward_list<T>
vector
vs. std::vector
exception guaranteesvector<bool>
specializationstd::memset
Boost.Container aims for full C++11 conformance except reasoned deviations, backporting as much as possible for C++03. Obviously, this conformance is a work in progress so this section explains what C++11/C++14/C++17 features are implemented and which of them have been backported to earlier standard conformig compilers.
For compilers with rvalue references and for those C++03 types that use
Boost.Move rvalue reference
emulation Boost.Container supports all C++11
features related to move semantics: containers are movable, requirements
for value_type
are those
specified for C++11 containers.
For compilers with variadic templates, Boost.Container
supports placement insertion (emplace
,
...) functions from C++11. For those compilers without variadic templates
support Boost.Container uses the preprocessor
to create a set of overloads up to a finite number of parameters.
C++03 was not stateful-allocator friendly. For compactness of container objects and for simplicity, it did not require containers to support allocators with state: Allocator objects need not be stored in container objects. It was not possible to store an allocator with state, say an allocator that holds a pointer to an arena from which to allocate. C++03 allowed implementors to suppose two allocators of the same type always compare equal (that means that memory allocated by one allocator object could be deallocated by another instance of the same type) and allocators were not swapped when the container was swapped.
C++11 further improves stateful allocator support through std::allocator_traits
.
std::allocator_traits
is the protocol between
a container and an allocator, and an allocator writer can customize its behaviour
(should the container propagate it in move constructor, swap, etc.?) following
allocator_traits
requirements.
Boost.Container not only supports this model
with C++11 but also backports it to C++03
via boost::container::allocator_traits
including some C++17 changes. This class offers some workarounds for C++03
compilers to achieve the same allocator guarantees as std::allocator_traits
.
In [Boost.Container] containers, if possible, a single allocator is hold
to construct value_type
s.
If the container needs an auxiliary allocator (e.g. an array allocator used
by deque
or stable_vector
), that allocator is also
stored in the container and initialized from the user-supplied allocator
when the container is constructed (i.e. it's not constructed on the fly when
auxiliary memory is needed).
C++11 improves stateful allocators with the introduction of std::scoped_allocator_adaptor
class template. scoped_allocator_adaptor
is instantiated with one outer allocator and zero or more inner allocators.
A scoped allocator is a mechanism to automatically propagate the state of
the allocator to the subobjects of a container in a controlled way. If instantiated
with only one allocator type, the inner allocator becomes the scoped_allocator_adaptor
itself, thus using
the same allocator resource for the container and every element within the
container and, if the elements themselves are containers, each of their elements
recursively. If instantiated with more than one allocator, the first allocator
is the outer allocator for use by the container, the second allocator is
passed to the constructors of the container's elements, and, if the elements
themselves are containers, the third allocator is passed to the elements'
elements, and so on.
Boost.Container implements its own scoped_allocator_adaptor
class and backports this feature also to C++03 compilers.
Due to C++03 limitations, in those compilers the allocator propagation implemented
by scoped_allocator_adaptor::construct
functions will be based on traits (constructible_with_allocator_suffix
and constructible_with_allocator_prefix
)
proposed in N2554:
The Scoped Allocator Model (Rev 2) proposal. In conforming C++11
compilers or compilers supporting SFINAE expressions (when BOOST_NO_SFINAE_EXPR
is NOT defined), traits
are ignored and C++11 rules (is_constructible<T,
Args...,
inner_allocator_type>::value
and is_constructible<T,
allocator_arg_t,
inner_allocator_type,
Args...>::value
) will be used to detect if the allocator
must be propagated with suffix or prefix allocator arguments.
LWG Issue #233 corrected a defect in C++98 and specified how equivalent keys were to be inserted in associative containers. Boost.Container implements the C++11 changes that were specified in N1780 Comments on LWG issue 233: Insertion hints in associative containers:
a_eq.insert(t)
:
If a range containing elements equivalent to t exists in a_eq, t is inserted
at the end of that range.
a_eq.insert(p,t)
:
t is inserted as close as possible to the position just prior to p.
Boost.Container supports initialization, assignments and insertions from initializer lists in compilers that implement this feature.
Boost.Container implements C++14 Null Forward Iterators, which means that value-initialized iterators may be compared and compare equal to other value-initialized iterators of the same type. Value initialized iterators behave as if they refer past the end of the same empty sequence (example taken from N3644):
vector<int> v = { ... }; auto ni = vector<int>::iterator(); auto nd = vector<double>::iterator(); ni == ni; // True. nd != nd; // False. v.begin() == ni; // ??? (likely false in practice). v.end() == ni; // ??? (likely false in practice). ni == nd; // Won't compile.
The document C++ Extensions for Library Fundamentals (final draft) includes classes that provide allocator type erasure and runtime polymorphism. As Pablo Halpern, the author of the proposal, explains in the paper (N3916 Polymorphic Memory Resources (r2)):
“A significant impediment to effective memory management in C++ has been the inability to use allocators in non-generic contexts. In large software systems, most of the application program consists of non-generic procedural or object-oriented code that is compiled once and linked many times.”
“Allocators in C++, however, have historically relied solely on compile-time polymorphism, and therefore have not been suitable for use in vocabulary types, which are passed through interfaces between separately-compiled modules, because the allocator type necessarily affects the type of the object that uses it. This proposal builds upon the improvements made to allocators in C++11 and describes a set of facilities for runtime polymorphic memory resources that interoperate with the existing compile-time polymorphic allocators.”
Most utilities from the Fundamentals TS were merged into C++17, but Boost.Container offers them for C++03, C++11 and C++14 compilers.
Boost.Container implements nearly all classes
of the proposal under the namespace boost::container::pmr
.
There are two groups,
polymorphic_allocator
.
monotonic_buffer_resource
.
unsynchronized_pool_resource
.
synchronized_pool_resource
.
get_default_resource
/
set_default_resource
/
new_delete_resource
/
null_memory_resource
pmr::vector
,
etc.)
Boost.Container's polymorphic resource library is usable from C++03 containers, and offers some alternative utilities if the required C++11 features of the Library Fundamentals specification are not available.
Let's review the usage example given in N3916 and see how it can be implemented using Boost.Container: Suppose we are processing a series of shopping lists, where a shopping list is a container of strings, and storing them in a collection (a list) of shopping lists. Each shopping list being processed uses a bounded amount of memory that is needed for a short period of time, while the collection of shopping lists uses an unbounded amount of memory and will exist for a longer period of time. For efficiency, we can use a more time-efficient memory allocator based on a finite buffer for the temporary shopping lists.
Let's see how ShoppingList
can be defined to support an polymorphic memory resource that can allocate
memory from different underlying mechanisms. The most important details are:
allocator_type
typedef. This allocator_type
will be of type memory_resource *
,
which is a base class for polymorphic resources.
ShoppingList
has
constructors taking memory_resource*
as the last argument.
ShoppingList
has
constructors taking allocator_arg_t
as the first argument and memory_resource*
as the second argument.
Note: In C++03 compilers, it is
required that the programmer specializes as true
constructible_with_allocator_suffix
or constructible_with_allocator_prefix
as in C++03 there is no way to automatically detect the chosen option at
compile time. If no specialization is done, Boost.Container
assumes the suffix option.
//ShoppingList.hpp #include <boost/container/pmr/vector.hpp> #include <boost/container/pmr/string.hpp> class ShoppingList { // A vector of strings using polymorphic allocators. Every element // of the vector will use the same allocator as the vector itself. boost::container::pmr::vector_of <boost::container::pmr::string>::type m_strvec; //Alternatively in compilers that support template aliases: // boost::container::pmr::vector<boost::container::pmr::string> m_strvec; public: // This makes uses_allocator<ShoppingList, memory_resource*>::value true typedef boost::container::pmr::memory_resource* allocator_type; // If the allocator is not specified, "m_strvec" uses pmr::get_default_resource(). explicit ShoppingList(allocator_type alloc = 0) : m_strvec(alloc) {} // Copy constructor. As allocator is not specified, // "m_strvec" uses pmr::get_default_resource(). ShoppingList(const ShoppingList& other) : m_strvec(other.m_strvec) {} // Copy construct using the given memory_resource. ShoppingList(const ShoppingList& other, allocator_type a) : m_strvec(other.m_strvec, a) {} allocator_type get_allocator() const { return m_strvec.get_allocator().resource(); } void add_item(const char *item) { m_strvec.emplace_back(item); } //... };
However, this time-efficient allocator is not appropriate for the longer lived collection of shopping lists. This example shows how those temporary shopping lists, using a time-efficient allocator, can be used to populate the long lived collection of shopping lists, using a general purpose allocator, something that would be annoyingly difficult without the polymorphic allocators.
In Boost.Container for the time-efficient
allocation we can use monotonic_buffer_resource
,
providing an external buffer that will be used until it's exhausted. In the
default configuration, when the buffer is exhausted, the default memory resource
will be used instead.
#include "ShoppingList.hpp" #include <cassert> #include <boost/container/pmr/list.hpp> #include <boost/container/pmr/monotonic_buffer_resource.hpp> void processShoppingList(const ShoppingList&) { /**/ } int main() { using namespace boost::container; //All memory needed by folder and its contained objects will //be allocated from the default memory resource (usually new/delete) pmr::list_of<ShoppingList>::type folder; // Default allocator resource //Alternatively in compilers that support template aliases: // boost::container::pmr::list<ShoppingList> folder; { char buffer[1024]; pmr::monotonic_buffer_resource buf_rsrc(&buffer, 1024); //All memory needed by temporaryShoppingList will be allocated //from the local buffer (speeds up "processShoppingList") ShoppingList temporaryShoppingList(&buf_rsrc); assert(&buf_rsrc == temporaryShoppingList.get_allocator()); //list nodes, and strings "salt" and "pepper" will be allocated //in the stack thanks to "monotonic_buffer_resource". temporaryShoppingList.add_item("salt"); temporaryShoppingList.add_item("pepper"); //... //All modifications and additions to "temporaryShoppingList" //will use memory from "buffer" until it's exhausted. processShoppingList(temporaryShoppingList); //Processing done, now insert it in "folder", //which uses the default memory resource folder.push_back(temporaryShoppingList); assert(pmr::get_default_resource() == folder.back().get_allocator()); //temporaryShoppingList, buf_rsrc, and buffer go out of scope } return 0; }
Notice that the shopping lists within folder
use the default allocator resource whereas the shopping list temporaryShoppingList
uses the short-lived
but very fast buf_rsrc
. Despite
using different allocators, you can insert temporaryShoppingList
into folder because they have the same ShoppingList
type. Also, while ShoppingList
uses memory_resource directly, pmr::list
,
pmr::vector
and
pmr::string
all
use polymorphic_allocator
.
The resource passed to the ShoppingList
constructor is propagated to the vector and each string within that ShoppingList
. Similarly, the resource used
to construct folder
is propagated
to the constructors of the ShoppingLists that are inserted into the list
(and to the strings within those ShoppingLists
).
The polymorphic_allocator
template is designed to be almost interchangeable with a pointer to memory_resource
,
thus producing a bridge between the template-policy
style of allocator and the polymorphic-base-class style of allocator.
This example actually shows how easy is to use Boost.Container to write type-erasured allocator-capable classes even in C++03 compilers.
Boost.Container does not offer C++11 forward_list
container yet, but it will
be available in future versions.
vector
does not support
the strong exception guarantees given by std::vector
in functions like insert
,
push_back
, emplace
, emplace_back
,
resize
, reserve
or shrink_to_fit
for either
copyable or no-throw moveable classes. In C++11 move_if_noexcept
is used to maintain C++03 exception safety guarantees combined with C++11
move semantics. This strong exception guarantee degrades the insertion performance
of copyable and throwing-moveable types, degrading moves to copies when such
types are inserted in the vector using the aforementioned members.
This strong exception guarantee also precludes the possibility of using some
type of in-place reallocations that can further improve the insertion performance
of vector
See Extended
Allocators to know more about these optimizations.
vector
always uses
move constructors/assignments to rearrange elements in the vector and uses
memory expansion mechanisms if the allocator supports them, while offering
only basic safety guarantees. It trades off exception guarantees for an improved
performance.
Several container operations use a parameter taken by const reference that can be changed during execution of the function. LWG Issue 526 (Is it undefined if a function in the standard changes in parameters?) discusses them:
//Given std::vector<int> v v.insert(v.begin(), v[2]); //v[2] can be changed by moving elements of vector //Given std::list<int> l: l.remove(*l.begin()) //The operation could delete the first element, and then continue trying to access it.
The adopted resolution, NAD (Not A Defect), implies that previous operations must be well-defined. This requires code to detect a reference to an inserted element and an additional copy in that case, impacting performance even when references to already inserted objects are not used. Note that equivalent functions taking rvalue references or iterator ranges require elements not already inserted in the container.
Boost.Container prioritizes performance and has not implemented the NAD resolution: in functions that might modify the argument, the library requires references to elements not stored in the container. Using references to inserted elements yields to undefined behaviour (although in debug mode, this precondition violation could be notified via BOOST_ASSERT).
vector<bool>
specialization
has been quite problematic, and there have been several unsuccessful tries
to deprecate or remove it from the standard. Boost.Container
does not implement it as there is a superior Boost.DynamicBitset
solution. For issues with vector<bool>
see the following papers:
Quotes:
vector<bool>
is not a container and vector<bool>::iterator
is not a random-access iterator
(or even a forward or bidirectional iterator either, for that matter).
This has already broken user code in the field in mysterious ways.”
vector<bool>
forces a specific (and potentially
bad) optimization choice on all users by enshrining it in the standard.
The optimization is premature; different users have different requirements.
This too has already hurt users who have been forced to implement workarounds
to disable the 'optimization' (e.g., by using a vector<char> and
manually casting to/from bool).”
So boost::container::vector<bool>::iterator
returns real bool
references and works as a fully compliant container. If you need a memory
optimized version of boost::container::vector<bool>
,
please use Boost.DynamicBitset.
Boost.Container uses std::memset
with a zero value to initialize some types as in most platforms this initialization
yields to the desired value initialization with improved performance.
Following the C11 standard, Boost.Container
assumes that for any integer type, the object representation where
all the bits are zero shall be a representation of the value zero in that
type. Since _Bool
/wchar_t
/char16_t
/char32_t
are also integer types in C, it considers
all C++ integral types as initializable via std::memset
.
By default, Boost.Container also considers
floating point types to be initializable using std::memset
.
Most platforms are compatible with this initialization, but in case this
initialization is not desirable the user can #define
BOOST_CONTAINER_MEMZEROED_FLOATING_POINT_IS_NOT_ZERO
before including library headers.
By default, it also considers pointer types (pointer and pointer to function
types, excluding member object and member function pointers) to be initializable
using std::memset
. Most platforms are compatible with
this initialization, but in case this initialization is not desired the user
can #define BOOST_CONTAINER_MEMZEROED_POINTER_IS_NOT_ZERO
before including library headers.