Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

Managed Memory Segments: Object construction

Object construction API for managed memory segments
Named Object construction function family
Anonymous instance construction
Unique instance construction
Synchronization guarantees
Obtaining information about a constructed object
Uses-allocator protocol for object construction

Note: The following features are common to all managed memory segment classes, but we will use managed shared memory in our examples. We can do the same with memory mapped files or other managed memory segment classes.

Boost.Interprocess' managed memory segments allows a varied object construction styles:

  • Associated with a name (named construction). Objects can be later found using the name as a key.
  • Not associated with any name (anonymous construction).
  • Singleton-like allocation (unique construction). A single object of this type can live in the managed segment. Objects can be found using their type as a key.

The object construction API allows constructing a type T in a managed memory segment instance ms as:

  • An individual object:
    • Invoked as ms.construct<T>("Name"/unique_instance/anonymous_instance)(args...))
    • T is constructed as T(args...)
  • An arrays of N objects. Elements of an array can be constructed:
    • with the same parameters for all elements:
      • Invoked as ms.construct<T>("Name"/unique_instance/anonymous_instance)[N])(args...)
      • All array elements are constructed as T(args...)
    • with a different parameter for each element coming from a derefenced iterator it
      • Invoked as ms.construct_it<T>("Name"/unique_instance/anonymous_instance)[N])(it)
      • Elements are constructed as T(*(it++))

When constructing objects in a managed memory segment (managed shared memory, managed mapped files...) associated with a name, the user has a varied object construction family to "construct" or to "construct if not found". Boost.Interprocess can construct a single object or an array of objects.

//!Allocates and uses-allocator constructs an object of type MyType (throwing version).
MyType *ptr = managed_memory_segment.construct<MyType>("Name") (par1, par2...);

//!Allocates and uses-allocator constructs an array of objects of type MyType (throwing version).
//!Each object receives the same parameters.
MyType *ptr = managed_memory_segment.construct<MyType>("Name")[count](par1, par2...);

//!Tries to find a previously created object. If not present, allocates
//!and uses-allocator constructs an object of type MyType (throwing version)
MyType *ptr = managed_memory_segment.find_or_construct<MyType>("Name") (par1, par2...);

//!Tries to find a previously created object. If not present, allocates and
//!uses-allocator constructs an array of objects of type MyType (throwing version). Each object
//!receives the same parameters (par1, par2, ...)
MyType *ptr = managed_memory_segment.find_or_construct<MyType>("Name")[count](par1, par2...);

//!Allocates and uses-allocator constructs an array of objects of type MyType (throwing version)
//!Each object receives parameters returned with the expression (*it1++, *it2++,... )
MyType *ptr = managed_memory_segment.construct_it<MyType>("Name")[count](it1, it2...);

//!Tries to find a previously created object. If not present, allocates and uses-allocator constructs
//!an array of objects of type MyType (throwing version).  Each object receives
//!parameters returned with the expression (*it1++, *it2++,... )
MyType *ptr = managed_memory_segment.find_or_construct_it<MyType>("Name")[count](it1, it2...);

//!Tries to find a previously created object. Returns a pointer to the object and the
//!count (if it is not an array, returns 1). If not present, the returned pointer is 0
std::pair<MyType *,std::size_t> ret = managed_memory_segment.find<MyType>("Name");

//!Destroys the created object, returns false if not present
bool destroyed = managed_memory_segment.destroy<MyType>("Name");

//!Destroys the created object via pointer
managed_memory_segment.destroy_ptr(ptr);

All these functions have a non-throwing version, that is invoked with an additional parameter std::nothrow. For example, for simple object construction:

//!Allocates and uses-allocator constructs an object of type MyType (no throwing version)
MyType *ptr = managed_memory_segment.construct<MyType>("Name", std::nothrow) (par1, par2...);

Sometimes, the user doesn't want to create class objects associated with a name. For this purpose, Boost.Interprocess can create anonymous objects in a managed memory segment. All named object construction functions are available to uses-allocator construct anonymous objects. To allocate an anonymous objects, the user must use "boost::interprocess::anonymous_instance" name instead of a normal name:

MyType *ptr = managed_memory_segment.construct<MyType>(anonymous_instance) (par1, par2...);

//Other construct variants can also be used (including non-throwing ones)
...

//We can only destroy the anonymous object via pointer
managed_memory_segment.destroy_ptr(ptr);

Find functions have no sense here, since anonymous objects have no name. We can only destroy the anonymous object via pointer.

Sometimes, the user wants to emulate a singleton in a managed memory segment. Obviously, as the managed memory segment is constructed at run-time, a user must construct and destroy this object explicitly. But how can a user be sure that the object is the only object of its type in the managed memory segment? This can be emulated using a named object and checking if it is present before trying to create one, but all processes must agree in the object's name, that can also conflict with other existing names.

