Monday, December 21, 2009

Mixed Types as Parameters




I l@ve RuBoard


Mixed Types as Parameters


Classes Complex and Rational are good examples of programmer-defined types that emulate the properties of built-in C++ numeric types. The objects of these types can be operated on by using the same set of operators as that used for an ordinary numeric variable. This supports the C++ goal of treating programmer-defined types in the same way as C++ built-in types are.



This analogy, however, is not complete. You can apply a number of operators to variables of numeric types that you cannot apply to Complex or Rational objects. Examples of such operators are modulo division, bitwise logical operators, and shifts. Of course, you can overload these operators similar to arithmetic and comparison operators, but the meaning of these operators (whatever meaning you decide to implement, the compiler will go along) will not be intuitively clear to the client programmer and to maintainer. An example of such arbitrary assignment of meaning is Complex::operator+() that I implemented to display the values of the data members of the Complex object. Intuitively, it is not clear at all what the expression +x should do to the Complex variable x.



Another problem with treating the objects of built-in types and programmer-defined types equally is the problem of implicit type conversions. C++ supports type conversions without reservation. For example, these expressions are syntactically and semantically correct for any numeric types.





c += b; // ok for b and c of any built-in numeric types
c += 4; // ok for c of any built-in numeric type



Whatever the numeric type of variable b, it is implicitly converted to the numeric type of variable c; whatever the numeric type of variable c, integer 4 is implicitly converted to that type. If variables b and c in the code snippet above are of type Rational, the second line above is in error. For this line to be syntactically correct, one of these functions should be available in the scope of the client code.





void Rational::operator+=(int x); // c+=4; is c.operator+=(4);
void operator+=(Rational &r, int x); // c+=4; is operator+=(c,4);



None of these functions is implemented in Listing 10.5, and that results in the syntax error in the client code. This is an example of the member function that eliminates the error.





void Rational::operator += (int x) // target object changes
{ nmr = nmr + x * dnm; // n1/d1 + n = (n1+n*d1)/d1
this->normalize(); }



Notice that if both functions are available, a member function and a global function with these interfaces, the second line in the code snippet above is still in error. This time around it is an ambiguity of the function call. Since either of these functions can foot the bill (the bill here is the interpretation of the statement c += 4;) the compiler does not know which function to call.



However, if variables b and c in the code snippet above are of type Complex, both lines in the snippet are syntactically correct. Why? Because I implemented two versions of operator+=() in Listing 10.4.





void Complex::operator += (const Complex &b);
void Complex::operator += (int b);



In the client code, the first function is called for the first line of code, and the second function is called for the second line of code.





c += b; // c.Complex::operator+=(b); Complex argument
c += 4; // c.Complex::operator+=(4); integer argument



This resolves the problem of using operands of mixed types in expressions. The second overloaded operator function works not only for integer arguments, but also for characters, short integers, long integers, floating point, and double floating point arguments. According to the rules of argument conversion, a value of each of these built-in types can be converted to an integer. There is no need to overload the function operator +=() for each of these built-in types. One function would suffice.



But do not sigh with relief yet. What about other operators such as -=, *=, /=? Each of these operators requires yet another overloaded operator function with a numeric parameter. And what about other arithmetic operator functions such as operator+(), operator-(), operator*(), and operator/()? Consider the following snippet for object instances of class Rational.





c = a + b; // c = a.operator+(b);
c = a + 5; // ?? incompatible types ??



Again, the second line results in a syntax error because the overloaded operator expects an object of the type Rational as the actual argument, not a value of a built-in numeric type. Meanwhile, all of these expressions are not a product of inflamed imagination. Numeric values are mixed with complex numbers and rational numbers in algorithms. And what about comparisons? You should be able to compare Rational objects with integers, and this poses yet additional problems.



The solution that I used so far is legitimate but boring. For each operator function with a Rational (or any other class) object as an argument, I have to write yet another operator function with a long integer value as an argument. (An integer might not be sufficient on 16-bit machines.)



Can anything be done about that? And here C++ offers you a beautiful tool that allows you to get away with only one set of operator functions (with class parameter) and force these operator functions to accept actual arguments of built-in numeric types.



What is this tool? It is a tool that allows you to cast a numeric value to a value of the class. Let us start with a very simple example.





Rational c = 5; // incompatible types ??



It goes without saying that this line is in error. In Chapter 3, "Working with C++ Data and Expressions,"
I discussed the concept of the cast that converts the value of one built-in numeric type to the value of another built-in numeric type. Of course, these casts are available only between built-in types, not between built-in types and the programmer-defined type Rational. But if a cast between built-in types and type Rational existed, how would it look? Its syntax would be the same as for numeric types: the type name in parentheses. And the type name would be the name of the type to which the value is converted.





Rational c = (Rational)5; // this is how the cast should look like



If you remember, in Chapter 3 you saw two syntactic forms for the cast, one that comes from C (the form I used in the line above) and another is the C++ function-like cast.





Rational c = Rational(5); // this is how the cast could look like



