For most kinds of numerical data, the addition operation is commutative. To make the fraction addition operation, operator+, fully commutative, it must function correctly in three different cases. Let f1 and f2 be two fraction objects, and i be an integer:
|
|
≡
, means that the expressions on either side produce the same result or have the same effect. We use the operator notation on the left side in practice. In contrast, the more familiar functional form on the right side helps us understand the value of implementing the operators as friend
functions.
Let a and b be two instances of the fraction class:
fraction operator+(fraction f);
function (see Figure 4)fraction operator+(fraction f);
functionfraction fraction::operator+(int i);
function (see Figure 5)friend
function.The problem represented by case (d) cannot be solved with any member function. Instead, it can only be solved with a non-member function, which doesn't have an implicit or this
object.
Member Function | Non-Member (friend) Function |
---|---|
(a) | (b) |
this
object on the left hand side of the dot operator, which is not valid for non-object data.this
object, so both operands become function arguments.While implementing the operator as a non-member function solves the problem of having a non-object before the dot operator, it introduces another problem: The function must be able to access the private variables of the class (e.g., the numerator and denominator), but non-member functions, by definition, cannot access any of the object's private features. If we make the numerator and the denominator public, we eliminate the encapsulation that is the hallmark of the object-oriented paradigm. It is for solving this and similar problems that friend
functions were created.
Friend functions are not members of the friending class, so they are not bound to an instance of the class, but at least one argument must be an instance of the friending class. A class can make a friend function simply by declaring it with the friend
keyword. But the class must declare the function as a friend, which prevents a function from unilaterally befriending a class and thereby gaining access to the class's private features. Although friend functions do circumvent encapsulation, they do so in a controlled way that preserves the spirit of encapsulation.
class fraction { private: int numerator; int denominator; public: fraction(int n = 0, int d = 1) : numerator(n), denominator(d); fraction operator+(fraction f); // (a) fraction operator+(int i); // (b) friend fraction operator+(int i, fraction f2); // (c) };
(a) | (b) | (c) |
friend
function. Second, we need an automated way of converting an integer (represented by 5 in versions (b) and (c)) to a fraction. We tackle both problems in the next section.
Friend functions, like overloaded operators themselves, are controversial and are disliked by some. However, there are situations, such as the one presented here, where rigidly following encapsulation results in inelegant code at best and an inability to solve a necessary problem at worst. Enter friend
functions. Think of friend functions as good, trusted neighbors: they are not members of our family, but we still give them a key to our house so that they can access the locked, private parts of our homes so that they can help us solve special problems (like feeding the cat and bringing in the mail while we're on vacation).
Although a.operator+(b)
and operator+(a, b)
are both valid ways of calling an overloaded operator implemented as a member function and as a friend function respectively, neither form is used in practice. In practice, both versions of the overloaded + operator are called using an operator notation: a + b
.
The valid but unused forms are presented here to help explain why friend functions are necessary: the dot operator is not supported (that is, cannot be used) by simple, fundamental data types (like integers, doubles, pointers, etc.). Only objects, instances of structures and classes, may use the dot operator. For example, the statement Feature
is only valid when "X" is an instance of a structure or a class.
Why don't we solve the problem of adding two integers?
Two techniques help me understand a program's behavior. First, is imagining how a program changes memory as it runs. You see this technique in action throughout the textbook in the many "abstract representations" illustrating many computing concepts. Second, and the technique relevant to the fraction addition question, is imagining how the compiler will treat a code fragment. In the case of adding two integers, let i1 and i2 be two integers. How does the compiler process the expression i1 + i2? This expression uses "ordinary" addition that is unrelated to the fraction class. At least one operand must be a fraction object to invoke one of the overloaded fraction operators.