Using member new to map devices - Embedded.com

Using member new to map devices

Declaring operator new as a class member can be a handy way to provide guaranteed initialization for memory-mapped objects.

Last year, I wrote a number of columns describing alternative techniques for representing and manipulating memory-mapped devices in C and C++. Due to space limitations, the initial articles left a lot of details unresolved.1 ,2,3 Most of the columns I've written since have focused on filling in the missing details, including a detour into constructors and automatic initialization.4 ,5

Classes typically use constructors to perform object initialization. Classes for memory-mapped devices should be no different. However, as I explained in August, many common declarations for memory-mapped objects don't invoke constructors implicitly.6

In my most recent column, I explained how you can use new with placement in C++ to invoke constructors explicitly.7 Explicit constructor calls can be very useful when you need to control initialization order precisely. However, implicit constructor calls should still be the norm because they provide guaranteed initialization.

This month, I'll show you how to provide guaranteed initialization for memory-mapped devices by defining operator new as a class member.

Where we were

For my examples, I've been using a class that represents a programmable timer. The class definition looks like:

typedef uint32_t volatile device_register;class timer_type    {public:    enum { TICKS_PER_SEC = 50000000 };    typedef uint32_t count_type;    timer_type() { disable(); }     void disable() { TMOD &= ~TE; }    void enable() { TMOD |= TE; }    void set(count_type c) { ... }    count_type get() const { ... }private:    enum { TE = 0x01 };    device_register TMOD;    device_register TDATA;    device_register TCNT;    };   

The class has private data members that represent the timer's device registers, along with public member functions that provide a modest assortment of basic timer operations. One of those operations is a default constructor (highlighted above in red).

The Standard C++ Library provides a placement form of operator new declared in the standard header as:

void *operator new(std::size_t, void *p) throw ();   

Calling this function does nothing but return the value of its second argument. Programs can use this placement operator new to construct an object at a particular address.

For example, you declare a timer object as:

extern timer_type the_timer;   

and use the linker to place the object in memory. Then you can apply the constructor via the placement new-expression:

new (&the_timer) timer_type;   

(I explained the placement new syntax in my previous column.7 ) If you define the timer using a constant pointer instead:

timer_type *const the_timer    = reinterpret_cast(0xFFFF6000);   

then you can apply the constructor via the placement new-expression:

new (the_timer) timer_type;   

Using placement new doesn't provide guaranteed initialization. By declaring a memory-mapped object and then initializing it using placement new , you can inadvertently do the first step without the second. Fortunately, you can get guaranteed initialization by using member operator new to combine the steps.
Operator new as a class member
In applications that use dynamic memory, it often happens that the majority of dynamically-allocated objects are of just a few types. When that is the case, you may be able to achieve significant performance improvements by using special-purpose allocation and deallocation functions for just those few heavily-used types, while still using the library's general-purpose allocation and deallocation functions for all the other (less used) types.

C++ lets you implement special-purpose memory managers for objects of a particular class by defining operators new and delete as members of that class, as in:

class widget    {public:    void *operator new(std::size_t n);    void operator delete(void *p) throw ();    ...    };   

Thereafter, a new-expression such as

pw = new widget;   

allocates memory using widget::operator new rather than the global operator new . Likewise,

delete pw;   

deallocates memory using widget::operator delete .

You can use a member operator new to place and automatically construct a memory-mapped object, as in the timer_type class shown below:

class timer_type    {public:    enum { TICKS_PER_SEC = 50000000 };    typedef uint32_t count_type;    void *operator new(std::size_t)        {        return reinterpret_cast              (0xFFFF6000);        }    timer_type() { disable(); }    ~~~    };   

This operator new behaves like (global) placement new in that it places the object at a specified address. However, a new-expression that calls this member new doesn't use the placement syntax.

You can invoke this member operator new by using an ordinary-looking new-expression, such as:

timer_type *const the_timer = new timer_type;   