Doesn't this line look like a constructor call? Now, what do you call the function that produces the value of the class type? Don't you call it a constructor? So this function looks like a constructor and behaves like a constructor. The conclusion is that it is a constructor.



Next questionę¢¬hat constructor? This is simple. In Chapter 9, we called a constructor with one parameter of nonclass type a conversion constructor. Now you should understand why this name is used. This constructor converts a value of its parameter type into the value of the class type. To make the line above syntactically correct, you have to write a constructor with one parameter.



What should this constructor do with its single parameter? If the value of the parameter is, say, 5, the value of the Rational object should be set to 5 or to 5/1. If the value of the parameter is 7, the value of the object should be set to 7/1. Hence, the value of the parameter should be used to initialize the numerator, and the denominator should be set to 1 for any value of the actual argument. This results in the following constructor.





Rational::Rational(long n) // conversion constructor
{ nmr = n; dnm = 1; } // initialize to a whole number



This constructor is called every time that a function that expects a Rational parameter is called with a numeric actual argument. The class Rational now should look this way.





class Rational {
long nmr, dnm; // private data
void normalize(); // private member function
public:
Rational() // default constructor: zero value 0/1
{ nmr = 0; dnm = 1; }
Rational(long n) // conversion constructor: whole value n/1
{ nmr = n; dnm = 1; }
Rational(long n, long d) // general constructor: fraction as n/d
{ nmr = n; dnm = d;
this->normalize(); }
Rational operator + (const Rational &x) const
{ return Rational(nmr*x.dnm + x.nmr*dnm, dnm*x.dnm); }
// THE REST OF CLASS Rational
} ;



Some programmers dislike writing several constructors if one constructor with default parameters can do the job. A popular constructor that can be used as general constructor, conversion constructor, and default constructor will look this way.





Rational(long n=0, long d=1) // general, conversion, default constructor
{ nmr = n; dnm = d;
this->normalize(); }



Make sure that you see that this constructor is called when the client code supplies two arguments for object initialization: one argument and no arguments; default values are used instead of missing arguments when defining Rational objects.





Rational a(1,4); // Rational a = Rational(1,4); - two arguments
Rational b(2); // Rational b = Rational(2,1); - one argument
Rational c; // Rational c = Rational(0,1); - no arguments



Notice that the actual arguments supplied in this example are of type int but the constructor expects arguments of type long. This is not a problemę¢šmplicit built-in conversion from int to long is available by default as it is available between all built-in numeric types. In function calls, the compiler allows not more than one built-in conversion and not more than on class-defined conversion (a conversion constructor call).



In compiling expressions with Rational operands, the compiler converts the int arguments first to long and then to Rational; after this conversion, the compiler generates the call to the appropriate operator.





c = a.operator+(Rational((long)5)); // real meaning of c = a + 5;



Now the client code above compiles without the operator Rational::operator+(long). A temporary Rational object is created, the conversion constructor is then called, then the operator+(), and then the Rational destructor.



Now you can write client code with numeric values as the second operand, while the first operand is of type Rational.





int main()
{ Rational a(1,4), b(3,2), c, d;
c = a + 5; // c = a.operator+(Rational((long)5));
d = b - 1; // d = b.operator-(Rational((long)1));
c = a * 7; // c = a.operator*(Rational((long)7));
d = b / 2; // d = b.operator/(Rational((long)2));
c += 3; // c.operator+=(Rational((long)3));
d *= 2; // d.operator*=(Rational((long)2));
if (b < 2) // if (b.operator<(Rational((long)2))
cout << "Everything works\n";
return 0; }




Listing 10.6 shows a new version of class Rational that supports mixed types in binary expressions. The output of the program run is shown in Figure 10-6.





Figure 10-6. Output for program in Listing 10.6.










Example 10.6. Class Rational that supports mixed types in expressions.


#include <iostream>
using namespace std;

class Rational {
long nmr, dnm; // private data
void normalize(); // private member function
public:
Rational(long n=0, long d=1) // general, conversion, default
{ nmr = n; dnm = d;
this->normalize(); }
Rational operator + (const Rational &x) const; // const target
Rational operator - (const Rational &x) const;
Rational operator * (const Rational &x) const;
Rational operator / (const Rational &x) const;
void operator += (const Rational &x); // target changes
void operator -= (const Rational &x);
void operator *= (const Rational &x);
void operator /= (const Rational &x);

bool operator == (const Rational &other) const; // const target
bool operator < (const Rational &other) const;
bool operator > (const Rational &other) const;
void show() const;
} ; // end of class specification

Rational Rational::operator + (const Rational &x) const
{ return Rational(nmr*x.dnm + x.nmr*dnm, dnm*x.dnm); }
Rational Rational::operator - (const Rational &x) const
{ return Rational(nmr*x.dnm - x.nmr*dnm, dnm*x.dnm); }

Rational Rational::operator * (const Rational &x) const
{ return Rational(nmr * x.nmr, dnm * x.dnm); }

Rational Rational::operator / (const Rational &x) const
{ return Rational(nmr * x.dnm, dnm * x.nmr); }

