I have previously written a number of columns detailing 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 initialisation.4,5
Classes typically use constructors to perform object initialisation. 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 initialisation order precisely. However, implicit constructor calls should still be the norm because they provide guaranteed initialisation.
This month, I'll show you how to provide guaranteed initialisation 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 initialisation. 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 initialisation 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 initialise the object automatically. Pretty neat, huh?
Name lookup
In addition to providing guaranteed initialisation, 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:
inline
void *operator new(std::size_t, void *p) throw ()
{
return p;
}
文章评论(0条评论)
登录后参与讨论