This new-expression uses the timer_type 's operator new to place the timer_type object in its memory-mapped location, and uses the timer_type 's default constructor to initialize the object automatically. Pretty neat, huh?

Name lookup
In addition to providing guaranteed initialization, using a member operator new instead of placement new avoids a potential mishap.

Once again, the Standard C++ Library provides a placement form of operator new declared as:

void *operator new(std::size_t, void *p) throw ();   

Calling this function does nothing but return the value of its second parameter. As I explained in my previous column, most C++ implementations define it as an inline function:

inlinevoid *operator new(std::size_t, void *p) throw ()    {    return p;    }   

The C++ Standard states that placement allocation functions are reserved, and a C++ program may not define functions that displace the versions in the Standard C++ library.8 However, a program can overload the placement allocation functions. As diligent reader willc2010 observed in an online comment, “there doesn't seem to be anything to prevent someone from defining this:

void *operator new(std::size_t count, timer_type *p)    {    do_something ;    return some_value ;    }   

which can completely, and silently, change the behaviour.”

Here's why this is a potential problem. You can define a pointer to the timer registers as:

timer_type *const the_timer    = reinterpret_cast        (0xFFFF6000);   

then initialize the timer using a placement new expression:

new (the_timer) timer_type;   

The compiler selects the placement operator new whose second parameter type most closely matches the type of the placement argument. In this case, compiler selects the overloaded placement new because its second parameter has the same type as the placement argument, the_timer , namely “pointer to timer_type “.

Defining operator new as a class member avoids any problems caused by an inappropriate overloading of global placement new . When the compiler sees a new-expression as in:

the_timer = new timer_type;   

it looks first for an operator new that's a member of class timer_type . If the compiler finds any member operator new , it stops looking and never considers any operator new declared globally. In this case, the compiler finds a member declared as:

void *operator new(std::size_t);   

and uses it to compile the new-expression just above.Since timer_type 's member operator new has no placement parameters, a placement new-expression such as:

new (the_timer) timer_type;   

simply won't compile.

If you define a usual (non-placement) operator new as a member of class timer_type , and you still want to be able to use placement new-expressions with timer_type , you must define both usual and placement operator new as class members, as in:

class timer_type    {public:    ~~~    void *operator new(std::size_t)        {       return reinterpret_cast            (0xFFFF6000);        }    void *operator new(std::size_t, void *p)        {        return p;        }    ~~~    };   

More to come
The member operator new defined just above works just fine when you have only one timer. When you have multiple timers at distinct addresses, you need to do something else. Of course, you can always define a placement operator new as a member, but then you don't get guaranteed initialization.

In a future column, I'll consider ways to augment member operator new so that it supports multiple instances of a device.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. For more information about Dan Saks, visit his website at www.dansaks.com. Dan also welcomes your feedback: e-mail him at . For more information about Dan .

Endnotes:
1. Saks, Dan. “Alternative models for memory-mapped devices,” Embedded Systems Design , May 2010, p. 9. www.embedded.com/224700534 .
2. Saks, Dan. “Memory-mapped devices as C++ classes,” Embedded.com, June 2010. www.eetimes.com/4200572.
3. Saks, Dan. “Compared to what?,” Embedded.com, August 2010. www.eetimes.com/4205983.
4. Saks, Dan. “Demystifying constructors,” Embedded Systems Design , January/February 2011, p. 9. www.eetimes.com/4212701.
5. Saks, Dan. “Constructors and object definitions,” Embedded.com, March 2011. www.eetimes.com/4213712.
6. Saks, Dan. “Difficulties constructing memory-mapped objects,” Embedded.com, August 2011. www.eetimes.com/4218361.
7. Saks, Dan. “Calling constructors with placement new,” Embedded Systems Design , September 2011, p. 9. www.eetimes.com/4219506.
8. ISO/IEC Standard 14882:2003(E), Programming languages – C++.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.