Columns


Questions & Answers

Pete Becker

Availability vs. Membership

Derived classes don't "possess" inherited data members they merely have access to them. Pete explores this and other subtle mysteries of C and C++.


Q

When a derived class inherits a member function from a base class, why can that function access private data in the base class?

A

When I got this question I started to write down the answer, thinking it was one of those questions that I already understood. After several false starts I had to move it into a different category, and have spent the last several weeks thinking about it off and on. Understanding this question is clearly fundamental to understanding how inheritance and access control interact in C++; yet it's something even experienced C++ programmers don't think about much. It just works. My problem is this: the words I would use to describe why this works don't fit. I have had to rethink how I talk about inheritance. (Maybe you are luckier, and got the terminology correct along with the concepts the first time; but I needed some review. Of course, if you are new to C++ this is all new.)

C++ lets you declare class members public, protected, or private. This is known as access control. The goal of access control is to help the writer of the class suitably protect the class members from misuse. In most cases suitable protection implies making data private, so that it is only available to member functions, friend functions, and friend classes. If you make the data in your classes private then users of your classes cannot inadvertently write code that depends on the data representation that you have chosen: they can use only the public member functions that you have provided. This sort of access control leaves you free to change the implementation in the future if that becomes appropriate. Proper use of access control improves maintainability by forcing users of the class to use only a clearly defined and robust interface. This also makes the class easier to understand and use.

When a class is derived from another class we often say that the members of the base class are members of the derived class. That's where I found my terminology confusing. In fact members of the base class are not members of the derived class, but are available to users of the derived class. For example:

class Base
{
public:
    int Size() { return DataSize; }
private:
    int DataSize;
};
class Derived : public Base
{
public:
    void Func();
};


Here, the class Derived has no data and only one member function of its own. Derived does, however, inherit both the data and the member function defined in Base. That is, it is perfectly valid to call Size for an object of type Derived:

Derived d;
cout << d.Size();

That's the whole point of inheritance: a derived class takes on the characteristics of its base class, and adds to them or specializes them to support its own behavior.

It is not correct, however, to say that Size is a member of Derived. It is not. It is a member of Base, and it is inherited by Derived. This may sound like a bit of a circumlocution, but keeping this distinction clear is critical to understanding the interaction between inheritance and access control in C++. If we let ourselves get sloppy and say that Size and DataSize are members of Derived we run into trouble: since member functions can access private data, the implication is that Func, which is a member of Derived and not of Base, can then also access DataSize. This, of course, would significantly weaken the benefits of declaring DataSize private in Base.

With this new clarity of vision it now becomes easy to understand why Size can access DataSize, even when it is called from an object of type Derived: Size is a member function of Base, so it can access private data defined in Base. The fact that we are accessing it from Derived doesn't change this, it can get at the private data because it is a member of Base. Func, on the other hand, is a member of Derived. It cannot access private members of Base, so it would not be valid to refer to DataSize in the implementation of Func.

One of the key notions underlying C++ is that you ought to be able to read a class definition and discern the valid operations on objects of that class. This readability should flow from the fact that the characteristics of a class are defined by the class itself. Deriving from a class does not change the validty of operations on base class members. That's fundamental to C++'s support for encapsulation.

Q

We subscribe to CUJ and appreciate the many helpful articles in your great publication. My question is: is it possible to print an amount of money from a variable in Borland C++ with a routine that inserts commas thus:

$123,456,789.00

instead of $123456789.00, with no commas? If C has this capability, where can I get an example of the routine which handles the printout?

— Rex McDaniel

A

There's no built-in function in C or C++ to do this, but writing your own isn't very hard. The key, as in most programming problems, is to be clear on what you want to accomplish before you start writing code. So let's begin by setting out in more detail what the task is.

First, let's decide, somewhat arbitrarily, to represent amounts of money as long integer values which hold the amount in pennies. This avoids the need for floating-point arithmetic and the confusion that often surrounds its use. Using long limits us to around twenty million dollars on a 32-bit system; for most of us this will not be a significant limitation.

To display a dollar amount, we need to show the pennies as two digits to the right of the decimal point. In most uses of currency if the amount is less than one dollar we expect to see a 0 to the left of the decimal point. Other than that, we don't want to put in any leading zeros. We also need to insert a comma to separate the thousands, unless the amount is less than $10,000. That is, amounts like $1995.00 are usually written without any commas. Finally, if the amount is negative we need a minus sign at the left end of the number, and we always want to begin with a dollar sign.

We'll use a little trick to make the job of formatting easier. The simplest way to pick off individual digits of the dollar amount is to take the remainder modulo ten and convert that value to a character, then divide the dollar amount by 10, and continue until the remaining dollar amount is 0. This gives us the correct characters to show the dollar amounts. As we generate these characters we have to insert them into a text buffer from right to left. A trick that makes this much simpler is to begin inserting at the right-hand end of the text buffer, without regard to how many characters we're eventually going to put into the buffer. When we're done we return a pointer to the leftmost character, which won't necessarily be in the first character position in the buffer. That doesn't matter, though. We can use that pointer just as easily as a pointer to the beginning of the buffer.

Here's the code to do this:

const char *money(long amount, char *buf, int max_len)
{
    char *current;
    int digit;
    int comma;
    int negative;
    /* check that buffer is */
    /* big enough for $0.00 */
    if( max_len < 6 )
        return NULL;
    /* initialize the text buffer */
    current = buf+max_len-1;
    *current— = '\0';
    /* set up for negative amounts */
    negative = 0;
    if( amount < 0 )
        {
        negative = 1;
        amount = -amount;
        }
    /* convert the pennies to text */
    digit = amount % 10;
    amount /= 10;
    *current— = (char)(digit + '0');
    digit = amount % 10;
    amount /= 10;
    *current— = (char)(digit + '0');
    *current— = '.';
    /* set up counter for handling commas */
    comma = 3;
    if( amount < 10000 )
       comma = 6;
    /* convert the dollars to text */
    do  {
        /* check for buffer overflow */
        if( current == buf )
            return NULL;
        digit = amount % 10;
        amount /= 10;
        *current— = (char)(digit + '0');
        /* do we need a comma? */
        if( -comma == 0)
          {
          comma = 3;
          *current— = ',';
          }
        } while( amount != 0 );
    /* insert minus sign if needed */
    if( negative )
        *current— = '-';
    /* insert dollar sign */
    *current = '$';
    return current;
}

We can exercise this function with a quick little test program:

#include <stdio.h>
int main()
{
    char buf[20];
    printf( "%s\n", money( 1L, buf, sizeof buf) );
    printf( "%s\n", money( -1L, buf, sizeof buf) );
    printf( "%s\n", money( 100L, buf, sizeof buf) );
    printf( "%s\n", money( 1000000L, buf, sizeof buf) );
    printf( "%s\n", money( 100000000L, buf, sizeof buf) );
    printf( "%s\n", money( -1000000000L, buf, sizeof buf) );
    return 0;
}

When we run it we get this output:

$0.01
$-0.01
$1.00
$10,000.00
$1,000,000.00
$-10,000,000.00


Now, it may bother you that function money takes a pointer to a buffer, rather than creating its own. Your first inclination might have been to write money with a static text buffer, and then return a pointer to the buffer. The problem with that approach is that it won't work in a multi-threaded environment: if two threads called money at the same time, they'd both be writing into the buffer at the same time, and the results would be unpredictable. You could guard against this by using a semaphore, but it's much easier to write the function so that it doesn't need to guard its data. The calling function provides the buffer, so you don't have to worry about it.

You do, however, have to worry about calling money twice within the same statement. You'll run into trouble if you do it like this:

