Class Struggle

Class Struggle

By Mike Toms

Overload, 1(2):, June 1993


For this instalment of Class Struggle, we are going to put ourselves in the position of a 'C' programming team about to take the first tentative step into C++. Our business is foreign trading, and over the years we have developed a number of libraries to cope with the handling, conversion and general manipulation of foreign currencies between various programs that our company has developed.

Due to our inexperience with C++, having only just returned from the training course, we are anxious to convert all our programs to embrace the OO paradigm. Our managers, being older and wiser, curb our enthusiasm, and limit us to producing a single currency class that should serve the business for the next 10 years. We are constrained further by the nature of our equipment, which varies from PCs and Macs through UNIX boxes and Mainframes, and must therefore confine our class to standard C++ and not to utilise any features specific to our PC compiler.

Where do we start?

The first and most obvious step is to draw up the requirements for our class. This will involve examining how currency is used in all our systems, and deciding whether to build a class flexible enough to cater for all eventualities, or to build a suitable base class, and have custom derivations from the general base class.

After looking at the current programs, we realise that all the currency usage has been catered for in our currency handling suite, and so decide to go for one class which will cater for all. Should anybody want anything extra-special, they are still able to derive from it.

The Requirements

Our requirement of the class can be simply put as "A class that can represent an international currency, to which we can perform as many of the operators that mean anything and to have sufficient functions to allow the class to be universally used in a 'C-like' or C++ environment throughout our business. The class must also be able to display itself in an output stream in the correct manner". This is a bit woolly, but will do for our purposes.

Data held by the class

  • The currency type, Pounds, Yen etc. This will also double as an indicator to show that the class is in an invalid state.
  • The number of units of the currency, for simplicity sake, we will represent this as a double number, though a BCD representation would be more desirable.

The Member Functions required

Constructors
- Default (£0.00)
- Value only (£value) default to pounds
- Currency Only (?0.00) default to zero whatever currency selected
- Complete (?value) no defaulting - set to value/currency supplied
- Copy Constructor
Destructor

- Not needed but supplied anyway as matter of course
Set value functions

- accept complete value/currency, (others could present loopholes)
- accept complete currency/value,
- another currency value
Convert_to
- Changes a currency of object to another currency and preserves it value on the currency market (i.e. converts $3.00 to 20.00 French Francs at exchange rates of $1.50 to the Pound Sterling and 10.00 French Francs to the Pound Sterling).
Percent

