[Postscript - 30 pages]

The Vector Data Type

    One of the goals of good software development in C++ is to construct each class so that it appears, to the applications programmer, to be equivalent to a built-in type for the language. A C++ class written in this way has been termed a ``concrete data type''. As with any class, the member functions of a concrete data type are quite specialized to the use of the class, but it is the underlying structure of the class that is specific to the concrete type. Once this underlying struture is seen, it is very easy to adapt to the design of all classes.

We have found that designing classes with this structure vastly increases our productivity with the language, as many of the errors are caught in the compiling of the programs (i.e. less debugging), and once compiled, the class tends to operate as the interface specifies.

It is easy to specify what a concrete data type should do. Unfortunately, it is more difficult to actually design the class so that it acts as a concrete type. Sometimes it is much better to see an example of an actual class an adapt our new classes from a working example - and this is what we offer in these notes. Here, the interface and implementation of a concrete data type of 3-dimensional vectors is presented. Each function is discussed in detail, including both the interface and implementation details which enable the class to be utilized as a built-in type.

The reader can obtain a copy of this class by looking here. World-Wide Web. The reader of these notes may find that some of the material is repetitive and, without the links, is somewhat disjoint. 

The class interface for the Vector data type is given below. Whereas such a class may have many member functions, we have included only a few for this example: the arithmetic functions that are required by the vector space properties of this class, and four sample member function - dot products, cross products, normalization and length.

#ifndef VECTOR_H
#define VECTOR_H

class Vector {

private :
double _x, _y, _z ;

public :
// Constructors

Vector () ;

Vector ( const double, const double = 0.0, const double = 0.0 ) ;

// Copy Constructor

Vector ( const Vector& ) ;

// Destructor

virtual tex2html_wrap_inline749Vector() ;

// Assignment

Vector& operator= ( const Vector& ) ;

// Output

friend ostream& operator<< ( ostream&, const Vector& ) ;

// Comparison

friend int operator== ( const Vector&, const Vector& ) ;

friend int operator!= ( const Vector&, const Vector& ) ;

// Arithmetic Operations

friend Vector operator+ ( const Vector&, const Vector& ) ;

friend Vector operator- ( const Vector&, const Vector& ) ;

friend Vector operator- ( const Vector& ) ;

friend Vector operator* ( const double&, const Vector& ) ;

friend Vector operator* ( const Vector&, const double& ) ;

friend Vector operator/ ( const Vector&, const double& ) ;

Vector& operator+= ( const Vector& ) ;

Vector& operator-= ( const Vector& ) ;

Vector& operator*= ( const double& ) ;

Vector& operator/= ( const double& ) ;

// Other Member Functions

friend double dot ( const Vector&, const Vector& ) ;

friend Vector cross ( const Vector&, const Vector& ) ;

void normalize () ;

double length () const;

// Access Functions

double x() const { return _x ; } ;

double y() const { return _y ; } ;

double z() const { return _z ; } ;

} ;

// Global Constant Data

const Vector VZero ( 0.0, 0.0, 0.0 ) ;
const Vector VX ( 1.0, 0.0, 0.0 ) ;
const Vector VY ( 0.0, 1.0, 0.0 ) ;
const Vector VZ ( 0.0, 0.0, 1.0 ) ;

#endif

To access information for any function in the interface, just select the desired function or modifier as presented in the interface. To get general information about the interface and implementation, select from the topics below.

Private Data
Public Area of the Interface
Constructors
The Copy Constructor
The Destructor
The Assignment Operator
Output
Comparison
Arithmetic Operations
Addition of Two Vectors
Subtraction of Two Vectors
Negation of a Vector
Scalar Multiplication
Scalar Divide
Vector Addition with Assignment
Vector Subtraction with Assignment
Scalar Multiplication with Assignment
Scalar Divide with Assignment
Other Member Functions
Dot Product
Cross Product
Normalize
Length of a Vector
The Coordinate Access Functions
Global Constants
Inline Functions
Uses of Const
Friends and Friend Functions
Compiler Directives

Private Data

The class is an instance of an abstract data type (or ADT) which combines data and the functions that operate on the data. The data is commonly kept in the private area of the class (to keep the general user from modifying it) and is part of the implementation of the class.

