11.3. Overloaded Operators As friend Functions

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:

  1. f1 + f2
  2. f1 + i
  3. i + f1

In the previous section, we created two overloaded operators, the fraction::operator+(fraction) (which is symmetric) and fraction::operator+(int) (which is asymmetric). These functions handle the first two cases. But neither overloaded operator can handle the third case. (Question: Why didn't we include a fourth case adding two integers? You should be able to answer that question. Please see the discussion at the bottom of the page.)
  1. a + b ≡ a.operator+(b)
  2. b + a ≡ b.operator+(a)
  1. a + 5 ≡ a.operator(5)
  2. 5 + a ≢ 5.operator(a)
Commutativity and overloaded operators. We can call overloaded operators in two ways. The first is used in practice, while the second shows the relation between an operator's operands and a function's arguments. The equivalence symbol, , 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:

  1. Valid: matches the fraction operator+(fraction f); function (see Figure 4)
  2. Also valid: matches the fraction operator+(fraction f); function
  3. Valid: matches fraction fraction::operator+(int i); function (see Figure 5)
  4. Invalid: while 5 + a is arithmetically valid, the syntax 5.operator(a) is not - the left hand operand of the dot operator must be an object (see Figure 7). We solve this problem with a friend function.

friend Functions

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 picture showing how the expression '5+a' maps to the incorrect member function call '5.operator+(a)'. A picture showing how the expression '5+a' maps to the correct non-member function call 'operator+(5,a)'.
(a)(b)
The relationship between operands and arguments. The problem with having a constant as the left hand operand and a solution using a non-member function.
  1. Whenever an overloaded binary operator is implemented with a member function, the left hand operand will always become the implicit or this object on the left hand side of the dot operator, which is not valid for non-object data.
  2. The solution is to implement the operator as a non-member function. Binary operators implemented as non-member functions do not have an implicit or 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)
};
An image showing the expression 'a+b' written as the function call 'a.operator+(b)' An image showing the expression 'a+5' written as the function call 'a.operator+(5)' An image showing the expression '5+a' written as the function call 'operator+(5,a)'
(a)(b)(c)
Three versions of the fraction operator+ function. Each version of operator+ solves one of the cases listed at the top of the page.
  1. Member function where both operands are fractions.
  2. Member function where the left operand is a fraction and the right operand is an integer.
  3. Non-member function where the left operand is an integer and the right operand is a fraction.
Together, the three overloaded operators create a fully symmetric operation by handling all three fraction addition cases. Can we consolidate the three overloaded operators into a single operator? Happily, we can with two changes. First, we make a new 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).

Using Overloaded Operators In Practice

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.