![]() |
Home | Libraries | People | FAQ | More |
Incomplete types allow type erasure and recursive data types, and C and C++ programmers have been using it for years to build complex data structures, like tree structures where a node may have an arbitrary number of children.
What about standard containers? Containers of incomplete types have been under discussion for a long time, as explained in Matt Austern's great article (The Standard Librarian: Containers of Incomplete Types):
“Unlike most of my columns, this one is about something you can't do with the C++ Standard library: put incomplete types in one of the standard containers. This column explains why you might want to do this, why the standardization committee banned it even though they knew it was useful, and what you might be able to do to get around the restriction.”
“In 1997, shortly before the C++ Standard was completed, the standardization committee received a query: Is it possible to create standard containers with incomplete types? It took a while for the committee to understand the question. What would such a thing even mean, and why on earth would you ever want to do it? The committee eventually worked it out and came up with an answer to the question. (Just so you don't have to skip ahead to the end, the answer is "no.") But the question is much more interesting than the answer: it points to a useful, and insufficiently discussed, programming technique. The standard library doesn't directly support that technique, but the two can be made to coexist.”
“In a future revision of C++, it might make sense to relax
the restriction on instantiating standard library templates with incomplete
types. Clearly, the general prohibition should stay in place - instantiating
templates with incomplete types is a delicate business, and there are too
many classes in the standard library where it would make no sense. But perhaps
it should be relaxed on a case-by-case basis, and vector
looks like a good candidate for such special-case treatment: it's the one
standard container class where there are good reasons to instantiate it with
an incomplete type and where Standard Library implementors want to make it
work. As of today, in fact, implementors would have to go out of their way
to prohibit it!”
C++11 standard is also cautious about incomplete types and STL: “17.6.4.8 Other functions (...) 2. the effects are undefined in the following cases: (...) In particular - if an incomplete type (3.9) is used as a template argument when instantiating a template component, unless specifically allowed for that component”.
Finally C++17 added support for incomplete types in std::vector,
std::list and std::forward_list
(see N4510: Minimal incomplete
type support for standard containers, revision 4 for details),
but no other containers like std::set/map/unordered_set/unordered_map,
Fortunately all Boost.Container containers
except static_vector,
small_vector
and basic_string
are designed to support incomplete types. static_vector
and small_vector
are special because they statically allocates memory for value_type
and this requires complete types. basic_string
implements Small String Optimization which also requires complete types.
Boost.Container containers supporting incomplete types also support instantiating iterators to those incomplete elements.
Most Boost.Container containers can be used to define recursive containers:
#include <boost/container/vector.hpp> #include <boost/container/stable_vector.hpp> #include <boost/container/deque.hpp> #include <boost/container/list.hpp> #include <boost/container/map.hpp> #include <boost/container/string.hpp> using namespace boost::container; struct data { int i_; //A vector holding still undefined class 'data' vector<data> v_; vector<data>::iterator vi_; //A stable_vector holding still undefined class 'data' stable_vector<data> sv_; stable_vector<data>::iterator svi_; //A stable_vector holding still undefined class 'data' deque<data> d_; deque<data>::iterator di_; //A list holding still undefined 'data' list<data> l_; list<data>::iterator li_; //A map holding still undefined 'data' map<data, data> m_; map<data, data>::iterator mi_; friend bool operator <(const data &l, const data &r) { return l.i_ < r.i_; } }; struct tree_node { string name; string value; //children nodes of this node list<tree_node> children_; list<tree_node>::iterator selected_child_; }; int main() { //a container holding a recursive data type stable_vector<data> sv; sv.resize(100); //Let's build a tree based in //a recursive data type tree_node root; root.name = "root"; root.value = "root_value"; root.children_.resize(7); root.selected_child_ = root.children_.begin(); return 0; }
Containers of incomplete types are useful to break header file dependencies
and improve compilation types. With Boost.Container, you can write a header
file defining a class with containers of incomplete types as data members,
if you carefully put all the implementation details that require knowing
the size of the value_type
in your implementation file:
In this header file we define a class (MyClassHolder) that holds a vector
of an incomplete type (MyClass)
that it's only forward declared.
#include <boost/container/vector.hpp> //MyClassHolder.h //We don't need to include "MyClass.h" //to store vector<MyClass> class MyClass; class MyClassHolder { public: void AddNewObject(const MyClass &o); const MyClass & GetLastObject() const; private: ::boost::container::vector<MyClass> vector_; };
Then we can define MyClass
in its own header file.
//MyClass.h class MyClass { private: int value_; public: MyClass(int val = 0) : value_(val){} friend bool operator==(const MyClass &l, const MyClass &r) { return l.value_ == r.value_; } //... };
And include it only in the implementation file of MyClassHolder
//MyClassHolder.cpp #include "MyClassHolder.h" //In the implementation MyClass must be a complete //type so we include the appropriate header #include "MyClass.h" void MyClassHolder::AddNewObject(const MyClass &o) { vector_.push_back(o); } const MyClass & MyClassHolder::GetLastObject() const { return vector_.back(); }
Finally, we can just compile, link, and run!
//Main.cpp #include "MyClassHolder.h" #include "MyClass.h" #include <cassert> int main() { MyClass mc(7); MyClassHolder myclassholder; myclassholder.AddNewObject(mc); return myclassholder.GetLastObject() == mc ? 0 : 1; }
The paper N2913, titled SCARY
Iterator Assignment and Initialization, proposed a requirement that
a standard container's iterator types have no dependency on any type argument
apart from the container's value_type,
difference_type, pointer type,
and const_pointer type. In
particular, according to the proposal, the types of a standard container's
iterators should not depend on the container's key_compare,
hasher, key_equal,
or allocator types.
That paper demonstrated that SCARY operations were crucial to the performant implementation of common design patterns using STL components. It showed that implementations that support SCARY operations reduce object code bloat by eliminating redundant specializations of iterator and algorithm templates.
Boost.Container containers implement SCARY
iterators so the iterator type of a container is only dependent on the allocator_traits<allocator_type>::pointer type (the pointer type of the
value_type to be inserted
in the container). Reference types and all other typedefs are deduced from
the pointer type using the C++11 pointer_traits
utility. This leads to lower code bloat in algorithms and classes templated
on iterators.
In some environments, such as game development or embedded systems, C++ exceptions are disabled or a customized error handling is needed. According to document N2271 EASTL -- Electronic Arts Standard Template Library exceptions can be disabled for several reasons:
In order to support environments without C++ exception support or environments
with special error handling needs, Boost.Container
changes error signalling behaviour when BOOST_CONTAINER_USER_DEFINED_THROW_CALLBACKS
or BOOST_NO_EXCEPTIONS is
defined. The former shall be defined by the user and the latter can be either
defined by the user or implicitly defined by Boost.Confg
when the compiler has been invoked with the appropriate flag (like -fno-exceptions in GCC).
When dealing with user-defined classes, (e.g. when constructing user-defined classes):
BOOST_NO_EXCEPTIONS
is defined, the library avoids using try/catch/throw
statements. The class writer must handle and propagate error situations
internally as no error will be propagated through Boost.Container.
BOOST_NO_EXCEPTIONS
is not defined, the library propagates
exceptions offering the exception guarantees detailed in the documentation.
When the library needs to throw an exception (such as out_of_range
when an incorrect index is used in vector::at),
the library calls a throw-callback declared in boost/container/throw_exception.hpp:
BOOST_CONTAINER_USER_DEFINED_THROW_CALLBACKS
is defined, then the programmer must provide its own definition for all
throw_xxx functions.
Those functions can't return, they must throw an exception or call std::exit or std::abort.
BOOST_NO_EXCEPTIONS
is defined, a BOOST_ASSERT_MSG
assertion is triggered (see Boost.Assert
for more information). If this assertion returns, then std::abort is called.
std::out_of_range).
basic_string,
with an internal buffer of 11/23 bytes (32/64 bit systems) without increasing the usual sizeof
of the string (3 words).
[multi]set/map
containers are size optimized embedding the color bit of the red-black
tree nodes in the parent pointer.
[multi]set/map
containers use no recursive functions so stack problems are avoided.