Monday, December 21, 2009

Friend Functions




I l@ve RuBoard


Friend Functions


Let us look back at the story of overloaded operator functions and see what they accomplished for us in treating programmer-defined types similar to numeric types and what remains to be done.



I started with the statement that it is highly desirable to be able to treat variables of built-in types and of programmer-defined types in exactly the same way. C++ supports this approach by entering into a deal with the programmer. You as a programmer are required to give up your freedom of choice for function names. You start your function name with the keyword operator and you append to this keyword the symbol (or symbols) of the C++ built-in operator that you would like to use with the objects of your class.



There are some minor limitations on what you are allowed or not allowed to do, such as using only existing C++ operators (you cannot make up your own operator that the language does not recognize); and you cannot change the relative priority of operators, their associativity, or the number of operands they take. But these are minor. If you stick to your end of the bargain, C++ sticks to its end of the bargain: It recognizes the expressions that use that operator as function calls to the function that you designed according to the rules outlined above.



C++ still allows you to call the overloaded operator functions in the same way you call other C++ functions梑y using the function name (keyword operator plus the operator symbol), but few programmers ever do that. If you go to the trouble to call this function as a function, why bother to use the keyword operator? It is better to use your naming freedom and give the function a more-descriptive name, like addComplex() or addToComplex() or whatever. In the examples in this chapter, I wrote function calls using the full names of overloaded operator functions with one purpose only: to make sure you do not forget what is going on under the hood of a C++ program. Every use of the overloaded operator in an expression is in reality a function call. At least one function call. If local objects or returned objects are used, the use of operators also entails calls to constructors and destructors for these objects.



As with some bargains in real life, here you got more than you bargained for. The sky is the limit to what you can do inside the function whose header abides by the rules of overloaded operator functions. A good example is the operator+() function overloaded for class Complex in Listing 10.4. What is the meaning of this client code?





Complex x(20,40), y(30,50); // defined, initialized
+x; +y; // same as x.operator+(); and y.operator+();



If x and y were integers, the meaning of the second line would be clear: keep the sign of the value. Not a very interesting operation, but there can be no two opinions about it. With Complex objects, it does not mean: keep the sign of the value. It could mean anything. In this case, it means: print the contents of data members. For many classes, the operators that could be used on numbers cannot be applied to objects. Treating objects as numbers opens the way to producing code whose meaning is not intuitively clear, like using the plus sign for an output operation. (I will discuss better ways to overload operators for input and output of objects later.) This is a serious danger.



As with many bargains in real life, you also get less than you would like to. The overloaded operator functions are straightforward when they are applied to two object instances. If one operand is an object instance (the target of the message) and the second operand is of numeric type, there is a problem梩he use of the operator syntax becomes a function call to the overloaded operator function with incompatible argument type.



In the previous section, I discussed two possible solutions to this problem. One is to double the number of overloaded operator functions: For each function with the parameter of the class type you write an overloaded function with the same name and the parameter of the numeric type. This is a good solution but it bloats the class design and makes it more difficult to understand.



Another solution is to overload only one function for each operator (with the parameter of the class type) and to make sure that the class has a conversion constructor. This constructor converts the value of the numeric type into the value of the class type. When the operator is used with two operands of the class type, the constructor is not called before the overloaded operator function is called. When the second operand (the function parameter) is of a numeric type, the constructor is called implicitly (or explicitly, if it is defined with the keyword explicit) before the call to the overloaded operator function. This solution keeps the class size manageable, but it entails the creation and destruction of a temporary class object each time a numeric value is used as the actual argument. This might affect program performance. For example, the first line in the next code snippet does not call any conversion constructor, but the second line does.





Rational a(1,4), b(3,2), c;
c = a + b; // c = a.operator+(b); - match, no constructor call
c = a + 5; // c = a.operator+(5); - conversion constructor is called



But this is not the end of the story about mixed types in expressions. What about this sequence of statements in client code? The adding of two Rational objects is supported directly. Adding a Rational object and a number is supported through an additional call to the conversion constructor. But adding a number and a Rational object is not supported.





Rational a(1,4), b(3,2), c;
c = a + b; // c = a.operator+(b); - match, no constructor call
c = a + 5; // c = a.operator+(5); - conversion constructor is called
c = 5 + a; // syntax error: c = 5.operator+(a); is impossible