To solve this, Boost.Interprocess offers a "unique object" creation in a managed memory segment. Only one instance of a class can be created in a managed memory segment using this "unique object" service (you can create more named objects of this class, though) so it makes easier the emulation of singleton-like objects across processes, for example, to design pooled, shared memory allocators. The object can be searched using the type of the class as a key.

// Uses-allocator constructs a unique (segment-wide) instance of MyType
MyType *ptr = managed_memory_segment.construct<MyType>(unique_instance) (par1, par2...);

// Find it
std::pair<MyType *,std::size_t> ret = managed_memory_segment.find<MyType>(unique_instance);

// Destroy it
managed_memory_segment.destroy<MyType>(unique_instance);

// Other construct and find variants can also be used (including non-throwing ones)
//...
// We can also destroy the unique object via pointer
MyType *ptr = managed_memory_segment.construct<MyType>(unique_instance) (par1, par2...);
managed_shared_memory.destroy_ptr(ptr);

The find function obtains a pointer to the only object of type T that can be created using this "unique instance" mechanism.

One of the features of named/unique allocations/searches/destructions is that they are atomic. Named allocations use the recursive synchronization scheme defined by the internal mutex_family typedef defined of the memory allocation algorithm template parameter (MemoryAlgorithm). That is, the mutex type used to synchronize named/unique allocations is defined by the MemoryAlgorithm::mutex_family::recursive_mutex_type type. For shared memory, and memory mapped file based managed segments this recursive mutex is defined as interprocess_recursive_mutex.

If two processes can call:

MyType *ptr = managed_shared_memory.find_or_construct<MyType>("Name")[count](par1, par2...);

at the same time, but only one process will create the object and the other will obtain a pointer to the created object.

Raw allocation using allocate() can be called also safely while executing named/anonymous/unique allocations, just like when programming a multithreaded application inserting an object in a mutex-protected map does not block other threads from calling new[] while the map thread is searching the place where it has to insert the new object. The synchronization does happen once the map finds the correct place and it has to allocate raw memory to construct the new value.

This means that if we are creating or searching for a lot of named objects, we only block creation/searches from other processes but we don't block another process if that process is inserting elements in a shared memory vector.

Once an object is constructed using construct<> function family, the programmer can obtain information about the object using a pointer to the object. The programmer can obtain the following information:

  • Name of the object: If it's a named instance, the name used in the construction function is returned, otherwise 0 is returned.
  • Length of the object: Returns the number of elements of the object (1 if it's a single value, >=1 if it's an array).
  • The type of construction: Whether the object was constructed using a named, unique or anonymous construction.

Here is an example showing this functionality:

#include <boost/interprocess/managed_shared_memory.hpp>
#include <cassert>
#include <cstring>

class my_class
{
   //...
};

int main()
{
   using namespace boost::interprocess;

   //Remove shared memory on construction and destruction
   struct shm_remove
   {
      shm_remove() { shared_memory_object::remove("MyName"); }
      ~shm_remove(){ shared_memory_object::remove("MyName"); }
   } remover;

   managed_shared_memory managed_shm(create_only, "MyName", 10000*sizeof(std::size_t));

   //Construct objects
   my_class *named_object  = managed_shm.construct<my_class>("Object name")[1]();
   my_class *unique_object = managed_shm.construct<my_class>(unique_instance)[2]();
   my_class *anon_object   = managed_shm.construct<my_class>(anonymous_instance)[3]();

   //Now test "get_instance_name" function.
   assert(0 == std::strcmp(managed_shared_memory::get_instance_name(named_object), "Object name"));
   assert(0 == std::strcmp(managed_shared_memory::get_instance_name(unique_object), typeid(my_class).name()));
   assert(0 == managed_shared_memory::get_instance_name(anon_object));

   //Now test "get_instance_type" function.
   assert(named_type     == managed_shared_memory::get_instance_type(named_object));
   assert(unique_type    == managed_shared_memory::get_instance_type(unique_object));
   assert(anonymous_type == managed_shared_memory::get_instance_type(anon_object));

   //Now test "get_instance_length" function.
   assert(1 == managed_shared_memory::get_instance_length(named_object));
   assert(2 == managed_shared_memory::get_instance_length(unique_object));
   assert(3 == managed_shared_memory::get_instance_length(anon_object));

   managed_shm.destroy_ptr(named_object);
   managed_shm.destroy_ptr(unique_object);
   managed_shm.destroy_ptr(anon_object);
   return 0;
}