The implementation of a 3-dimensional vector class needs 3 required components - the x, y, and z-coordinate of the vector. There are several methods by which these three values can be defined - a three-element array, three individual elements, etc. - and each affects the way that the implementation is written. In these notes, the data defined by three individual values in the private area of the class.

   private :
        
      double                _x, _y, _z ;

All member and friend functions of this class manipulate these three values.

Note: We commonly utilize underscores as a prefix to our private data. Underscores are a common software engineering tool utilized in C++ to distinguish private data from other variables in the implementation code.

We adhere, in the design of concrete data types, to rule 20 of Meyers which states ``avoid data members in the public interface''. This gives the designer much more flexibility in the implementation of a class. If a data member is made public, then every user has access to it and can modify it - and the designer of the class must take this into consideration. For example, if it were necessary to change the implementation of the class, the fact that many applications have directly accessed a data item may make it impossible to change the class without changing the interface - and thus every application program - not desirable if this class is highly utilized.

If the applications programmer needs access to the private data items, then access functions should be provided. In the case of the Vector class above, access functions are provided to allow the user to utilize the x, y, and z coordinates of the vector.
singlespace325

In general, no class data item should ever be placed in the public interface.

The data can be made semi-public by utilizing access functions - and only member or friend functions should be allowed to change the data. If we follow these ideas, and late wish to change the implementation of the class then each of the member functions could be changed to reflect this new implementation, without changing the interface to the user.

Public Area of the Interface

The public area of the interface includes all functions that are publicly accessible to the users of this class. Normally, using the true encapsulation and information hiding principles of abstract data types, this will not include the data that is specific to the implementation of the class. Such data should only be contained in the private or protected areas of the interface.

Constructors

The default constructor is called by the compiler when the user either declares variables of the Vector type, initializes members of a array of Vectors, or calls the constructor directly. The constructor initializes the Vector objects to default values.

The implementation of the default constructor for the Vector class is given as follows:


singlespace339

The default constructor is normally in the public interface. (If the designer does not want arrays of the class type to be constructed, he can provide alternate constructors and put the default constructor in the private area so it cannot be accessed.)

If no constructors were provided for the class, then the compiler will provide the the default constructor on the programmer's behalf. In general, this shortcut causes the programmer more problems than it saves them coding time and it is not recommended. It is the designer who knows how the class is to operate and what default values should be set - not the compiler.

In the concrete data type paradigm, we always provide default constructors.

A second constructor is provided in the Vector class. This constructor is defined in the interface by
singlespace343
It takes three parameters, all doubles, with the last two defaulted to zero. This implies that this constructor can be called in three ways - with either one, two or three parameters.
singlespace345

In this case, v1 would represent the vector <1,0,0>, v2 would represent the vector <1,1,0>, and v3 would represent the vector <1,1,1>. The implementation will be unconditionally passed the three variables, with defaulted variables replaced, and can be implemented as follows:


singlespace350

Copy Constructor

The copy constructor and the assignment operator are similar as they both produce exact copies of an instance of a class. The copy constructor is the ``invisible'' member function in its operation - that is, it is rarely called explicitly by the programmer. It is utilized extensively by the compiler for type conversion, passing variables by value, returning objects from procedures, and for object initialization (which is the only time it is called explicitly).

The Vector copy constructor is defined in the interface by
singlespace356
The parameter passed to the routine is a reference to a Vector type, which means that this function has the actual object to work with, rather than a copy of it or a pointer to it.

The routine must copy each item of the instance referred to by the parameter, into the current instance of the class. An implementation of the Vector copy constructor is given below.


singlespace360

C++, as the compiler is written, will produce both copy constructors and assignment operators for the user. However, as Meyers notes, these automatically generated routines do not work whenever dynamically allocated memory is utilized in the class - the memory cannot be managed, and unfortunate side effects through scoping can happen. Thus this shortcut can cause the programmer more problems than it saves them in coding time and it is not recommended.

All classes should have a copy constructor and an assignment operator.

We frequently find, when working with existing classes, that we need to derive a new class, or modify an existing class to add functionality to our system. When doing this, it is quite common to create problems if the copy constructor and assignment operator are not present.