The expression that uses an overloaded operator member function is always a message to its left operand. Hence, this left operator must be an object instance. In the last line of the code snippet above, the left operand is a number. You cannot send a message to a number. It takes an object of a programmer-defined type to accept a message. Meanwhile, the last line in this code snippet is as legitimate as the previous line from the point of view of equal treatments of objects and numbers. Hence, it should be supported if you want to follow through with treating built-in types and programmer-defined types equally.



When you want to use the function whose interface is different from what the client code needs, one way to deal with the problem is to create a wrapper function. A wrapper function is a function with the same name you want to use and whose interface satisfies the client code and whose only purpose is to call the function you wanted to use in the client code to begin with. In the case of the operator+() for class Rational, the wrapper function should have the same name but it should be able to accept a numeric value as its first parameter.





Rational Rational::operator + (int i, const Rational &x) const
{ Rational temp1(i); // conversion constructor
Rational temp2 = temp1.operator+(x); // overloaded operator
return temp2; }



Or better yet:





Rational Rational::operator + (long i, const Rational &x) const
{ Rational temp(i); // call to the conversion constructor
return temp + x; } // call to operator+(const Rational&);



However, this function is impossible to use. There are three players here, the target of the message, the numeric parameter, and the object parameter. How do you put them together in a function call?





Rational a(1,4), b(3,2), c;
c.operator+(5, b); // c + ???



The overloaded function operator is called as a message to its left operand. That means that the meaning of the last line of code is the object c plus something else. But I want to add 5 and something else, and put the result in c. Hence, this line is a syntax error. Let us try again.





Rational a(1,4), b(3,2), c;
c = b.operator+(5, b); // c = b + ???



If the function name did not include the keyword operator, this would do. The value 5 would be converted to Rational, added with object b, and the result would be copied into object c. The use of the object b as the target of the message seems out of place梩his object has nothing to do with the operation. But the function name does include the keyword operator, and this syntax is no good either. There should be only two players, not three.



Actually, it would be nice to get rid of the target object altogether and call the function with two parameters only.





Rational a(1,4), b(3,2), c;
c = operator+(5, b); // c = 5 + b; ???



Remember the first overloaded operator functions that I used for class Complex in Listing 10.2? These functions were not class members. They were global functions. All I have to do to make the code snippet above work is to define the wrapper function as a global function.





Rational operator + (long i, const Rational &x) // not a class member function
{ Rational temp(i); // call to the conversion constructor
return temp + x; } // call to Rational::operator+(const Rational&);



Actually, I did more than just erase the class scope operator. I also eliminated the const modifier that specified that the function body does not change the fields of the target object. There is no target object here, and there is no need to testify that its fields do not change.



This is a good solution but it is too limited. It would be nice to use this function for other ways of writing the expression, not only for the case when the first operand is numeric. A good way to generalize this function is to eliminate the local Rational object and use the conversion constructor with the first parameter rather than in the body of the function.



When we use member functions to redefine a binary operator, the left argument is implicit, in the form of the pointer.





Rational operator + (const Rational &x, const Rational &y)
{ return x.operator+(y); } // call to Rational::operator+(const Rational&);



Notice that the expression syntax in the body of this function is not appropriate. It will be interpreted as a recursive call to the global function operator+() I am defining here.





Rational operator + (const Rational &x, const Rational &y)
{ return x + y; } // recursive call to operator+(): infinite loop



This is one of the few examples where an explicit call to the class member function using the function call syntax rather than the operator syntax is necessary. It allows the global function operator+() with two parameters to call the class member function (with one parameter).



I went through the steps of designing the interface for this global function because I felt you should trace these steps in detail. Many C++ programmers are not comfortable with writing the same algorithm as either a member function or a global function. I formulated the rules of transition in Chapter 9: The global function has one extra class parameter. The member function does not have this parameter but uses the argument object as the target of the message. Make sure you are comfortable with this transition.



Now I have to admit that this design has a flaw. I did not want to discuss it simultaneously with other issues and spread your scope of attention too thin, but now it is time to get to the problem. When the operator syntax is used in the client code, the compiler has two options for interpreting the expression: either to call the class member function with one parameter or to call the global function with two parameters. Each function provides a legitimate interpretation of the expression, be it the expression with two object instances or the one with one class instance and one operand of a built-in type (with the appropriate call to the conversion constructor). Of course, if both operands are of built-in types, there is no ambiguity梩he compiler interprets the expression as a built-in operator rather than as a call to an overloaded operator function.