int main()
{
    char buf[20];
    printf( "%s, %s\n",
            money( 1L, buf, sizeof buf ),
            money( 2L, buf, sizeof buf ) );
    return 0;
}

In this case, depending on how your compiler handles the two calls to money, you'll get either $0.01 or $0.02 printed twice. The compiler has to make both calls to money before calling printf, so whichever call it makes second overwrites the result of the first call. This is basically the same problem that you'd have in a multi-threaded application: the code uses the same buffer for two different purposes at once. It's easy enough to fix: don't put two calls to money in the same statement, or provide two buffers.

One problem that this version of money does not deal with is internationalization. This version is hard-coded to produce strings formatted in terms of U.S. dollars. If you need to modify it to support other writing systems you will need to explore the locale function in the standard C library. Use locale to determine what character to use to indicate the decimal point, to separate thousands, and for the currency symbol. locale also tells you whether the currency symbol belongs on the left of the amount or on the right. The changes needed to make this work are fairly straightforward. Now, I've forgotten: how many yen can I get for a dollar?

Q

Please tell me if I'm wrong here. I always thought that if I open a file in binary mode and read it with fread, I will get the data that actually resides on the file (unmolested by any system I/O functions). I thought that opening in text mode allowed me to use string-oriented operations such as fgets. Basically, I thought that these string oriented functions recognized newline characters and replaced them with null characters. Is it true that if I open a file in binary mode and read it with fread that I will get exactly what's recorded in the file? I seem to be able to do this on the systems I have experience with, but some people have said this is undefined behavior if the file was written in some other mode (or by some other language).

A

You've hit on one of the points that I keep trying to make in this column: no matter how many systems you are familiar with where a particular construct works, you cannot claim that it is portable unless the language definition says it is. The general behavior you describe for text mode and binary mode files is correct for DOS and Windows, and from what I hear, for UNIX as well. That doesn't mean that that's what every compiler must do on every system, though. In order to determine what a compiler should do we need to look at the language definition. In this case we should go to the ANSI or ISO standard for C.

Section 4.9.2 of the ANSI C standard provides the definition of a stream. There are two forms of stream: text and binary. Here's a part of what the standard says about text streams:

A text stream is an ordered sequence of characters composed into lines, each line consisting of zero or more characters plus a terminating newline character. Whether the last line requires a terminating newline character is implementation-defined. Characters may have to be added, altered, or deleted on input and output to conform to differing conventions for representing text in the host environment. Thus, there need not be a one-to-one correspondence between the characters in a stream and those in the external representation.

That's the part that most people have run into at one time or another: text mode streams read and write data in accordance with the conventions of the host environment. To the extent that those conventions do not map directly into C, the input and output functions will modify the data that they deal with in an appropriate manner. That is, provided that data meets a few simple requirements:

Data read in from a text stream will necessarily compare equal to the data that were earlier written out to that stream only if: the data consist only of printable characters and the control characters horizontal tab and newline; no newline character is immediately preceded by space characters; and the last character is a newline character. Whether space characters that are written out immediately before a newline character appear when read in is implementation-defined.

If your input data meets these requirements you can use a text mode stream to read it into a program. Incidentally, the business in the earlier section about the last line possibly requiring a newline is also reflected in section 2.1.1.2, which requires that "a source file that is not empty shall end in a newline character .." Yes, there are file systems out there that cannot handle text files that don't end in newlines.

In contrast, the description of binary streams is quite a bit shorter:

A binary stream is an ordered sequence of characters that can transparently record internal data. Data read in from a binary stream shall compare equal to the data that were earlier written out to that stream, under the same implementation. Such a stream may, however, have an implementation-defined number of null characters appended to the end of the stream.

Pretty simple, isn't it? You can only read in a binary stream that was written out under the same implementation. There is no guarantee that what is written out bears any easily observable resemblance to the data contained in the program. The only requirement is that when you read it back in you get what you had before. So, for example, a stream implementation that compresses data when it writes and decompresses it when it reads conforms to the standard. If you try to read an ordinary text stream with such an implementation the decompressed result probably won't make much sense.