The Destructor

The purpose of the destructor is to clean up any of the resources that the object acquired through execution of its constructors or member functions. In the case of the Vector destructor, no dynamic resources are ever required, so that the implementation of the destructor is quite simple.

     Vector :: ~Vector () { }

We note that the definition of the destructor in the interface was

      virtual ~Vector () ;
The declaration of this function as virtual, is insurance against further derivations of this class. A virtual destructor will cause the destructors to be instantiated in the derived classes (if there is one), and thus all the destructors in the inheritance graph will be correctly called. Thus, this is insurance that if the Vector class is ever utilized as a base class, then the derived class destructor will correctly execute.

We note that this is insurance. But any well-written class will eventually be utilized as a base class (one of my observations of C++) and therefore all destructors should be declared virtual. It will save future debugging time.

The Assignment Operator

The assignment operator and the copy constructor are similar in function as they both produce exact copies of an instance of a class. In the case of the assignment operator, Meyers gives several rules that are worth studying. These rules state that

  • A copy constructor and assignment operator should be defined for each class with dynamically allocated memory.

    C++, as the compiler is written, will produce both copy constructors and assignment operators for the user. However, these automatically generated routines do not work whenever dynamically allocated memory is utilized in the class as the memory cannot be managed, and unfortunate side effects through scoping can happen. Thus this shortcut can cause the programmer more problems than it saves them coding time and it is not recommended.

    The concrete data type concept extends this rule by stating that all classes must have a copy constructor and an assignment operator. We frequently find, when working with existing classes, that we need to derive a new class, or modify an existing class to add functionality to a class. When doing this, it is quite common to create problems if the copy constructor and assignment operator are not present.

  • Have operator= return a reference to *this

    The reason for this is simple. As programmers, we frequently want to chain statements together as in the following

                   a = b = c ;

    Since the assignment operator is right associative, this expression is equivalent to

                   a.operator= ( b.operator= ( c ) )

    Thus, the return value of operator= must be acceptable as input to itself. Since the input to operator= is a reference to a class object, then it must also be the result of the operator (i.e. *this).

  • Make sure to assign to all data members of the class in both the copy constructor and the assignment operator.
  • Check for assignment to self in operator=.

    This is frequently overlooked by many programmers, but gets us out of the

                   a = a ;
    problem. This is actually legal code, but it is also sometimes generated without knowledge of the programmer (through reference variables, etc.). Thus, the assignment operator should always check for self assignment and immediately exit if it is the case.

These rules, as applied to the Vector assignment operator result in the following code segment to implement operator=.
singlespace419
Note that this function returns a reference to *this (a Vector&), so that the input and output of the operator is the same and the result of this operation can be used as input to another operator=. The function checks for assignment to itself in the first statement, and then assigns to all data members.

Output

An output function is a member of any concrete data type. In our mission to make every type to be similar to a built-in type, this is a necessary addition, as output is one of the more frequent operations that we utilize, especially in debugging. This routine is defined in the interface by

      friend ostream& operator<< ( ostream&, const Vector& ) ;
There are several reasons why this operator is not a member function, but a friend function. The reader should review the section on friends, if interested. The implementation of this function is straightforward and is given as follows:
singlespace427
co is a reference to an output stream. The input Vector object is passed by reference, so that this program is dealing with the actual object. The routine must return a reference to an output stream so that the output of this operator can be input to another output operator in order to string together output items (see also the similar reasons for chaining together assignment statements).
     cout << a << " = " << c << " units" ;

We note that the existence of this function is one requirement of a concrete data type that can be violated. The problem is that a class may be so large or so complex that it may not be viable to print it out. In this case, it is probably best to still write the operator<< and only to output the relevant parts of the class - although it should be mentioned that I have always found that I could use an output function, not matter how complex the class.

Also note that if an output function is written for each of the classes one designs, then writing the output function for each succeeding class becomes straightforward, as one has a library of output routines written for each of the components of the class to access.

The Comparison Operators

Relational operators are frequently required for all built-in data types and it is imperative that they be defined for any concrete data type. In general they are straightforward to implement, as they just compare two objects of the same class. Since they are binary functions, they are defined as friend functions in the interface
singlespace437

