Advertisement

Getting it just right

June 16, 2004

Dan_Saks-June 16, 2004

Sometimes the only way to catch your coding mistakes is by thoughtful critique from others.

In my last column ("How to Enforce Write-Only Access," May 2004, p. 37), I showed one of the many ways you can use C++ classes to impose semantic constraints on data objects, turning potential run-time errors into compile-time errors. In particular, I presented a class for a write-only device register, which lets you write a new value to a register but prevents you from reading a value from that register. I then generalized that specific class into a class template that can generate write-only instances for almost any data type.

A few readers wrote to me wondering if I had made mistakes in that column. Others speculated that I should have implemented the class or the template differently. This month, I'll address their concerns.

Accessing memory-mapped device registers
The first letter came from Michael Barr (mbarr@netrino.com). Michael recently stepped down as editor in chief of this magazine. As you'll see, it wasn't because he lost his edge. He wrote:

Dan,

It looks like someone messed with your code in the online version of "How to Enforce Write-Only Access." I believe that the pointer definitions:

    #define UART ((special_register *)0x03FFD000)

    special_register *const UART
        = (special_register *)0x03FFD000;


should have the special_register * replaced by UART *. Or am I missing something? Otherwise, another great column.

Cheers,
Michael

Well, I certainly agree with that last statement. Unfortunately, I have to agree with the rest as well. And much as I'd like to point a finger elsewhere, I'm the someone who messed with my code. Chalk this one up to carelessness. Allow me to recap what I wrote and correct the mistakes.

To illustrate the need for a write-only type, I drew an example from the ARM Evaluator-7T, a single-board computer built around the Samsung KS32C50100 processor (based on the ARM7 core). The Evaluator-7T includes a small assortment of peripheral devices, all of which use memory-mapped device addressing. Programs running on this processor communicate with devices by reading from and writing to selected locations in a 64KB bank of "memory" called the special registers.

The special registers start at address 0x03FF0000. The memory is byte-addressable, but each register is a four-byte word aligned to an address that's a multiple of four. Special registers are volatile—they may change state in ways that the compiler can't detect. Therefore, you should declare them as volatile objects. A typedef such as:

typedef unsigned volatile special_register;

is a good way to capture these properties of special registers. The Evaluator-7T has two UARTs. Each UART has six special registers. You can access each of these registers as members of a struct defined as:

struct UART
    {
    special_register ULCON;
    special_register UCON;
    special_register USTAT;
    special_register UTXBUF;
    special_register URXBUF;
    special_register UBRDIV;
    };

Now, here's where I made the mistake. I wrote:

For UART 0, these registers reside at address 0x03FFD000. A program can access these registers via a pointer that points to that address. You can define that pointer as a macro, as in:

#define UART ((special_register *)0x03FFD000)

or as a constant pointer, as in:

special_register *const UART
    = (special_register *)0x03FFD000;

As Michael observed, I declared each pointer with the wrong type. The pointer type should be UART * rather than special_register *. Had I used the correct type, the declarations would have looked like:

#define UART ((UART *)0x03FFD000)

and:

UART *const UART = (UART *)0x03FFD000;

Then it would have been obvious that I also misspelled the name of the pointer. The pointer should be UART0, not just plain UART. The correct declarations are:

#define UART0 ((UART *)0x03FFD000)

and:

UART *const UART0 = (UART *)0x03FFD000;

Later in the original article I wrote expressions that accessed the UART's device registers, such as:

UART->UBRDIV = 0xA20;

All occurrences of UART-> should be UART0->, as in:

UART0->UBRDIV = 0xA20;

A subtlety about initializing objects
I made another minor error in my last column. In describing the write_only_special_register class, I wrote:

The second member function is also a constructor:

write_only_special_register(special_register v)     : m(v)     {     }

This constructor initializes a write_only_special_register by copying the value of a special_register. The construct : m(v) on the second line of the constructor is a member initializer, which tells the constructor to initialize data member m by copying parameter v. This constructor would be called from a definition such as:

write_only_special_register w = 0;