Rational a(1,4), b(3,2), c;
c = a + b; // ambiguity: c = a.operator+(b); or c = operator+(a,b); ??
c = a + 5; // c=a.operator+(Rational(5)); or c=operator+(a,Rational(5));
c = 5 + a; // no ambiguity: c=operator+(Rational(5),a); no 5.operator+(a);
c = 5 + 5; // no ambiguity: the built-in binary addition operator



This is a pity because it contradicts the general algorithm of parsing the meaning of a name by the compiler as described in Chapter 9. For nonoperator functions, the compiler first looks at class member functions, and only if no match for the name is found in the class scope does it look up the name among the global functions known in this file. No such luck for operator functions.



To eliminate ambiguity for the expression where both operands are objects, I can eliminate the member operator function and implement the algorithm of the operator in the global function directly. Then the compiler will find only one way to interpret the expression.





Rational operator + (const Rational &x, const Rational &y) // no Rational::
{ return Rational(y.nmr*x.dnm+x.nmr*y.dnm,y.dnm*x.dnm); } // private data??



This is a nice solution, but it is too "direct"梚t directly accesses the fields of its parameters, but the function itself is outside the scope of class Rational and hence has no right to do so. This means that this function will not compile.



C++ offers you an interesting workaround: the use of friend functions. A friend function is a nonmember function that has the same access rights to class members as does any member function. Notice that I am careful to say "access rights to class members" rather than just "access rights to class data" because a friend function can access private (or protected) member functions as easily as it can access private (or protected) data members.



A friend function can be either a global function or a member function of another class. Actually, there are situations where you want to allow access to class members by all member functions of another class. In this case, you will define another class as being a friend of this class. (We will discuss this situation in more detail in Chapter 12, "Composite Classes: Pitfalls and Advantages."
). However, most friend functions are global functions: If you feel that you want to define a single function of another class as a friend of this class, think again; you are probably making things more complex than is necessary.



To define a function as a friend of the class, you insert the prototype of this function into the class specification (as if it were a class member), and you precede the prototype with the keyword friend. This does the trick; for all intents and purposes this function is like a member of the class.





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(); }
friend Rational operator + (const Rational &x, const Rational &y);
// THE REST OF CLASS Rational:
// no need for operator+() functions
} ;



Well, I got excited about this trick and my last statement goes too far. There is a difference between a friend function and a member function. To call a friend function, you do not have to specify the target object as you do when you call a class member function. But this function can access the Rational class members as if it were a class member function, and hence this version of the function is now perfectly legitimate.





Rational operator + (const Rational &x, const Rational &y) // no Rational::
{ return Rational(y.nmr*x.dnm+x.nmr*y.dnm,y.dnm*x.dnm); } // yes, private data



Replacing the class member function with the friend function removes ambiguity from the client code.





Rational a(1,4), b(3,2), c;
c = a + b; // no ambiguity: c = operator+(a,b);
c = a + 5; // no ambiguity: c = operator+(a,Rational(5));
c = 5 + a; // no ambiguity: c=operator+(Rational(5),a);
c = 5 + 5; // no ambiguity: the built-in binary addition operator



All three forms of the expression with Rational objects are supported. If you are concerned with calls to the Rational conversion constructor, you can avoid them by overloading the operator function three times and defining all three functions as friends to class Rational.





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(); }
friend Rational operator + (const Rational &x, const Rational &y);
friend Rational operator + (const Rational &x, long y);
friend Rational operator + (long x, const Rational &y);
// THE REST OF CLASS Rational
} ;



As I mentioned earlier, you can use the similar technique of multiple overloading with member functions. Friend functions have an advantage over member functions because member functions can support only those forms where a Rational object is the left operand and not the form where the left operand is a numeric variable. Since a call to an overloaded operator member function is interpreted as a message to the left operand, supporting this form would require the compiler to assign meaning to expressions like this:





c = 5.operator+(a); // an integer cannot respond to Rational messages



Friend functions are more flexible for mixing numeric and object operands because they do not necessarily need the left operand as an object.



A similar approach can be applied to relational operators. A class member function with an object parameter supports only expressions that have object instances as its operands. If you want to support expressions with a numeric value as the right operand, you should add either a conversion operator or another overloaded operator function with the numeric parameter. Still, this does not support the expressions where the left operand is a numeric value and the right operand is an object.