The implementation of the ``equals'' operator is given below.


singlespace439

and the implementation of the ``not equals'' operator is just the opposite.


singlespace441

Frequently, the programmer can utilize the equals operator to implement the not-equals operator by writing.
singlespace443
This is a common practice for a class with a large number of data items, where the extra call (to the equals operator) would be a very minor part of the execution of the operator.

Arithmetic Operations

Part of the vast popularity of C++ is its class structure and operator overloading features that allow the definition of mathematical operations for many classes. These operations, if done properly, can be intermingled with the standard arithmetic operations for the C/C++ built-in types and can have all the semantic properties of the desired mathematics.

The arithmetic operations usually fall into two types

  • binary operations, which are implemented as friend functions, and
  • combinations of arithmetic operations and assignment statements (+=, -=, etc.), which are implemented as member functions.

For the Vector class, we implement all the common mathematical operations for vectors, including scalar multiplication. Frequently, more than one function is defined in order to get commutativity of the operations.

Addition of Two Vectors

This routine adds two vectors. Its implementation is straightforward, as Vector addition is done componentwise.


singlespace453

We note that this routine returns a Vector and not a reference to a Vector. In order to sum the two vectors componentwise, it was necessary to create a local Vector variable vv. If we returned a reference to this variable we would be in a difficult situation, as the variable goes out of scope and is destructed when the operator+ routine ends. Thus, we must utilize the copy constructor to return a copy of vv, which forces us to return a Vector.

Subtraction of Two Vectors

This routine subtracts two vectors. Its implementation is straightforward, as Vector subtraction is done componentwise.


singlespace464

We note that this routine returns a Vector and not a reference to a Vector. In order to subtract the two vectors componentwise, it is necessary to create a local Vector variable vv. If a reference to this variable was returned, we would be in a difficult situation, as the variable goes out of scope and is destructed when the operator+ routine ends. Thus, the copy constructor must be utilized to return a copy of vv, which forces us to return a Vector.

Negation of a Vector

This routine negates a vector. Its implementation is straightforward, as Vector negation is done componentwise.


singlespace475

We note that this routine returns a Vector and not a reference to a Vector. In order to negate the Vector componentwise, it is necessary to create a local Vector variable vv. If a reference to this variable was returned, we would be in a difficult situation, as the variable goes out of scope and is destructed when the operator+ routine ends. Thus, the copy constructor must be utilized to return a copy of vv, which forces us to return a Vector.

Scalar Multiplication

This routine implements scalar multiplication of a vector. Its implementation is straightforward, as scalar multiplication is done componentwise. However, we must define two functions, one for multiplication on the left, and one for multiplication on the right.

The implementation of left scalar multiplication is given by
singlespace486
Right scalar Multiplication is nearly identical, but the parameters are in a different order.
singlespace488

We note that these routines return a Vector and not a reference to a Vector. In order to perform scalar multiplication componentwise, it is necessary to create a local Vector variable vv. If a reference to this variable was returned we would be in a difficult situation, as the variable goes out of scope and is destructed when the operator+ routine ends. Thus, the copy constructor must be utilized to return a copy of vv, which forces us to return a Vector.

Scalar Divide

This routine performs division of a Vector by a scalar. Its implementation is straightforward with componentwise operations.


singlespace498

We only allow the operations Vector-divided-by-double in this class by defining the one function above. The operation double-divided-by-Vector, which does not make mathematical sense, is not allowed.

We note that this routine returns a Vector and not a reference to a Vector. In order to divide the Vector componentwise, it was necessary to create a local Vector variable vv. If a reference to this variable was returned, we would be in a difficult situation as the variable goes out of scope and is destructed when the operator+ routine ends. Thus, the copy constructor must be utilized to return a copy of vv, which forces us to return a Vector.

Vector Addition with Assignment

This is a compound assignment operator which adds a Vector to an existing one. The addition is componentwise, so the implementation is straightforward.


singlespace513

We note that this function implicitly contains an assignment and is therefore defined in the same way as the assignment statement.

Vector Subtraction with Assignment

This is a compound assignment operator which subtracts a Vector from an existing one. The subtraction is componentwise, so the implementation is straightforward.


singlespace518

