Calling constructors with placement new

August 31, 2011

Dan_Saks-August 31, 2011

ESC Boston 2011 speaker logoIn C++, classes are usually the best tool for modeling memory-mapped devices.1 You can use a class to hide messy details, such as device register layouts and bit masks, behind a simpler and more robust interface.

A constructor is a special class member function that provides guaranteed initialization for objects of its class type.2 Using constructors to initialize objects is common practice in C++. C++ programmers should reasonably expect classes for memory-mapped devices to do initialization via constructors, unless there's some compelling reason to do otherwise.

In many embedded systems, the appropriate way to initialize a device is to put it into an inactive state. For example, the constructor for a programmable timer might simply make sure that the timer is disabled, as in:

class timer_type
    timer_type() { disable(); }
Last month, I explained the problem with getting constructors for memory-mapped devices to execute automatically.3 This month, I'll explore one way to overcome it.

Recapping the problem
With most C and C++ compilers, you can name a memory-mapped object using a standard extern declaration such as:
extern timer_type the_timer;
and then use the linker to map the_timer to the desired address. However, this declaration is not a definition, so the compiler doesn't generate code to allocate storage or call a constructor.

Some C and C++ compilers provide a nonstandard language extension that lets you position an object at a specified memory address, such as:
timer_type the_timer @ 0xFFFF6000;
With most compilers that support this sort of declaration, this isn't a definition, so the compiler won't generate a constructor call for this, either.

Defining a pointer or reference to a memory-mapped object, such as:
timer_type &the_timer
    = *reinterpret_cast<timer_type *>(0xFFFF6000);
has the same initialization problem as the previous object declarations. This reference definition doesn't define the memory-mapped object itself. Here, too, the compiler won't generate a constructor call applied to the memory-mapped object.

If the compiler won't call a constructor implicitly, why not just write an explicit call, say, immediately after the object or reference declaration? That is, if you declare the timer object as:
extern timer_type the_timer;
why not write an explicit constructor call to go with it, such as:
the_timer.timer_type();     // ?
Because it won't compile. C++ won't let you call a constructor as if it were any other class member function. In taking on the job of generating constructor calls automatically, C++ denies you the ability to do it yourself. Well, almost.

In truth, although you can't call a constructor using the usual member function call syntax, you can call a constructor using a particular form of new-expression known as placement new-expression. To appreciate how it works, let's first review new-expressions in general.

In C++, you typically allocate dynamic storage using a new-expression such as:
pt = new T; 
where T is a type and pt is an object of type "pointer to T". When T is a class type, the new-expression not only allocates storage, but also invokes a constructor. Thus, using new is generally preferable to using the standard malloc function. Whereas malloc allocates raw storage of indeterminate value, new can create objects with coherent initial values.

A new-expression allocates memory by calling a function named operator new. Each C++ environment provides a default global allocation function declared as:
void *operator new(std::size_t n);
As with malloc, the argument to operator new is the size (in bytes) of the storage request, and the return value is the address of the allocated storage. However, operator new reports failure differently from malloc. Whereas malloc returns a null pointer if it can't allocate the requested storage, operator new throws an exception.4

Thus, for a class type T, a new-expression such as:
pt = new T; 
translates more-or-less into something like:
pt = static_cast(operator new(sizeof(T)));
The first statement acquires storage for a T object by calling operator new, and converts the address of that storage from type void * to type T *. The second initializes the storage by applying T's default constructor. As I mentioned earlier, a C++ compiler won't let you write this explicit constructor call, but it's happy to do it for you.

If class T has a constructor that accepts arguments, you can get the new-expression to call that constructor by providing the constructor arguments as part of the new-expression, as in:
pt = new T (x, y, z);.
In this case, the new-expression translates more-or-less into something like:
pt = static_cast(operator new(sizeof(T)));
pt->T(x, y, z);
Overloading operator new
As with any other function, you can overload operator new by simply declaring additional functions with the same name but different parameter types. For example, in addition to the standard allocation function:
void *operator new(std::size_t n);
you might declare:
void *operator new(std::size_t n, other_info i);
where other_info is some user-defined type for conveying additional information to the allocation function. That's simple enough, but how can you get a new-expression to use this alternative allocation function?

The problem is finding some way to pass the additional argument to operator new. You can't add a parenthesized argument list after the type name in the new-expression because the compiler will interpret that list as arguments to a constructor, not as arguments to an operator new. That is:
pt = new T (x); 
passes x as an argument to a T constructor, not as an additional argument to operator new.

Rather, you must squeeze the additional argument(s) to the allocation function into some other spot in the new-expression. That spot is between the keyword new and the allocated type. For example, the new-expression in:
other_info info;
pt = new (info) T;
specifies info as an additional argument to operator new. As always, the new-expression uses sizeof(T) as the first argument to operator new. Thus, the new-expression results in a call to:

operator new(sizeof(T), info).

Compilers apply the usual rules for argument matching in overload resolution to find an allocation function that will accept this assembled argument list.5 The new-expression won't compile if no operator new is visible that will accept the given arguments.

< Previous
Page 1 of 2
Next >

Loading comments...