Rational a(1,4), b(3,2);
if (a < b) cout << "a < b\n"; // a.operator<(b);
if (a < 5) cout << "a < 5\n"; // a.operator<(5);
if (1 < b) cout << "1 < b\n"; // 1.operator<(b); is nonsense
if (1 < 5) cout << "1 < 5\n"; // built-in inequality operator



Adding to the program a global overloaded operator function can be used to support the fourth line of this client code.





bool operator < (const Rational &x, const Rational &y)
{ return x.operator<(y); }



Similar to the case of the arithmetic operator, using both global and member operator functions creates ambiguity for the second and the third lines of code.





Rational a(1,4), b(3,2);
if (a < b) cout << "a < b\n"; // a.operator<(b); or operator<(a,b);
if (a < 5) cout << "a < 5\n"; // a.operator<(5); or operator<(a,5);
if (1 < b) cout << "1 < b\n"; // no ambiguity: operator<(1,b);
if (1 < 5) cout << "1 < 5\n"; // built-in inequality operator



To support all forms of relational expressions, you can replace each member operator function with the global operator function that accesses the data members of its parameters directly. To make this access legitimate, you should define this global operator function as a friend of the class.





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(); }
friend Rational operator + (const Rational &x, const Rational &y);
friend Rational operator - (const Rational &x, const Rational &y);
friend Rational operator * (const Rational &x, const Rational &y);
friend Rational operator / (const Rational &x, const Rational &y);
friend bool operator < (const Rational &x, const Rational &y);
friend bool operator > (const Rational &x, const Rational &y);
friend bool operator == (const Rational &x, const Rational &y);
// THE REST OF CLASS Rational
} ;



This design eliminates ambiguities and supports all forms of relational expressions with objects as both operands, only the right operand, and only the left operand (the most difficult case).





Rational a(1,4), b(3,2);
if (a < b) cout << "a < b\n"; // operator<(a,b);
if (a < 5) cout << "a < 5\n"; // operator<(a,Rational(5));
if (1 < b) cout << "1 < b\n"; // operator<(Rational(1),b);
if (1 < 5) cout << "1 < 5\n"; // built-in inequality operator



As you see, friend operator functions can do the same job as member operator functions do and more. The only operators that cannot be overloaded as friends are the assignment operator (operator=()), the subscript operator (operator[]()), the arrow selector operator (operator->()), and the function call or parentheses operator (operator()()). This limitation is necessary to make sure that the first operand is an lvalue (a target of the message). In all previous examples in this section, both the first operand and the second operand are rvalues.



Next, let us look at arithmetic assignment operators. The situation here is somewhat different because these operators do not return a value (return type is void). Instead, they modify the state of the target object. Since they do not return a new value of class Rational, they do not call the Rational constructor that normalizes the state of the object. Hence, the arithmetic operators have to call the Rational::normalize() function before returning.





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



This operator supports the expressions where both the left and the right operands are objects (e.g., c+=b;).With the conversion constructor, this operator supports the expressions where the left operand is an object and the right one is a numeric value (e.g., c+=5;).





Rational a(1,4), b(3,2), c;
c = a + b; // c = operator+(a,b);
c += b; // c.operator+=(b);
c += 5; // c.operator+=(Rational(5));
5 += c; // 5.operator+=(c); is nonsense, is it not?



Replacing the overloaded operator member function with a global overloaded operator function does not buy you much, at least at first glance.





class Rational {
long nmr, dnm; // private data
void normalize(); // private member function
public:
Rational(long n=0, long d=1) // general, conversion, default constructor
{ nmr = n; dnm = d;
this->normalize(); }
friend void operator += (Rational &x, const Rational &y); // no const!
// THE REST OF CLASS Rational
} ;



This operator changes the value of its first parameter. This is why this parameter does not have the const modifier.





void operator += (Rational &x, const Rational &y) // no const!
{ x.nmr = x.nmr*y.dnm + y.nmr*x.dnm; x.dnm *= y.dnm;
x.normalize(); } // here, normalize() has the message target



Remember I told you that a friend function has access to all class members, not just to data members? This consistent treatment of class members pays off here: this operator function accessed private data members of its parameters and the private member function normalize().



This function supports the same forms of expression as the member operator function supports.