Although some compilers will accept this definition for w, according to the C++ standard, they shouldn't. If your compiler compiles this definition as is, try compiling it again using the compiler's option to use only standard language features. For example, with Microsoft Visual C++, the /Za compile option disables language extensions. Then the definition should produce a compile-time error. If it still compiles, I suspect your compiler isn't enforcing the C++ standard properly.

The problem with the definition is that it uses an = operator to specify the initial value. I should have written the initial value enclosed in parentheses, as in:

write_only_special_register w (0);

In C++, the definition of an object of class type, like w just above, calls a constructor. A constructor may have zero or more parameters. The object definition passes arguments to the constructor by listing those arguments in a parenthesized list appearing immediately after the object's name in the definition. Here, the 0 in parentheses is the argument to the constructor.

A default constructor is a constructor that you can call without passing any arguments. A copy constructor for a class X is a constructor that initializes an X object by copying another X object. For example,

X a; // uses a default constructor
...
X b (a);

// uses a copy constructor

These constructors are so important to C++ that, in many cases, C++ compilers will generate them automatically. For class X, the typical declaration for its copy constructor is:

X(X const &x);

Again, this copy constructor copies one X object to another X object. In other words, it reads the value of one X object so it can write that value to another X object. That's why we don't want the compiler generating this constructor for the write_only_special_register class. We want to prevent such reads.

Unfortunately, when your compiler has the urge to generate a copy constructor, it's hard to make it stop. C++ doesn't have a way to simply tell the compiler "don't generate the copy constructor for this class." In many cases, the best you can do is declare the copy constructor as a private member, rendering it inaccessible outside the class. That's what I did in the write_only_special_register class, as shown in Listing 1.

Listing 1: A class for a write-only register type

class write_only_special_register
    {
public:
    write_only_special_register()
        {
        }
    write_only_special_register(special_register v)
        : m(v)
        {
        }
    write_only_special_register &
    operator=(special_register v)
        {
        m = v;
        return *this;
        }
private:
    special_register m;
     // Leave these functions undefined...
    write_only_special_register
        (write_only_special_register const &);
    write_only_special_register &
        operator=(write_only_special_register const &);
    };

I'll bet you're still wondering why:

write_only_special_register w (0);

compiles, but:

write_only_special_register w = 0;

doesn't. Well, I'll tell you. The first of these definitions (with parentheses) initializes w using direct initialization. That is, it initializes w by calling the constructor:

write_only_special_register(special_register v);