We note that this function implicitly contains an assignment and is therefore defined in the same way as the assignment statement.

Scalar Multiplication with Assignment

This is a compound assignment operator which multiplies a Vector by a scalar. The multiplication is componentwise, so the implementation is straightforward.


singlespace523

We note that this function implicitly contains an assignment and is therefore defined in the same way as the assignment statement.

Scalar Divide with Assignment

This is a compound assignment operator which divides a Vector by a scalar. The division is componentwise, so the implementation is straightforward.


singlespace528

We note that this function implicitly contains an assignment and is therefore defined in the same way as the assignment statement.

Other Member Functions

The expected operation of a concrete data type of vectors includes several operators that are common to vectors. In particular, dot products and cross products are frequently utilized with vectors, along with operations to determine the length or magnitude of a vector, and operations that normalize a vector.

Dot Product

The inner product, or dot product, is fundamental to the operation of a Vector class. The operator is a binary operator, thus being implemented as a friend function, and returns a scalar value. Its implementation is given below:


singlespace544

Cross Product

The cross product, or vector product, takes two Vectors as input, and returns a new Vector. The implementation is given below, and follows the definition of cross product from any of a number of engineering or mathematics texts:


singlespace548

We note that this routine returns a Vector and not a reference to a Vector. In order to calculate the cross product, it is necessary to create a local Vector variable vv. If a reference to this variable was returned, we would be in a difficult situation as the variable goes out of scope and is destructed when the cross product routine ends. Thus, the copy constructor must be utilized to return a copy of vv, which forces us to return a Vector.

Normalizing a Vector

This routine makes a Vector into a unit Vector - a vector that has length one. The routine is straightforward to implement as it simply divides each component by the length of the Vector.


singlespace559

We note that the routine utilizes the member function length. The routine only operates on the current instance of the class and therefore returns void.

Length of a Vector

This routine calculates the length (or magnitude) of a Vector.
singlespace565

We note that the routine is declared const which implies that the routine will not modify any of the data members of the class.

Coordinate Access Functions

These functions, commonly called access functions, are utilized to give the applications programmer access to the individual x, y, and z components of the class. They are defined in the interface (not in the implementation) so that they will be compiled as inline functions.


singlespace572

Note that the routines are all declared const which implies that the routine will not modify any of the data members of the class. This also insures that the compiler will log an error if any of these are used on the left-hand side of an assignment statement, or passed to a routine through a non-const parameter.

Global Constants

Most classes have a set of useful constants that can be defined and the vector class is no exception. In this class we have defined four constants that are frequently utilized in practice: The zero vector <0,0,0>, and the unit vectors in the three principal directions, <1,0,0>, <0,1,0> and <0,0,1> respectively. These constants are normally defined in the interface, so that they can be utilized by any program that uses the Vector class.


singlespace578

Inline Functions

Inlining is theoretically a great idea: Inlined functions act and look like functions, but avoid the actual function call; they act like macros to the C programmer, but are not compiler directives; and, they help out the optimizers by eliminating function calls.

Unfortunately, inlining is much more complicated than it seems. Meyers in his Rule 33 outlines many problems of inlining - some of which actually create more code and longer running programs that those without inlining.