So the people who told you that you get undefined behavior if you try to use a binary stream to read data that was written in some other mode or by some other language are right. The C language definition does not place any constraints on what a conforming compiler should do in those circumstances. The effects are undefined. You may be tempted to ignore this warning, because you know how your compiler works when you read text in binary mode. But code that relies on such behavior is not portable. Sooner or later someone is going to try to move your code to a different system, a different compiler, or even a newer version of your current compiler, and they'll be rather upset with you if it doesn't work. If you do this, be sure that you document any assumptions you make about the form of the input stream. Two years from now the maintenance programmer who has to make your code work on a new system will thank you. That maintenance programmer might even be you.

Q

My question concerns object pointers and operators. I have the following code:

STRINGCLASS *ptr;
char ch;
ptr = new STRINGCLASS("THIS IS A TEST");
// class STRINGCLASS has an operator
// [] defined that will return the
// character at the specified index
// of the string.  I am trying to
// access the ith character in the
// string as in the following:
ch = ptr[3];
//but without any luck.

My question is, how does ptr use the [] operator? I cannot seem to get it to work correctly. I've tried many different ways. I can access other member functions okay by using the ptr->xxx format but it does not work for my [] operator.

Thanks, Paul Shinohara

A

It actually does work using operator ->, but the syntax is not what you might expect if you haven't come across this before. Remember that in your class declaration you created your [] operator by naming it operator[](int). You can call it through a pointer by using that name:

ch = ptr->operator[](3);

I hope that that form of expression makes you wince. It's ugly, and should be avoided if at all possible. You do need to know about it, though, because sometimes it's the only way to do things. For example:

class Base
{
public:
     int operator[](int);
};
class Derived : public Base
{
public:
     int operator[](int);
};

Now if, for some reason, the operator[] in Derived needs to call the operator[] in Base, the easiest way to do it is with an explicit function call:

int Derived::operator[](int i)
{
     return static_cast<Base *>(this)->operator[](i);
}


But I'm digressing. In almost all cases there are much better ways to call operator[] through a pointer to an object. The thing to keep in mind is that this operator is a member of your class, and will be invoked by the compiler only for objects of your class and of classes derived from it. It will not be invoked on a pointer. Pointers already have an index operator defined for them, and you cannot change the behavior of this built-in operator. So in an expression like ptr[3] the compiler generates code that accesses the third object of type STRINGCLASS in the array pointed to by ptr. This is ordinary pointer arithmetic, just like in C.

When you want to use the index operator on a STRINGCLASS object you must provide the compiler with an expression that produces a STRINGCLASS object and apply the index operator to the result of that expression. The most common way to get at an object that a pointer points to is to dereference the pointer:

ch = (*ptr)[3];

This tells the compiler to figure out what object ptr points to, apply the index operator to that object, and assign the result to ch.

There's another way to get at the object that a pointer points to: use the built-in index operator with an index of 0. If you do this the resulting code will look like this:

ch = ptr[0][3];

This code is misleading because it implies that there is an array of objects of type STRINGCLASS at the location pointed to by ptr. So I recommend against using this technique when you have a pointer to a single object. However, if you actually have an array of STRINGCLASS objects this is simply the usual way of getting at them:

STRINGCLASS ptr = new STRINGCLASS[NUM_STRINGS];
for( int i = 0; i < NUM_STRINGS; i++ )
     ch += ptr[i][3];

In the expression ptr[i][3] the first index operator is the built-in index operator for pointers. It selects the STRINGCLASS object to be operated on. The second index operator is your index operator, and it will be applied to the STRINGCLASS object that the first index operator selected.

The key to understanding what's happening here is to remember that overloaded operators must be applied to at least one user-defined object. When an operator is applied only to built-in types you get the built-in operator.