Rational a(1,4), b(3,2), c; long x = 5;
c = a + b; // c = operator+(a,b);
c += b; // operator+=(c,b);
c += 5; // operator+=(c,Rational(5));
5 += c; // a constant cannot be used as an lvalue
x += c; // operator+=(Rational(x),c); what is this?



Adding anything to a numeric literal (a constant value) is a syntax error, no questions asked. Adding to a numeric variable is more complex. This variable can be changed, especially when passed to a function whose reference parameter does not have the const modifier.



However, this argument is not a Rational object, and the type conversions are required. The compiler creates a temporary object, calls the Rational conversion constructor to initialize it, and passes the value of x to the constructor as an argument. Now what? Modifying this temporary object within the operator function (as parameter x) is useless because this object is going to die when the function terminates, and the change will not be passed back to the variable x. A decent compiler should declare this a syntax error.



Even though in this case we cannot treat numerical types and programmer-defined types equally, this example shows that we can go a very long way toward that goal. Actually, many programmers prefer to use global friend operator functions rather than member functions, because the global operator functions are easier to write梩hey treat their operators symmetrically.




Listing 10.7 shows the implementation of class Rational with overloaded operator functions implemented as friends rather than as member functions. Figure 10-7 shows the output of the program.





Figure 10-7. Output for program in Listing 10.7.










Example 10.7. Class Rational that uses friend functions to support mixed type expressions.


#include <iostream.h>

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(); }
friend Rational operator + (const Rational &x, const Rational &y);
friend Rational operator - (const Rational &x, const Rational &y);
friend Rational operator * (const Rational &x, const Rational &y);
friend Rational operator / (const Rational &x, const Rational &y);
friend void operator += (Rational &x, const Rational &y);
friend void operator -= (Rational &x, const Rational &y);
friend void operator *= (Rational &x, const Rational &y);
friend void operator /= (Rational &x, const Rational &y);
friend bool operator == (const Rational &x, const Rational &y);
friend bool operator < (const Rational &x, const Rational &y);
friend bool operator > (const Rational &x, const Rational &y);
void show() const;
} ; // end of class specification
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; // search for greatest common divisor
while (value != gcd) { // stop when the GCD is found
if (gcd > value)
gcd = gcd - value; // subtract smaller number from the greater
else value = value - gcd; }
nmr = sign * (nmr/gcd); dnm = dnm/gcd; } // denominator is always positive
Rational operator + (const Rational &x, const Rational &y)
{ return Rational(y.nmr*x.dnm + x.nmr*y.dnm, y.dnm*x.dnm); }
Rational operator - (const Rational &x, const Rational &y)
{ return Rational(x.nmr*y.dnm - y.nmr*x.dnm, x.dnm*y.dnm); }
Rational operator * (const Rational &x, const Rational &y)
{ return Rational(x.nmr * y.nmr, x.dnm * y.dnm); }
Rational operator / (const Rational &x, const Rational &y)
{ return Rational(x.nmr * y.dnm, x.dnm * y.nmr); }
void operator += (Rational &x, const Rational &y)
{ x.nmr = x.nmr * y.dnm + y.nmr * x.dnm; x.dnm *= y.dnm;
x.normalize(); }
void operator -= (Rational &x, const Rational &y)
{ x.nmr = x.nmr*y.dnm + y.nmr*x.dnm; x.dnm *= y.dnm;
x.normalize(); }
void operator *= (Rational &x, const Rational &y)
{ x.nmr *= y.nmr; x.dnm *= y.dnm;
x.normalize(); }
void operator /= (Rational &x, const Rational &y)
{ x.nmr = x.nmr * y.dnm; x.dnm = x.dnm * y.nmr;
x.normalize(); }
bool operator == (const Rational &x, const Rational &y)
{ return (x.nmr * y.dnm == x.dnm * y.nmr); }
bool operator < (const Rational &x, const Rational &y)
{ return (x.nmr * y.dnm < x.dnm * y.nmr); }
bool operator > (const Rational &x, const Rational &y)
{ return (x.nmr * y.dnm > x.dnm * y.nmr); }
int main()
{ Rational a(1,4), b(3,2), c, d;
c = 5 + a;
cout << " " << 5 << " +"; a.show(); cout << " =";
c.show(); cout << endl;
d = 1 - b; // operator-(Rational(1),b);
cout << " 1 -"; b.show(); cout << " ="; d.show(); cout << endl;
c = 7 * a; // operator*(Rational(7),a);
cout << " 7 *"; a.show(); cout << " ="; c.show(); cout << endl;
d = 2 / b; // operator/(Rational(2),b);
cout << " 2 /"; b.show(); cout << " ="; d.show(); cout << endl;
c.show();
c += 3; // operator+=(c,Rational(3));
cout << " += " << 3 << " ="; c.show(); cout << endl;
d.show();
d *= 2; // operator*=(d,Rational(2))
cout << " *= " << 2 << " ="; d.show(); cout << endl;
if (a < 5) cout << " a < 5\n"; // operator<(a,Rational(5));
if (1 < b) cout << " 1 < b\n"; // operator<(Rational(1),b);
if (1 < 5) cout << " 1 < 5\n"; // built-in inequality operator
if (d * b - a == c - 1) cout << " d*b-a == c-1 ==";
(c - 1).show(); cout << endl;
return 0;
}