However, the most important negative impact of inline code is that most debuggers cannot cope with inlining (How do you set a breakpoint at a function that isn't there). This single fact has lead Meyers to a strategy which determines which functions should be inlined and which should not :

Don't inline anything, or at least limit your inlining to those functions that are truly trivial.

In general, inlines should be used judiciously and I recommend that only the most elementary functions (like the access functions in the Vector class) should be inlined. After the code executes and is debugged, you can determine where the bottlenecks of the execution exist and inline certain crucial routines.

Const

Const is an extremely useful semantic addition to C++ which is highly underutilized by programmers. const allows the programmer to specify a semantic constraint on your source code - that is, you can state to the compiler that an object should not be modified - and an error will be produced if, in the compiler's view, you are attempting to modify the object. This is a very powerful feature, and should be used by the programmer as much as possible as it results in more compile time errors and fewer run time errors (i.e. less debugging).

const can be utilized in a variety of places, but is primarily utilized in three locations: in data declarations, where the programmer can say ``I don't want this data to be changed''; in parameter lists, where he can say ``I don't want this parameter to be changed''; or with member functions, where she can say ``I don't want this function to change the class data in any way''.

Using Const with Data Declarations const can be utilized to specify when data is to remain constant.

      const pi = 3.141592 ;
In this case, the compiler will not allow the variable pi to be placed in a situation where it might be changed. In other words, it cannot be placed on the left-hand side of an assignment statement, nor can it be passed as a parameter to a function unless that parameter is also declared const.

Pointers can also be declared constant and this has implications on the data that the pointer refers to. The statement

      const char *p ;
implies that the data that p points to is const, the statement
      char * const p ;
implies that the pointer is constant but the data can be changed, and the statement
      const char * const p ;
implies that neither the pointer nor the data can be changed.

Using Const with Parameters const parameters act just like local const objects. The interface declaration statement

      Vector dot ( const Vector&, const Vector& ) ;
states that the parameter is a reference to an actual data item, and this item is not allowed to change. In general, this is the normal method of passing parameters, as we do not want side effects as a result of our functions. But there are cases where the compiler will declare variables const, and we must take them into consideration. For example, frequently we wish to substitute an expression for a parameter
      a = dot ( v1 + v2, v3 ) ;
In this case, the result of the expression v1 + v2 is placed into a temporary object by the compiler which cannot be changed and utilized by the programmer - it is an internal variable. In this case, the compiler will declare this temporary variable const. In other words, if the dot function were defined by
      Vector dot ( Vector&, Vector& ) ;
a compiler error would be generated by the above statement.

In general, all parameters should be passed by reference and as const unless explicit side effects are desired by the programmer.

Using Const with Member Functions When a member function is const it implies that the function or operator is not allowed to modify any of the data members of the class. The statement

      double length() const ;
in the Vector class, implies that the function that determines the length of a Vector should not change the contents of the Vector class data members.

If a member function is declared const, the compiler will not allow the function to be utilized in any situation where the data members of the class may be changed.

Member Functions for the Const and Non-Const Objects Frequently, it is useful to have nearly identical member functions that can be used on both const and non-const objects. This is most frequently done on operator[], as in the member function for a possible string class
singlespace626

which extracts a single character from the string. Utilizing these functions we can have two different things happen to the const and non-const data items of a class. The first function will be utilized by the compiler for the non-const objects (e.g. putting s[i] on the left-hand side of an assignment statement) and the second statement will be used for the const objects.

Friends and Friend Functions

  A friend is allowed access the private data of a class. The friend modifier can be applied to both functions and classes. Both uses are for similar purposes: Functions are frequently defined as friends of a class so that they can access and change the data in the private area of a class; Classes are declared friends of another class so that they can access and change the private data of the other class.

The two primary uses of the friend modifier are discussed below.

Friend Classes When class A is made a friend of class B, it implies that the member functions of class A have access to the private data of class B.

      class B {
         friend class A ;
         .
         .
         } ;

This feature is only occasionally useful, and should be utilized on a very limited basis. Its general use is discouraged for it puts the private data items of class B in the public interface of a second class, violating the principles of information hiding and encapsulation. It is much better to develop access functions to interrogate the data of class B and member functions of B that allow the functions of A to modify the data of B. Also, if class A is so intrinsically linked to class B that it must access its member functions, then the designer should consider the possibility of deriving class A from B.

Its primary use is in the construction of linked lists, where the pointers are kept with the data.

      class DataItem {
            friend class DataItem ;
            double        SomeData
            DataItem        *next_item ;
            .
            .
            } ;

      class DataList {
            .
            .
            } ;
In this case, the DataList class is the one that manages the next_item pointer in the DataItem class.

This is a technique that is frequently utilized by novice programmers when they want one class be able to change the data of a second class - without going through the process to produce a well-designed class with proper member functions and data access functions. The pitfall that they usually encounter is that only the member functions of class A can access the private data of B, not the friend functions. To find a workaround to this, they frequently opt for the easy out which is to put the private data of B into the public interface - which violates the principles of encapsulation and information hiding.

Making one class a friend of another is a bad practice in general and should be avoided.

Friend Functions The writer of C++ programs always has a problem as to whether to make a function a member function or a friend function. In general, this appears to be a complex question, but actually has a straightforward answer.

First, any function that is to be made virtual must be a class member. Second, Functions are made friend functions when the function must have access to the private data of a class, and when either

  • A specific order is necessary for execution of the command for which the member function definition is inappropriate, or
  • Implicit type conversion may be desirable for complete execution of the command

The reasons for these conditions are examined below. However, so as not to have to evaluate each function on a case-by-case basis, we state the following rule : All binary and multiple parameter operators should be defined as friend functions instead of member functions. This is not absolute, nor does it guarantee that a function must be declared a friend to function properly, but it works in virtually all cases. Those exceptional cases must be examined on an individual basis.

Example 1 - operator<< - order independent In the case of operator<<, the routine is defined in the interface by

      friend ostream& operator<< ( ostream&, const Vector& ) ;
If it were defined as a member function it would have been defined as
      ostream& operator<< ( ostream& ) ;
which would imply that it is called in the following way by the calling routine
singlespace667
which is equivalent to
singlespace669

which is backwards, as to the way that the << operator is used. We could define it as a global function outside of the class interface, but in order to print the contents of the class it needs access to the private data. Thus, it must be defined as a friend function.

      friend ostream& operator<< ( ostream&, const Vector& ) ;

Binary Operators In the case of binary operators like ==, !=, +, -, or dot (the last two are for Vectors only), the problem is in the first parameter. If we take operator* (scalar multiplication for Vectors) as an example, and define it as a member function, it would appear as
singlespace682

This works very well for expressions of the form
singlespace684
But this does not work for expressions of the form

      v1 = c * v2 ;    // or v1 = c.operator* ( v2 ) ;
as the double type does not have an operator* that takes Vectors as parameters.

It is necessary to have both examples above compile and execute, as we want scalar multiplication to be commutative. The definition of two scalar multiplication functions as friends solves the problem
singlespace688
Each of the above cases will now compile and execute correctly.

Type Conversion An example of the conversion problem is best illustrated with strings.

Consider a string class String, and the concatenation operator+ that concatenates two Strings into one. This operator could be defined as a member function by

      String operator+ ( const String& ) ;
and could be called by
singlespace694
It is clear that this works in either order - but we are not yet off the hook because in most string classes there is a constructor
      String ( const char * ) ;
that converts character strings to Strings. In this case, the C++ compiler will perform type conversion automatically and the concatenation can take the form
singlespace697

The second form cannot execute, as a member function is only invoked if the left operand is an explicit class object.

Thus, if we desire automatically type conversion (and we usually do because it is a powerful feature), then defining this concatenation as a member function does not work in all cases. However, defining the concatenation operator as a friend function

      friend String operator+ ( const String&, const String& ) ;
will work in all cases, as type conversion can be done to each of the parameters of such a function.

Thus, the general rule : Define all binary operators as friend functions. It will work.

Compiler Directives

The commands preceeded by a ``#'' in the interface are compiler directives. The three directives #ifndef, #define and #endif combine, in this case, to insure that the include file is read by the compiler only once. The directives each perform a specific function

  • #define is utilized to set a compiler variable. In this case, it only defines the variable VECTOR_H, which will indicate to the compiler that the Vector.h include file has been processed.
  • #ifndef will include the code segment between it and the next #endif, only if the referenced variable is not defined. In this case, we note that the variable is only defined immediately after the #define directive. The effect of this is that the code segment is included only once, because the variable will be defined when future #ifndefs are encountered.
  • #endif is used to indicate the end of the code to be included.

These statements, placed around the include file, in the following way
singlespace715
cause the include file to be processed only once by the compiler. You, of course, must be sure that the defined name is unique. If it is not unique, then either this include file or another may not be read in correctly (clearly only the first file that the compiler accesses will be processed).

This is the standard method of insuring a compiler processes a file only once. The interested reader should look in any of the include files in /usr/include or /usr/include/CC and note that all of these files handle this operation in the same way.

This document maintained by Ken Joy.
Comments to the author : joy@cs.ucdavis.edu

All contents copyright (c) 1998
Computer Science Department, University of California, Davis
All rights reserved.



Ken Joy
Wed Jan 7 14:50:03 PST 1998