Since Boost 1.91, Boost.Interprocess uses Boost.Container's extended uses-allocator construction utilities (see Boost.Container) so that constructing objects that use Boost.Interprocess allocators is simplified. As a result a user:

  • Does not longer need to explicitly pass allocator arguments (or segment_manager* arguments that are convertible to allocators) when constructing an object and its subobjects.
  • Boost.Inteprocess allocators' construct methods, in cooperation with Boost.Container containers and uses-allocator utilities, take advantage of the protocol to automatically propagate the state of the allocator to the uses_allocator compatible types stored in those containers.
  • Containers of containers, all using Boost.Interprocess allocators, automatically propagate the allocator state recursively without the user explicitly needing to do so.

More formally, a type T is compatible with the Boost.Interprocess implicit allocator propagation protocol for a managed segment instance ms if the following conditions are fulfilled when a user tries a ms.construct<T> or ms.construct_it<T> operation with argument list args...:

  • T::allocator_type is one of Boost.Interprocess allocator types (a void allocator fulfills this requirement)
  • T can be constructed with one of the following conventions:
    • The leading-allocator convention: T is constructible as T(allocator_arg, allocator_type, args...) -->
    • The trailing-allocator convention T is constructible as T(args..., allocator_type).

See the following example to see the difference between a type that does follow the uses-allocator protocol (UAType) and another that does not (Type). Note how when constructing UAType using the managed memory construction functions, the allocator is implicitly passed in constructors:

#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/container/vector.hpp>
#include <boost/container/uses_allocator_fwd.hpp>  //for allocator_arg_t

using namespace boost::interprocess;

typedef managed_shared_memory::segment_manager  seg_mngr_t;
typedef allocator<void, seg_mngr_t>             valloc_t;

struct Type    //Requires explicit allocator arguments
{
   typedef allocator<Type, seg_mngr_t> vec_alloc_t;
   boost::container::vector <Type, vec_alloc_t> m_vec;   //Recursive vector
   float m_v1;
   int   m_v2;

   Type(valloc_t va) //0-arg + alloc constructor
      : m_vec(va), m_v1(), m_v2() {}

   Type(std::size_t size, valloc_t va) //1-arg + alloc constructor
      : m_vec(vec_alloc_t(va)) //We can't use vector(size_type, allocator_type) as Type
      , m_v1(), m_v2()         //has no default constructor. Forced to one-by-one initialization.
   {  for(std::size_t i = 0; i != size; ++i)    m_vec.emplace_back(va);  }

   Type (valloc_t va, float v1, int v2) //allocator + 2-arg constructor
      : m_vec(vec_alloc_t(va)) //We can't use vector(size_type, allocator_type) as Type
      , m_v1(v1), m_v2(v2)     //has no default constructor. Forced to one-by-one initialization.
   {  for(std::size_t i = 0; i != 2; ++i) m_vec.emplace_back(va);  }
};

struct UAType   //Uses-Allocator compatible type
{
   typedef allocator<UAType, seg_mngr_t> vec_alloc_t;
   boost::container::vector <UAType, vec_alloc_t> m_vec;   //Recursive vector
   float m_v1;
   int   m_v2;

   typedef valloc_t allocator_type; //Signals uses-allocator construction is available

   UAType(valloc_t va)  //0 explicit args + allocator
      : m_vec(vec_alloc_t(va)), m_v1(), m_v2() {}

   UAType( boost::container::allocator_arg_t
         , allocator_type a , std::size_t size) //1 explicit arg + leading-allocator convention
      : m_vec(size, vec_alloc_t(a)), m_v1(), m_v2() {}

   UAType(float v1, int v2, allocator_type a) //2 explicit args + trailing-allocator convention
      : m_vec(vec_alloc_t(a)), m_v1(v1), m_v2(v2) {}
};

int main ()
{
   managed_shared_memory ms(create_only, "MyName", 65536);

   // 1 arg + allocator: Requires explicit allocator argument
   //    Type(size_type, valloc_t) called
   Type *ptype1 = ms.construct< Type >(anonymous_instance)(3u, ms.get_allocator<void>());

   // allocator + 2 arg: Requires explicit allocator-convertible argument (segment_manager*)
   //    Type(valloc_t, float, int) called
   Type *ptype2 = ms.construct< Type >(anonymous_instance)(ms.get_segment_manager(), 0.0f, 0);

   // 1 explicit arg + implicit leading allocator:
   //    UAType(allocator_arg_t, allocator_type, std::size_t) called
   UAType *pua1 = ms.construct<UAType>(anonymous_instance)(3u);

   // 2 explicit args + implicit trailing allocator:
   //    UAType(float, int, allocator_type) called
   UAType *pua2 = ms.construct<UAType>(anonymous_instance)(0.0f, 0);
   return 0;
}

PrevUpHomeNext