C++ Newsletter/Tutorial Issue 6
Issue #006
February, 1996
Contents
- Introduction to Stream I/O Part 1 - Overloading <<
- Writing Robust C++ Code Part 3 - Stream I/O
- Using C++ as a Better C Part 6 - Operator Overloading
- Stream I/O Performance
- Further Comment on New bool Fundamental Type
INTRODUCTION TO STREAM I/O PART 1 - OVERLOADING <<
In this issue we will begin discussing the stream I/O package that comes with C++. The first four sections of this issue are related and present several aspects of stream I/O along with some related topics.
If you've used C++ at all, you've probably seen a simple example of how to do output:
cout << "Hello, world" << "\n";
instead of:
printf("Hello, world\n");
cout is an output stream, kind of like stdout in C. The C example could be written as:
fprintf(stdout, "Hello, world\n");
which makes this correspondence a bit clearer.
Once you get beyond simple input/output usage, what is the stream I/O package good for? One quite useful thing it can do is to allow the programmer to take control of I/O for particular C++ types such as classes. This end is achieved by the use of operator overloading.
Suppose that we have a Date class:
class Date {
int month;
int day;
int year;
public:
Date(char*);
Date(int, int, int);
};
with an internal representation of a Date using three integers for month, day, and year, and a couple of constructors to create a Date object. How would we output the value of a Date object?
One way would be to devise a member function:
void out();
implemented as:
void Date::out()
{
printf("%d/%d/%d", month, day, year);
}
This function would operate on an object instance of a Date and would access the month/day/year members and display them. This approach will certainly work and may be suitable in some kinds of applications.
But this scheme doesn't integrate very well with stream I/O. For example, I cannot say:
Date d(9, 25, 1956);
cout << "Today's date is " << d;
but must say:
printf("Today's date is ");
d.out();
For this purpose it is necessary to overload the << operator. We can add a friend function to Date:
friend ostream& operator<<(ostream& os, const Date& d);
with definition:
ostream& operator<<(ostream& os, const Date& d)
{
return os << d.month << "/" << d.day << "/" << d.year;
}
With this definition, it is possible to say:
Date d(9, 25, 1956);
cout << "Today's date is " << d << "\n";
Several aspects of this example need explanation. An overloaded operator in C++ is an operator like "+" or "<<" that is given a special meaning for certain kinds of arguments, and turns into a function. Wherever the operator is used with these arguments, a function is called. So, for example, an output statement:
cout << "xxx";
is actually:
cout.operator<<("xxx");
which is a valid function call in C++ if you're using stream I/O.
cout is an instance of class ostream (at least conceptually; the actual hierarchy is a bit complicated). When we wrote the actual statement to output a formatted Date:
return os << d.month << "/" << d.day << "/" << d.year;
we returned the ostream reference so as to allow << usage to be chained. Because << operators group left to right, a sequence like:
cout << "x" << "y";
actually means:
cout.operator<<("x").operator<<("y");
Finally, the reason that we declared operator<<(ostream&, const Date&) as a friend and not a member is that a member function that is a binary operator has an implicit convention on argument usage, namely, that for some operator @:
x @ y
means:
x.operator@(y);
that is, the left operand of the operator must be an instance of the class of which the overloaded operator is a member.
WRITING ROBUST C++ CODE PART 3 - STREAM I/O
Suppose that you wish to output three values and you use some C-style output to do so:
printf("%d %d %d\n", a, b);
What is wrong here? Well, the output specification calls for three integer values to be output, but only two were specified. You can probably "get away" with this usage without your program crashing, with the printf() routine picking up a garbage value from the stack. But many cases of this usage will crash the program.
A similar case would be:
printf("%d %d %d\n", a, b, c, d);
which is even more likely to work, with the extra argument ignored. This problem is intrinsic to printf() and related functions.
Using stream I/O as illustrated above eliminates this particular problem completely:
cout << a << " " << b << " " << c << "\n";
as well as the related problem illustrated by:
int a;
printf("%s\n", a);
where the argument is of the wrong type. Stream I/O is fundamentally safer than C-style I/O; stream I/O is said to be "type safe".
USING C++ AS A BETTER C PART 6 - OPERATOR OVERLOADING
Suppose that you are using an enumeration and you wish to output its value:
enum E {e = 37};
cout << e;
37 will indeed be output, by virtue of the enumerator value being promoted to an int and then output using the operator<<(int) function found in iostream.h.
But what if you're interested in actually seeing the enumerator values in symbolic form? One approach to this would be as follows:
#include <iostream.h>
enum E {e1 = 27, e2 = 37, e3 = 47};
ostream& operator<<(ostream& os, E e)
{
char* s;
switch (e) {
case e1:
s = "e1";
break;
case e2:
s = "e2";
break;
case e3:
s = "e3";
break;
default:
s = "badvalue";
break;
}
return os << s;
}
main()
{
enum E x;
x = e3;
cout << x << "\n";
cout << e1 << "\n";
cout << e2 << "\n";
cout << e3 << "\n";
cout << E(0) << "\n";
return 0;
}
In the last output statement, we created an invalid enumerator value and then output it.
Operator overloading in C++ is very powerful but can be abused. It's quite possible to create a system of operators such that it is difficult to know what is going on with a particular piece of code.
Some uses of overloaded operators, such as [] for array indexing with subscript checking, -> for smart pointers, or + - * / for doing arithmetic on complex numbers, can make sense, while other uses may not.
STREAM I/O PERFORMANCE
Is stream I/O slower than C-style standard I/O? This question is a bit hard to answer. For a simple case like:
#ifdef CPPIO
#include <iostream.h>
#else
#include <stdio.h>
#endif
main()
{
long cnt = 1000000L;
while (cnt-- > 0)
#ifdef CPPIO
cout << 'x';
#else
putchar('x');
#endif
return 0;
}
the C++ stream I/O approach is about 50% slower for a couple of popular C++ compilers. But putchar() is a macro (equivalent to an inline function) that has been tuned, whereas the C++ functions in iostream.h are less tuned, and in the 50% slower case not all the internal little helper functions are actually inlined. We will say more about C++ function inlining some other time, but one of the issues with it is trading space for speed, that is, doing a lot of inlining can drive up code size.
And 50% may be irrelevant unless I/O is a bottleneck in your program in the first place.
FURTHER COMMENT ON NEW BOOL FUNDAMENTAL TYPE
In the last issue we talked about the new fundamental type "bool". Two additional comments should be made about this feature. An example of how Boolean has been faked in C was given:
typedef int Bool;
#define FALSE 0
#define TRUE 1
and then usage like this:
Bool b;
b = 37;
was presented, with a comment that a C compiler would not complain. A C++ compiler given similar usage:
bool b;
b = 37;
will not complain either, but the two sequences are not the same. In the C case, a later statement like:
if (b == TRUE)
...
will fail, because it reduces to:
if (37 == 1)
...
In the C++ case, the statement:
b = 37;
turns into:
b = true;
and a later test:
if (b == true)
...
will indeed succeed.
The issue was also raised as to why bool was not implemented as a class type in some C++ standard library. Dag Bruck of the ANSI/ISO C++ committees sent an example of why this will not work.
There is a rule in C++ that says that at most one user-defined conversion may be automatically applied. A user-defined conversion is a constructor like:
class A {
public:
A(int);
};
to convert an int to an A, or a conversion function:
class A {
public:
operator int();
};
to convert an A to an int.
If bool is a class type, for example:
class bool {
public:
operator int();
};
then the call "f(3 < 4)" in this code:
class X {
public:
X(int);
};
void f(X);
main()
{
f(3 < 4);
}
will result in two user-defined conversions, one to convert the bool class object resulting from "3 < 4" to an int, the other to call the X(int) constructor on the resulting int.
-------------------------
Copyright (c) 1996 Glen McCluskey. All Rights Reserved.
This newsletter may be further distributed provided that it is copied in its entirety, including the newsletter number at the top and the copyright and contact information at the bottom.
Glen McCluskey & Associates
Professional C++ Consulting
Internet: glenm@glenmccl.com
Phone: (800) 722-1613 or (970) 490-2462
Fax: (970) 490-2463
FTP: rmii.com /pub2/glenm/newslett (for back issues)
Web: http://www.rmii.com/~glenm