Q

I can chain an overloaded operator()(arg) function which returns a reference to a class. Why can't I chain a member function like modify()?

enum color {white,blue,tan,green};
enum form {regular,tall,big};
class Shirt
{
     color color_;
     form form_;
public:
     Shirt(color c=white,
          form f=regular)
          : color_(c), form_(f) {}
     Shirt& modify(color x)
          {
          color_ = x;
          return *this;
          }
     Shirt& modify(form x)
          {
          form_ = x;
          return *this;
          }
};
int main()
{
Shirt s1;                // white, regular
s1.modify(blue).modify(tall); // blue, tall
Shirt s2.modify(tall);        // Improper use of
                            // typedef 'Shirt'
Shirt s3(blue).modify(tall);  // Improper use of
                            // typedef 'Shirt'
return 0;
}

— Glen Deen

In short, because the lines that attempt to modify s2 and s3 also attempt to create them. C++ doesn't allow you to do this many different things in the same statement.

When you create an auto or static object in C++ you use one form of what the language grammar calls a declaration. This form of declaration consists of a decl-specifier followed by a declarator-list followed by a semicolon. A decl-specifier in this case is simply the name of a type. A declarator-list is a list of init-declarators separated by commas. An init-declarator, in turn, is a declarator followed optionally by an initializer. The declarator here is the name of the object being declared. The initializer is an expression-list enclosed in parentheses or an equal sign followed by an assignment-expression. If you didn't follow this explanation, don't worry too much: it's rather abstract. If you did follow it, don't take it too much to heart: it's greatly simplified.

The reason for going through this analysis, however, is to point out that the C++ language has a well-defined syntax that tells you what is valid in a statement that declares an object. Basically the initializer in the object declaration can take one of three forms:

// no initializer:
Shirt s0;
// assignment-expression:
Shirt s1 = blue;
// parentheses:
Shirt s2( blue, tall );

You've probably seen or used each of these forms at one time or another. You haven't seen or used any other form because there are no other valid forms for an object declaration. These are the only ones the grammar allows when you are creating a single object of a class that has a constructor. You cannot extend these forms by calling member functions on the object you are defining. There simply is no place in the grammar for such a call.

This isn't really as restrictive it may look. In particular, the second and third forms can contain arbitrarily complex expressions in their initializers. If you like this form of chained expression you can use it to create objects like this:

Shirt s3 = Shirt().modify(tall);
Shirt s4( Shirt(blue).modify(tall) );

In the first line the compiler will create a temporary object using the default constructor for Shirt, call modify(tall) on that object, use the resulting object as the initializer for s3, then destroy the temporary object. In the second line the compiler will create a temporary object using the constructor for Shirt that takes a color, call modify(tall) on that object, use the resulting object as the initializer for s4, then destroy the temporary object. Personally, I find these too complex and distracting, so I wouldn't write them this way. But it's a close call, and I wouldn't get upset with someone who wrote code like this.

There's another potential problem, though, besides readability: each of these initializers creates then destroys a temporary object. In these examples, a Shirt object is small enough that the space needed for the temporary object and the time needed to copy the temporary object into the object we are creating and to destroy it afterwards are probably not noticeable, so initializing with a temporary does not produce a significant performance problem. However, for larger objects, creating this extra object may require too much space or take too long. In such a case you should use the more straightforward forms:

Shirt s3;
s3.modify(tall);
Shirt s4(blue);
s4.modify(tall);


So, as usual, try to write your code so that it is clear. Chaining member function calls may look cool, but it tends to obscure what the code is doing. Unless there is an offsetting benefit, you shouldn't do it. o

Pete Becker is Senior QA Project Manager for C++ at Borland International. He has been involved with C++ as a developer and manager at Borland for the past six years, and is Borland's principal representative to the ANSI/ISO C++ standardization committee.