passing 0 as the constructor argument. (Recall that special_register is just a typedef that's an alias for a volatile unsigned [int]. The compiler can convert 0 to unsigned int with ease.)

In contrast, the second definition (with the equal sign) uses copy initialization, which initializes w in a more roundabout way. First, the program creates a temporary write_only_special_register object and initializes it by direct initialization with 0 as its argument. Then, it copies the temporary object to w using the copy constructor. Finally, it discards the temporary. In this case, however, the copy constructor is private and inaccessible at this point, so the declaration won't compile.

In many cases, compilers can optimize copy initialization into direct initialization. That is, compilers can, and usually do, eliminate the temporary object as well as the call to the copy constructor. Nonetheless, even if the compiler ultimately would eliminate the copy constructor call, it's not supposed to compile the definition if the copy constructor isn't callable.

By the way, I doubt you'd ever write a definition such as:

write_only_special_register w (0);

because write_only_special_register objects are usually memory-mapped objects referenced via pointers or references. Nonetheless, the declaration I wrote was wrong and I wanted to correct it. Also, this discussion of copy operations is good background for the next issue.

Passing by value vs. passing by reference-to-const
Mike DeSimone (desimone@arlut.utexas.edu) posed a couple of questions, the first of which was:

From what I understand, the constructor:

write_only_special_register(special_register v)

actually copies v twice: once when it copies the original register to the parameter before the function call, then again when the constructor copies the parameter to the [write-only] register. Would declaring it as:

write_only_special_register
    (special_register const &v)

eliminate a copy? Sure, it has to set the reference parameter, but it seems likely that the optimizer would eliminate that if the constructor were inlined.

I don't think it makes any difference either way. This definition appears within the class definition. In C++, a member function that's defined, not just declared, within its class definition is an inline function by default, even if it's not declared with the keyword inline. In my experience, C++ compilers have little trouble inlining functions as simple as this, and when they do, they also eliminate any unnecessary copying. I tested the class with several different compilers targeting a couple of different machines, and they all generated equally good code whether passing by value or by reference-to-const.

When I rewrote the write_only_special_register class into the class template write_only<T>, I rewrote the constructor:

write_only_special_register(special_register v);

as:

write_only(T v);

I left the constructor's parameter as a pass-by-value parameter. I wanted my readers to clearly see the similarities between the write_only_special_register class and the write_only<T> class template. But now I think it might be better to write the constructor with its parameter passed by reference-to-const.

First, the template parameter T could be replaced by either a built-in type or a class type. When T is a class type, passing a T parameter by value, as in:

write_only(T v);

calls T's copy constructor. If T's copy constructor is too complicated to inline, then the compiler might not be able to eliminate the extra copy.

I tested this idea on a couple of compilers. I created a class widget with a complicated copy constructor declared as a non-inline function. Then I defined:

widget w;
write_only<widget> wow (w);

to see if the compiler could avoid making an extra copy of w when initializing wow.

Here's what happened. When I declared the constructor as:

write_only(T v);

neither compiler eliminated the extra copy. However, when I changed the declaration to:

write_only(T const &v);

both compilers eliminated that extra copy. By the same reasoning, the assignment operator, which I previously declared as:

write_only &operator=(T v);

should be:

write_only &operator=(T const &v);

The revised write_only<T> class template appears in Listing 2.

Listing 2: A template for a write-only variant of any type T

template <typename T>
class write_only
    {
public:
    write_only()
        {
        }
    write_only(T const &v)
        : m(v)
        {
        }
    write_only &operator=(T const &v)
        {
        m = v;
         return *this;
        }
private:
    T m;
    // Leave these functions undefined...
    write_only(write_only const &w);
    write_only &operator=(write_only const &w);
    };

The result of an assignment
Paul Glaubitz (Paul.Glaubitz@aeroflex.com) wrote to me with a concern about the assignment operator in the write_only<T> template. He observed that given:

write_only<char> b, c;

an assignment such as:

c = b = 'A';

will not compile because the assignment to c uses the write_only<T> copy assignment operator, which is private and thus inaccessible here. He suggested that the assignment would compile if I changed the public assignment operator from:

write_only &operator=(T v)
    {
    m = v;
    return *this;
    }

to:

T &operator=(T v)
    {
    m = v;
    return v;
    }

This change may allow the assignment to compile, but then it produces undefined behavior. In particular, the revised operator= returns a reference to a parameter passed by value. The storage for that parameter disappears upon exit from the function, so the reference winds up referring to a nonexistent object. Many compilers issue a warning to alert you when this happens.

When you define an operator for a user-defined type, that operator should behave as much as possible like the built-in operators. The canonical way to implement an assignment operator is to return a reference to an object of the class type, referring to *this. This behavior matches the behavior of built-in assignment. However, write_only<T> is a class for a type whose value you can't read, so it disables its copy constructor and copy assignment by declaring them private. The consequence is that a statement that chains assignments together won't compile. This is as it should be.

Assignment operators (whether built-in or user-defined) always group from right to left so that:

c = b = 'a';

means:

c = (b = 'a');

which assigns 'a' to b, and then assigns b to c. The assignment from b to c attempts to read b, but since b is read-only, that assignment shouldn't compile. Indeed, it doesn't.

Fear no C++
I can imagine that some of you might be put off by some of these language details. It's unfortunate how often small changes in C++ code can produce substantive differences in the efficiency or compactness of the generated code. Some programmers don't have the patience for such details, and prefer to stick with C.

While I do agree that C++ is more complicated than it should be, I hope you're not deterred by it. I still think using C++ is well worth the bother.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. You can write to him at dsaks@wittenberg.edu.

Loading comments...