- returns the required percentage of a value in its current currency (i.e. state_tax = total_price.percent(6.00); will return $3.00 if the total_price is $50.00 and 2.52DM if total_price is 42.00DM.
The operators

This is a list of the operators that will be overloaded, only those for which a use can be found will be used.
! used to test if currency is valid (usually after screen input etc.)
+ add two currencies (binary) or as unary plus
- Subtract two currencies or unary minus
* multiply currency by constant or constant by currency
/ divide currency by constant giving currency - note should it behave differently if the divisor is an integer type? i.e. give integer quotient.
% divide currency by constant giving remainder in currency
= Assign another currency value, but to KEEP our original currency
() Copy another currency
+= Adds another currency to itself, preserving its currency type also adds a constant to itself
-= Subtracts another currency from itself, preserving its currency type also subtracts a constant from itself
*= multiply currency by a constant
/= divide a currency (see division for argument)
%= receive remainder of division
<< Output to a stream
>> Input from a stream
= test two currencies for equality
!= test two currencies for inequality
<= test two currencies for less than or equal to
>= test two currencies for greater than or equal to
< test two currencies for less than
> test two currencies for greater than
++ postfix and prefix required; change by one unit of currency
-- postfix and prefix required; change by one unit of currency

Other considerations

It has been realised by the team that a class to represent the currency types is required. This will need to hold such information as the Currency symbol, whether the symbol is displayed prefix or postfix, the current exchange rate and the number of decimal places to which the currency is normally displayed. This information should be file-based as it could potentially be updated daily or even minute-by-minute, depending on the nature of the system.

Note: Most code extracts have been simplified, the complete listings are provided on the companion disk. In order to keep things simple, the constness of any of the parameters or member functions has been ignored, as have a number of efficiency considerations (such as explicit inlines).

For simplicity sake the currency types are to be implemented as an array, and use enumerated constants to act as index values of currency type represented by the array, i.e.

enum currency_names
{ pounds = 0, dollars, yen, francs, lira };

The public interface for currency type is declared as follows:

class currency_type
{
protected:
... omitted - no need to know
public:
currencytype(void); // default constructor
~currency_type(void); // destructor
void set(char* symb, double r, int places,
position p = prefix);
currency_type& operator= (currency_type& ct);
char* get_symbol(void);
double get_rate(void);
int get_places(void);
position get_position(void);
};

None of the member functions are difficult to understand. The constructor has been written so that there are no parameters. This is because it is only intended to be used in an array, for which no parameters can be supplied. The set member function is used to store all the characteristics of the currency, and allocates a space long enough to hold the currency symbol. This then can be used for multi-character currency symbols (e.g. "AUS $" for Australian dollars). The destructor is used to free up the space allocated to the currency symbol when the currency type goes out of scope. The other member functions are used to access the internals of the currency type in a read-only manner.

Multiple Source Files

For those not familiar with multiple source file compilations, the #ifndef CURRTYPE_H statement at the top of the currtype.h file may seem a little strange. This is to prevent any multiple #includes (and hence multiple declarations) into any single compile unit. The first time this file is included into the compilation, the value CURRTYPE_H is not defined, the pre-processor then defines the value CURRTYPE_H. If any further attempts to include this file (perhaps as part of a different header file), the #ifndef statement will not be true and the pre-processor will jump to its corresponding #endif, which is at the end of the file.

The other section of pre-processor code that looks a little strange is:

#ifndef MAIN_CPP
extern
#endif
currency_type CurrType [
#ifdef MAIN_CPP
5
#endif
];

This is the same as writing

#ifdef MAIN_CPP 
currency_type CurrType [5];
#else
extern currency_type CurrType[];
#endif

It makes not a lot of difference how you choose to write it; the effect is the same. Just before this header file is included in the main program module, the value MAIN_CPP must be defined, thus making this a definition rather than an external declaration.

The Currency Class

The currency class header and source files can be found on the companion disk (as currency.h and currency.cpp respectively). Because of the repetitive nature of many of the overloaded operators, only the salient points are described here.

In order to save writing the same code twice, a private member function (setup) has been used to insert values into the currency, this is called both from constructors and the set function. Any attempt to give the currency a value other than pounds, dollars, yen, francs or lira will cause this member function to set the currency to an invalid value.

Declaration:

void setup(currency_names, double);
void setup(char*, double);

Definition:

void currency::setup(currency_names c, double d)
{
amount = d;
if (c < min_currency_val ||
c > max_currency_val)
currencyName = invalid_currency;
else
currencyName = c;
}

void currency::setup(char* c, double d)
{
amount = d;
currencyName = test_currency_symbol(c);
}

The definition and declaration have been written in different files so that the currency header can be sourced into the files that will use the currency class. The definitions will only need to be compiled once. These are then linked into the final .EXE file. This avoids the need of all statements being recompiled for any change in the program, as happens with a single source file. At least now, the only time the currency class will be recompiled is if changes are made to currency.cpp, currency.h or any header file used by this class. The declaration takes place in the header file.

The definition is in the .CPP file, and requires that the class to which the member function belongs is specified. This is done with the scope resolution operator (::) and a function header of this kind would take the simplified form of:

<return-type> <class>::<member>(<param>) {}

Inline Vs Outline

In the last Class Struggle, all the functions were declared and defined at the same time. These functions

are then called implicit in-line functions. The member functions declared "out-of-line" can be made in-line (explicit in-line) by prefixing both declaration and definition with the 'inline' keyword. Thus the previous two functions could have been written

Declaration:

inline void setup(currency_names, double);
inline void setup(char*, double);

Definition:

inline void currency::setup(currency_names c, double d)
{
amount = d;
if (c < min_currency_val ||
c > max_currency_val)
currencyName = invalid_currency;
else
currencyName = c;
}
inline void currency::setup(char* c, double d)
{
amount = d;
currencyName = test_currency_symbol(c);
}

The effect of in-lining the code is to avoid the overhead of a function call. This is fine for small, often-called functions, but if too many of the larger functions are inlined, it can result in large increases in the size of the executable. An inline function behaves like a real function with respect to scope of variables, arguments passed and local variables, but in fact could make copies of the function at every point in your program that the inline function is used. Why do I say could? According to Bjarne, the inline specifier, like the register specifier, is a "hint to the compiler" rather than a dictate. The compiler will decide if it can inline the function, but loops, other inline function calls and the size of the function can all affect whether the function will be inline or outline.

The test_currency_symbol() member function will match a currency symbol supplied with one on the internal currency type array. If a match is found the function returns the enumerated currency value, otherwise the invalid currency value is returned.

Declaration:

currency_names test_currency_symbol(char*); 

Definition:

currency_names currency::test_currency_symbol
(char* c)
{
currency_names result;
for (int i = min_currency_val;
i <= max_currency_val; i++)
{
if(strcmp(CurrType[i].get_symbol(),c)==0)
{
result = (currency_names)i;
return result;
}
}
return invalid_currency;
}

Only one other protected member function has been supplied, and that is a member to convert an amount from one currency to another. This would normally have to be done in so many places, that making it a member function should provide some safety against the conversion being copied wrongly.

Declaration:

double convert(currency_names, currency_names, double);

Definition:

double currency::convert(currency_names from, currency_names to,
double value)
{
return (from!=invalid_currency &&
to!=invalid_currency ?
(value/CurrType[from].get_rate() *
CurrType[to].get_rate())
:0.0);
}

We are now at the point where we can start generating all the member functions and overloaded operators that we need to make our class work. The functions I find most useful to define first are the constructors and destructor. In the case of all the constructors, a call to the protected member function which is overloaded to handle both the currency index/amount and currency symbol/amount parameters.

Declaration:

currency(void);
currency(currency_names, double = 0.0);
currency(char*, double = 0.0);
currency(double, currency_names = default_currency);
currency(currency&);

Definition:

currency::currency(void)
{
setup(invalid_currency,0.0);
}

currency::currency(currency_names c, double d)
{
setup(c,d);
}

currency::currency(char* c, double d)
{
setup(c,d);
}

currency::currency(double d, currency_names c)
{
setup(c,d);
}

currency::currency(currency& c)
{
setup(c.currencyName,c.amount);
}

The only constructor that may seem slightly strange is the last one. This is known as the copy constructor and must be defined with a reference to an object and not as a value of the object. Earlier compilers will normally provide a default copy constructor, which will perform a bitwise copy whilst current ones memberwise copy from one object to another. When the custom copy constructor is provided, the compiler will not generate a default. The copy constructor must have the form X::X(X&) or X::X(X&,int=0) etc. The form X::X(X) is illegal as a recursive loop will be set up where in order to make a copy of X in order to pass it by value to the copy constructor, the copy constructor would have to be used, and so on! This format although illegal in the forthcoming ANSI C++ standard is permitted by some older versions of Turbo C++.

In many cases the default copy constructor supplied by the compiler will be adequate. It is essential to remember though that copying a pointer to somewhere in free store will mean that the area is pointed to by two pointers. This allows one to delete the memory and the other to continue to use it, or so that both are modifying the same piece of memory. In this case the custom copy constructor would be responsible for allocating its own memory and copying the contents of the other object's memory to its own allocated memory area.

Declaration:

virtual ~currency(void); 

Definition:

virtual currency::~currency(void) 
{
}

The destructor does nothing, but will ensure that the destructor of anything derived from currency will have its destructor called. e.g.

class upgraded_currency : public currency
{
public:
...
~upgraded_currency() { cout << "Bye!";}
};
void main(void)
{
currency* ptr = new upgraded_currency;
delete ptr;
}

If the destructor is not virtual in the base class, the destructor will not be called for classes derived from it when destroyed via a pointer to the base class.

The next group of member functions are the overloaded set() functions. These are numerous as the team would like the order of parameters to be immaterial; the class is then able to handle ("£",40) as well as (40,"£").

Declaration:

currency& set(currency_names, double);
currency& set(char*, double);
currency& set(double, currency_names);
currency& set(double, char*);
currency& set(currency&);

Definition:

currency& currency::set(currency_names c, double d)
{
setup(c,d);
return (*this);
}
currency& currency::set(char* c, double d)
{
setup(c,d);
return (*this);
}
currency& currency::set(double d, currency_names c)
{
setup(c,d);
return (*this);
}
currency& currency::set(double d, char* c)
{
setup(c,d);
return (*this);
}
currency& currency::set(currency& c)
{
setup(c.currencyName,c.amount);
return (*this);
}

These functions do not differ much from the constructor member functions, except that they return a reference to themselves. This is to enable them to be used as intermediates in calculations. Bjarne Stroustrup went to a lot of trouble when designing C++ to ensure that user-designed classes could behave in an identical manner to built-ins. If the members of your class do not return sensible values/references, they cannot be chained together in the traditional 'C' way. For example, I may want to use the result of the set in an if statement:

if (transaction->set(this_currency) > max_allowed)...

If the set() member was of a void return type, then this statement would require multiple statements to accomplish the same task.

transaction->set(this_currency); 
if (*transaction > max_allowed)...

This can be seen in more benign situations where your C++ classes would appear not to behave as if they were built-ins in the case of the equal sign. In the case of a C built-in, it will also return the result of the equation so that it may be used to chain to other assignments:

a = b = c = d; 
&
if((a=func())>val)
etc.

The next two overloaded member functions perform the task of changing the currency of the current object, but without changing its value (unless an attempt is made to change it to a currency that does not exist).

Declaration:

currency& convert_to(currency_names);
currency& convert_to(char*);

Definition:

currency& currency::convert_to(currency_names c)
{
if (c < min_currency_val || c > max_currency_val)
currencyName = invalid_currency;
else
{
amount = convert(currencyName, c, amount);
currencyName = c;
}
return currency(*this);
}

currency& currency::convert_to(char* c)
{
return convert_to(test_currency_symbol(c));
}

Again these member functions return a reference to themselves, so that they too can be used to chain correctly in statements.

The next member function returns a currency object as opposed to a reference to the current object. This is because this function does not modify the value of currency object and needs to build a temporary currency object to contain the result.

Declaration:

currency percent(double);

Definition:

currency currency::percent(double d)
{
return currency(currencyName,amount/100.0*d);
}

Because all the data members are protected, and we need some means of finding out what currency the object represents, it is better to provide simple member functions to return the desired values, rather than allow member data items to be public. Should you be tempted to use the latter technique "because it's easier", just remember that you have then lost control of one of your data members. Any torn, dick or harry will then be able to place incorrect values into this field at any time. This makes debugging a lot more difficult, and makes it easier for subtle bugs to exist.

Declaration:

currency_names get_currency(void); 

Definition:

currency_names currency::get_currency(void) 
{
return currencyName;
}

It is necessary to be able to test on the status of a currency before using it. In this scheme I have elected to borrow a technique used by the stream library. There are two functions (good() and bad()) and the ! and void* operators. It is obvious what the functions do, but the operators are used as follows:

if (!currency_val) // if illegal_currency
if (currency_val.bad()) // same as above
if (currency_val) // if currency_val is a valid currency
if (currency_val.good()) // same as above

The binary plus and minus operators both behave in a similar way to the percent member function in that they need to create a temporary object to return to the evaluation in progress.

Declaration:

currency operator+(currency&);
currency operator-(currency&);

Definition:

currency currency::operator+(currency& a)
{
return currency(currencyName, amount +
convert(a.currencyName, currencyName, a.amount));
}

currency currency::operator-(currency& s)
{
return currency(currencyName,amount -
convert(s.currencyName, currencyName, s.amount));
}

Unlike their binary counterparts, the unary plus and minus operators behave in different ways. The unary plus need only return a reference to itself, whilst the unary negative must create a temporary object of the same currency as itself, but with the amount negated. The unary plus and minus (as opposed to binary ones) are signified by a void parameter list.

Declaration:

currency& operator+(void);
currency operator-(void);

Definition:

currency& currency::operator+(void)
{
return (*this);
}
currency currency::operator-(void)
{
return currency(currencyName, -amount);
}

The multiply operator (to be complete) should be able to cope with 2 forms, the (number * currency) and (currency * number). The latter case can be written in a similar manner to the binary plus operator. The latter case, however, requires that we overload the normal multiply operator. We then need to give this operator access to the internals of the currency class. This is achieved by making the system multiply operator a friend of the class. In brief, friends are allowed access to your private and protected member data/functions in addition to the public ones. Careful consideration should be given before declaring friends, as they override the protection set up in the class and used badly can cause the class to malfunction.

Robert Murray, in his book C++ Strategies and Tactics, claims the following quote (no name attributed, but you'd recognise the name)
"A friend is someone who can touch your private parts" -he was of course referring to private members!

Notice the syntax of the definition of the system multiply operator, where both values to the left and right of the multiply sign need to be defined. Also, as this is a system operator being overloaded, it is not a member of the class and is not a 'currency::operator *' but a plain 'operator *'.

Declaration:

currency operator*(double);
friend currency operator*(double, currency&);

Definition:

currency currency::operator*(double m)
{
return currency(currencyName, m * amount);
}
currency operator*(double& m, currency& c)
{
return currency(c.currencyName, c.amount*m);
}

There is nothing special about the divide and modulus operators. Just remember that if you divide by an int, the result is an integer division and not a rounding of the division by an integer.

Declaration:

currency operator/(int);
currency operator/(double);
currency operator%(int);

Definition:

currency currency::operator/(int d)
{
return currency(currencyName, long(amount)/d);
}
currency currency::operator/(double d)
{
return currency(currencyName, amount/d);
}
currency currency::operator%(int d)
{
return currency(currencyName, fmod(amount,d));
}

The assignment (single equal) operator has been designated as 'take the value of the assignment, but retain our current currency'. This has the same effect as a copy and then conver_to(). The test 'if (&c != this)' is to ensure that in the case where an assignment 'curr1 = curr1' occurs, no work need be done. In the case of this program, no harm will be done by omitting the test, but when dynamic memory allocation is involved, it is possible to delete the data you are about to assign from. This is a bit involved for Class Struggle, so I'll deal with it in another edition of Overload.

Declaration:

currency& operator=(currency&);

Definition:

currency& currency::operator=(currency& c)
{
if (&c != this)
{
if (currencyName == invalid_currency)
set(c.currencyName, c.amount);
else
amount = convert(c.currencyName,
currencyName, c.amount);
}
return *this;
}

The operator() has been overloaded such that it will produce an exact copy of the currency parameter. It is in effect another assignment. In some ways this is almost breaking one of the 'golden rules' where one should not produce any surprises when overloading operators. I envisage the overload of the operator () as acting in the same way as a copy constructor. It removes my guilt at possibly producing a surprise overload.

Declaration:

currency& operator()(currency&);

Definition:

currency& currency::operator()(currency& c)
{
if (&c != this)
set(c);
return *this;
}

Other operators such as +=, -=, *=, /= and %= all behave in the same manner as their individual signs, but perform value assignment in the same manner as the equal sign. The code for these operators can be found on the companion disk.

The next class of operators that we need to look at are the increment and decrement operators. These are unary operators, in the same manner as unary + and unary-, but they can be either postfix or prefix. The compiler will treat a void parameter list as a prefixed operator and a single int as a postfix operator. The mechanics of both prefix and postfix would be expected to be the same, but we must remember that all our types return either values or references, so that they may be chained. In order to achieve this, the prefix (done immediately) can return a reference to itself. The postfix operator, however, must return an object representing itself before the operation takes place. Fortunately this is easily achieved as can be seen in the following section of code:

Declaration:

currency& operator++(void);
currency operator++(int);
currency& operator--(void);
currency operator--(int);

Definition:

currency& currency::operator++(void)
{
amount++;
return *this;
}
currency currency::operator++(int)
{
return currency(amount++,currencyName);
}
currency& currency::operator--(void)
{
amount--;
return *this;
}
currency currency::operator--(int)
{
return currency(amount--, currencyName);
}

For our class to be complete we need to be able to compare currency objects. As I am essentially lazy, I have used the pre-processor to generate these operators, as they are all so similar.

In order for the comparison to work, both amounts must be in the same currency. It is also necessary in the test the equality or inequality checks for one or both currencies being invalid must be catered for.

#define TEST(X) (amount X \ 
convert(c.currencyName,currencyName,c.amount) \
?TRUE:FALSE)

Declaration:

bool operator==(currency&);
bool operator!=(currency&);
bool operator<=(currency&);
bool operator>=(currency&);
bool operator<(currency&);
bool operator>(currency&);

Definition:

bool currency::operator==(currency& c)
{
if (currencyName == invalid_currency &&
c.currencyName == invalid_currency)
return TRUE;
if (currencyName == invalid_currency ||
c.currencyName == invalid_currency)
return FALSE;
return TEST(==);
}

bool currency::operator!=(currency& c)
{
if (currencyName == invalid_currency &&
c.currencyName == invalid_currency)
return FALSE;
if (currencyName == invalid_currency ||
c.currencyName == invalid_currency)
return TRUE;
return TEST(!=);
}

The final two operators we are overloading are the ostream inserter and the istream extractor. These will the give users of our class the ability to input and output currencies in a consistent and predefined manner. Both of these operators must be set up as friends, as they are overloading the << and >> operators of ostream and istream respectively. Taking the output stream first, the format of the currency is as follows:

<Invalid Currency>

or

<-><prefixSymbol><amountInFixedDecimalPlaces>

or

<-><amountInFixedDecimalPlaces><postfixSymbol>

Declaration:

friend ostream& operator<<(ostream&, currency&);

Definition:

ostream& operator<<(ostream& os, currency& c)
{
if (c.currencyName==invalid_currency)
os << "<Invalid Currency>";
else
{
if (c.amount < 0.0)
os << '-';
if (CurrType[c.currencyName].getpositionQ ==
prefix)
  os << CurrType[c.currencyName].get_symbol();
if (CurrType[c.currencyName].getplacesQ > 0)
{
os << setiosflags(ios::fixed)
<< setiosflags(ios::showpoint)
<< setprecision(CurrType
[c.currencyName].get_places())
<< fabs(c.amount);
}
else
{
os << long(fabs(c.amount));
}
if (CurrType[c.currencyName].get_position() ==
postfix)
os << CurrType[c.currencyName].get_symbol();
}
return os;
}

The input stream is designed to take a single word (in the space-separated sense and not just two bytes) and translate that into a currency if possible. If there is a problem with the translation, the currency is then set as invalid and can be tested with the ! operator or bad() member function. The input stream can work from the keyboard, a text file or a memory stream.

The main part of the analysis is performed by this analyse currency member function. It will load either the translated value of the currency or, in the case of a bad currency, set the appropriate state.

Declaration:

void analyse_currency (char*);

Definition:

void currency::analyse_currency(char* b)
{
char* p = b;
char* q;
double value = 0.0;
int sign = 1;
// is first character + or - sign
if (*p =='-')
{
sign = -l;
p++;
}
else if (*p =='+')
p++;
// there can now follow either a number or a start
// of a character string
q = p;
if(isdigit(*p))
{
value = atof(p) * sign;
// advance to end of number
q += strspn(p,"0123456789.");
set(q, value);
// if the next statement is commented out,
// the input will *NOT* check which side the
// symbol is entered i.e. $7.95 and 7.95$ will then
// both be acceptable and equivalent
if (good() && CurrType[currencyName].
get_position() != postfix)
set(invalid_currency,0);
  } 
else
{
// skip string
p = strpbrk(q,"0123456789.");
// extract number
value = atof(p) * sign;
// set up string
*p = '\0';
set(q, value);
// If commented out will *N0T* check which side
// the symbol is entered
if (good() && CurrType[currencyName].
get_position() != prefix)
set(invalid_currency,0);
}
}

Declaration:

friend istream& operator>>(istream&, currency&);

Definition:

istream& operator>>(istream& is, currency& c)
{
char buffer[80];
is >> buffer;
c.analysecurrency(buffer);
return is;
}

That's it for now, I think I've overdone this article a bit. Sorry it got a bit boring, but there was a lot of basics to cover. I'm not sure I covered them all in sufficient detail. I'll probably go through some of the points mentioned in future articles but in the meantime you know where to write if you have problems!






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.