Many programmers struggle with implementing operators as member functions instead of using friend functions. The main reason for not using friends is the conviction that friends break encapsulation, information hiding, and all other good things that object-oriented programming promises us.



True, excessive use of friend functions makes code confusing and more difficult to maintain. There is no question about that. But what about reasonable use of friend functions? And what is reasonable and what is excessive when it comes to friends?



The best way to answer this question is to recall the major goal of using classes in C++. Remember that? We need classes because when we use stand-alone global functions that access data structures, the connection between the functions and the data is only in the mind of the designer, not necessarily in the mind of the maintainer or the client programmer. Also, the encapsulation is voluntary and any function can access data directly, without using access functions. Right? Also, we wanted to have the local class scope so that the names of function and data that we use for one part of the program would not conflict with the names that we use for other parts of the program. Remember that list? I want to make sure that you hear this list of goals often enough so that you will be able to apply it to evaluation of the quality of C++ code.



With these criteria in mind, let us take a look at the design with overloaded operators implemented as member functions in Listing 10.6.





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



Is the connection between data and function clear? Yes, the opening and closing braces of the class scope denote this connection. Is data protected from accessing from the functions other than member functions? Yes, data members are defined as private and cannot be accessed from the outside of the class. Is there a danger of name conflicts between members of class Rational and members of other classes? No any other class can define functions with names like operator+() and so on, and there will be no conflict.



It looks like a good design. Now let us compare it with the design that uses friend functions in Listing 10.7.





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(); }
friend Rational operator + (const Rational &x, const Rational &y);
friend Rational operator - (const Rational &x, const Rational &y);
friend Rational operator * (const Rational &x, const Rational &y);
friend Rational operator / (const Rational &x, const Rational &y);
friend void operator += (Rational &x, const Rational &y);
friend void operator -= (Rational &x, const Rational &y);
friend void operator *= (Rational &x, const Rational &y);
friend void operator /= (Rational &x, const Rational &y);
friend bool operator == (const Rational &x, const Rational &y);
friend bool operator < (const Rational &x, const Rational &y);
friend bool operator > (const Rational &x, const Rational &y);
void show() const;
} ; // end of class specification



Do you see what I am driving at? The list of functions connected with data is right here, between the opening and closing braces of the class scope. This list is clear not only to the class designer but also to the client programmer and the maintainer. Is data protected from access from the functions other than the functions declared between the class braces? Yes, data are declared private, and any function that needs access to data members have to be declared within class braces either as a member function or as a friend. What about name conflicts? Let us say we want to implement the overloaded operator function operator+() as a friend of class Complex. Will this function name conflict with the operator+() that is a friend of class Rational? No, the operator+() that deals with Complex objects will have a different signature.





Complex operator + (const Complex &x, const Complex &y);



So, what about friend functions that break encapsulation, information hiding, and other good things promised by object-oriented programming? This design with friend functions is every bit as good as the design with member functions. Mostly, it is a matter of taste. To my taste, friend operators are easier to code and to verify. Another important difference is that global operators support all forms of expressions, and member functions support only those forms where the left operand is an object, not a numeric value.



TIP



Do not hesitate to use friend functions when implementing overloaded operator functions. They are easier to design than member functions and they support all three forms of expression in the client code (both operands are objects, only the left operand is an object, only the right operand is an object). Do not use friend functions when they make code confusing.









I l@ve RuBoard

No comments: