Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

Unique features

Containers of Incomplete Types
SCARY iterators
C++ exception handling
Other features

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:

  • Exception handling incurs some kind of cost in all compiler implementations, including those that avoid the cost during normal execution. However, in some cases this cost may arguably offset the cost of the code that it is replacing.
  • Exception handling is often agreed to be a superior solution for handling a large range of function return values. However, avoiding the creation of functions that need large ranges of return values is superior to using exception handling to handle such values.
  • Using exception handling correctly can be difficult in the case of complex software.
  • The execution of throw and catch can be significantly expensive with some implementations.
  • Exception handling violates the don't-pay-for-what-you-don't-use design of C++, as it incurs overhead in any non-leaf function that has destructible stack objects regardless of whether they use exception handling.
  • The approach that game software usually takes is to avoid the need for exception handling where possible; avoid the possibility of circumstances that may lead to exceptions. For example, verify up front that there is enough memory for a subsystem to do its job instead of trying to deal with the problem via exception handling or any other means after it occurs.
  • However, some game libraries may nevertheless benefit from the use of exception handling. It's best, however, if such libraries keep the exception handling internal lest they force their usage of exception handling on the rest of the application.

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):

  • If 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.
  • If 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:

  • If 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.
  • Else if 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.
  • Else, an appropriate standard library exception is thrown (like std::out_of_range).
  • Default constructors don't allocate memory which improves performance and usually implies a no-throw guarantee (if predicate's or allocator's default constructor doesn't throw).
  • Small string optimization for 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.

PrevUpHomeNext