void Rational::operator += (const Rational &x)
{ nmr = nmr * x.dnm + x.nmr * dnm; // 3/8+3/2=(6+24)/16=15/8
dnm = dnm * x.dnm; // n1/d1+n2/d2 = (n1*d2+n2*d1)/(d1*d2)
this->normalize(); }

void Rational::operator -= (const Rational &x)
{ nmr = nmr * x.dnm - x.nmr * dnm; // 3/8+3/2=(6+24)/16=15/8
dnm = dnm * x.dnm; // n1/d1+n2/d2 = (n1*d2-n2*d1)/(d1*d2)
this->normalize(); }

void Rational::operator *= (const Rational &x)
{ nmr = nmr * x.nmr; dnm = dnm * x.dnm;
this->normalize(); }

void Rational::operator /= (const Rational &x)
{ nmr = nmr * x.dnm; dnm = dnm * x.nmr;
this->normalize(); }

bool Rational::operator == (const Rational &other) const
{ return (nmr * other.dnm == dnm * other.nmr); }

bool Rational::operator < (const Rational &other) const
{ return (nmr * other.dnm < dnm * other.nmr); }

bool Rational::operator > (const Rational &other) const
{ return (nmr * other.dnm > dnm * other.nmr); }

void Rational::show() const
{ cout << " " << nmr << "/" << dnm; }

void Rational::normalize() // private member function
{ if (nmr == 0) { dnm = 1; return; }
int sign = 1;
if (nmr < 0) { sign = -1; nmr = -nmr; }
if (dnm < 0) { sign = -sign; dnm = -dnm; }
long gcd = nmr, value = dnm; // greatest common divisor
while (value != gcd) { // stop when the GCD is found
if (gcd > value)
gcd = gcd - value; // subtract smaller number from greater
else value = value - gcd; }
nmr = sign * (nmr/gcd); dnm = dnm/gcd; } // denominator is positive

int main()
{ cout << endl << endl;
Rational a(1,4), b(3,2), c, d;
c = a + 5; // I'll discuss c = 5 + a; later
a.show(); cout << " + " << 5 << " ="; c.show(); cout << endl;
d = b - 1;
b.show(); cout << " - " << 1 << " ="; d.show(); cout << endl;
c = a * 7;
a.show(); cout << " * " << 7 << " ="; c.show(); cout << endl;
d = b / 2;
b.show(); cout << " / " << 2 << " ="; d.show(); cout << endl;
c.show();
c += 3;
cout << " += " << 3 << " ="; c.show(); cout << endl;
d.show();
d *= 2;
cout << " *= " << 2 << " ="; d.show(); cout << endl;
if (b < 2)
{ b.show(); cout << " < " << 2 << endl; }
return 0;
}


Remember that the conversions to type Rational, however implicit (silent), are function calls to the conversion constructor. When the function terminates, the temporary object created for this conversion is destroyed with the call to the destructor. (For this class, it is a default destructor supplied by the compiler.) Remember that story about two functions here and two functions there? (Actually, the story was about two dollars here and two dollars there.) Hence, this version of class Rational is somewhat slower than the version that does not rely on argument conversions and provides a separate overloaded operator for each type of the argument.



This implicit use of conversion constructors is supported not only for overloaded operators, but also for any function, member function, and global function that has object parameters. As I mentioned in Chapter 9, conversion constructors deal a blow to the C++ system of strong typing. If intentionally you use a numeric value instead of an object, fine. If you use it by mistake, the compiler does not tell you that you are making a mistake.



C++ offers a wonderful technique for preventing errors and for forcing the designer of client code to tell the maintainer what is going on. This technique consists of using the keyword explicit with the constructor.





explicit Rational(long n=0, long d=1) // cannot be called implicitly
{ nmr = n; dnm = d;
this->normalize(); }



By declaring a constructor explicit, you make any implicit call to this constructor a syntax error.





Rational a(1,4), b(3,2), c, d;
c = a + 5; // syntax error: implicit call
c = a + Rational(5); // ok: explicit call
d = b - 1; // syntax error: implicit call
d = b - (Rational)1; // ok: explicit call
if (b < 1) // syntax error: implicit call
if (b < Rational(2)) // ok: explicit call
cout << "Everything is fine\n";



This is a very good idea because it gives the class designer a better control over the way the class objects are used by the client programmer.



Programmer-defined classes like Complex and Rational do indeed have to emulate the behavior of built-in numeric types as much as possible. Using numeric variables instead of objects in expressions is not an error but a legitimate technique to implement computational algorithms. In the code snippet above, I marked some lines as ok and other lines as syntax error. Given a choice, every programmer would prefer to write code like in the lines marked as syntax errors. The need to spell out the casts every time a numeric operand is used is an imposition on the client programmer and results in code that is less aesthetically pleasing.



From that point of view, the use of the keyword explicit for the constructors of classes like Complex and Rational is probably overkill.



NOTE



Do not use the keyword
explicit
for constructors of numeric classes that implement overloaded operator functions. Utilize it for classes where using a built-in argument instead of the class object in a function call is an error, not a legitimate way of using the class.









I l@ve RuBoard

No comments: