Operator Overloading  «Prev  Next»
Lesson 5 Friend Functions in C++
Objective Compare the use of friend functions and member functions in a Matrix class

Friend Functions vs Member Functions in C++

The friend keyword in C++ grants non-member functions or other classes access to a class's private and protected members, appearing to violate encapsulation while serving legitimate design purposes. Understanding when friend functions provide advantages over member functions—and when they introduce unnecessary complexity—proves essential for designing clean, maintainable C++ interfaces. This lesson explores friend function mechanics through practical matrix operations, comparing friend and member function approaches to reveal the trade-offs between encapsulation, symmetry, implicit conversions, and implementation efficiency. By examining concrete examples and applying modern C++20/23 features, you'll learn to make informed decisions about when friendship enhances design and when it compromises it.

What Are Friend Functions

A friend function is a non-member function granted special access to a class's private and protected members. Unlike member functions that implicitly receive a this pointer, friend functions receive all arguments explicitly, operating outside the class while enjoying privileged access. The friend declaration appears inside the class definition but the function itself is not a member—it exists in the enclosing namespace scope. This hybrid status enables friend functions to access internals while maintaining flexibility about argument order and implicit conversions unavailable to member functions.
class matrix {
private:
   std::vector<std::vector<double>> data;
   size_t rows, cols;

public:
   matrix(size_t r, size_t c) : rows(r), cols(c) {
      data.resize(rows, std::vector<double>(cols, 0.0));
   }

   // Friend function declaration (not a member)
   friend matrix add_friend(const matrix& a, const matrix& b);

   // Member function declaration
   matrix add_member(const matrix& other) const;

   double& at(size_t i, size_t j) { return data[i][j]; }
   double at(size_t i, size_t j) const { return data[i][j]; }

   size_t get_rows() const { return rows; }
   size_t get_cols() const { return cols; }
};

Implementing Matrix Addition as a Friend Function

Implementing matrix addition as a friend function provides direct access to both matrices' private data members without requiring accessor methods. The friend function receives both operands as explicit parameters, treating them symmetrically. This approach minimizes function call overhead by accessing internal storage directly while maintaining the ability to work with both const and non-const matrices identically.
// Friend function implementation (outside the class)
matrix add_friend(const matrix& a, const matrix& b) {
   // Direct access to private members
   if (a.rows != b.rows || a.cols != b.cols) {
      throw std::invalid_argument("Matrix dimensions must match");
   }

   matrix result(a.rows, a.cols);

   // Direct access to private data vector
   for (size_t i = 0; i < a.rows; ++i) {
      for (size_t j = 0; j < a.cols; ++j) {
         result.data[i][j] = a.data[i][j] + b.data[i][j];
      }
   }

   return result;
}

void demonstrate_friend() {
   matrix m1(2, 3);
   matrix m2(2, 3);

   m1.at(0, 0) = 1.0;
   m1.at(1, 2) = 2.5;

   m2.at(0, 0) = 3.0;
   m2.at(1, 2) = 4.5;

   // Call friend function with both arguments explicit
   matrix sum = add_friend(m1, m2);

   std::cout << "sum[0][0] = " << sum.at(0, 0) << '\n';  // 4.0
   std::cout << "sum[1][2] = " << sum.at(1, 2) << '\n';  // 7.0
}

Implementing Matrix Addition as a Member Function

Implementing matrix addition as a member function provides implicit access to the left operand through this while receiving the right operand as an explicit parameter. This asymmetry means the function operates on one matrix while taking another as an argument. Member functions maintain encapsulation by default since they're part of the class interface, though they may still access private members of the parameter matrix if both objects are of the same class type.
// Member function implementation
matrix matrix::add_member(const matrix& other) const {
   // 'this' provides implicit access to left operand
   if (rows != other.rows || cols != other.cols) {
      throw std::invalid_argument("Matrix dimensions must match");
   }

   matrix result(rows, cols);

   for (size_t i = 0; i < rows; ++i) {
      for (size_t j = 0; j < cols; ++j) {
         // Access own private data directly
         // Access parameter's private data (same class type)
         result.data[i][j] = this->data[i][j] + other.data[i][j];
      }
   }

   return result;
}

void demonstrate_member() {
   matrix m1(2, 3);
   matrix m2(2, 3);

   m1.at(0, 0) = 1.0;
   m1.at(1, 2) = 2.5;

   m2.at(0, 0) = 3.0;
   m2.at(1, 2) = 4.5;

   // Call member function: m1 is implicit 'this'
   matrix sum = m1.add_member(m2);

   std::cout << "sum[0][0] = " << sum.at(0, 0) << '\n';  // 4.0
   std::cout << "sum[1][2] = " << sum.at(1, 2) << '\n';  // 7.0
}

Key Differences Between Friend and Member Functions

Friend and member functions differ in several important ways that affect design decisions. Friend functions treat all operands symmetrically as explicit parameters, while member functions treat the left operand asymmetrically through implicit this. Friend functions exist in namespace scope, enabling argument-dependent lookup and implicit conversions on all parameters. Member functions belong to the class, appearing in its interface and enabling use with the dot or arrow operators. Friend functions require explicit friend declarations, while member functions gain private access automatically. Understanding these differences guides appropriate usage in different contexts.
// Comparison of calling syntax
matrix m1(2, 2), m2(2, 2);

// Friend function: symmetric, all args explicit
matrix sum1 = add_friend(m1, m2);

// Member function: asymmetric, left arg implicit
matrix sum2 = m1.add_member(m2);

// With operator overloading
class matrix {
public:
   // Friend operator: symmetric, natural syntax
   friend matrix operator+(const matrix& a, const matrix& b);

   // Member operator: asymmetric
   matrix operator+(const matrix& other) const;
};

// Usage feels identical due to operator syntax
matrix result = m1 + m2;  // Works with either implementation

Implicit Conversions and Friend Functions

Friend functions enable implicit conversions on all parameters, while member functions only allow conversions on explicit parameters, not the implicit this object. This difference becomes significant when overloading operators for types that support conversions. A friend operator+ allows matrix + scalar and scalar + matrix through implicit conversion, while a member operator+ only allows matrix + scalar since the left operand must already be a matrix object.
class matrix {
public:
   matrix(size_t r, size_t c, double fill = 0.0);

   // Friend allows conversion on both operands
   friend matrix operator*(const matrix& m, double scalar);
   friend matrix operator*(double scalar, const matrix& m);

private:
   std::vector<std::vector<double>> data;
   size_t rows, cols;
};

// Friend implementation allows symmetry
matrix operator*(const matrix& m, double scalar) {
   matrix result(m.rows, m.cols);
   for (size_t i = 0; i < m.rows; ++i) {
      for (size_t j = 0; j < m.cols; ++j) {
         result.data[i][j] = m.data[i][j] * scalar;
      }
   }
   return result;
}

matrix operator*(double scalar, const matrix& m) {
   return m * scalar;  // Reuse other overload
}

void demonstrate_symmetry() {
   matrix m(3, 3, 1.0);

   matrix r1 = m * 2.0;    // Works: matrix * double
   matrix r2 = 2.0 * m;    // Works: double * matrix (friend required)
}


Friend Functions for Binary Operators

Binary operators like +, -, *, and == benefit from friend function implementation when symmetry matters or when both operands need identical treatment. Stream insertion (<<) and extraction (>>) operators must be friends since std::ostream or std::istream appears as the left operand, not the class being output or input. Friend implementation enables these operators while keeping the class interface clean.
class matrix {
public:
   // Arithmetic operators as friends for symmetry
   friend matrix operator+(const matrix& a, const matrix& b);
   friend matrix operator-(const matrix& a, const matrix& b);

   // Comparison operators as friends
   friend bool operator==(const matrix& a, const matrix& b);
   friend bool operator!=(const matrix& a, const matrix& b);

   // Stream operators MUST be friends
   friend std::ostream& operator<<(std::ostream& os, const matrix& m);
   friend std::istream& operator>>(std::istream& is, matrix& m);

private:
   std::vector<std::vector<double>> data;
   size_t rows, cols;
};

// Stream insertion operator implementation
std::ostream& operator<<(std::ostream& os, const matrix& m) {
   os << "[";
   for (size_t i = 0; i < m.rows; ++i) {
      if (i > 0) os << ", ";
      os << "[";
      for (size_t j = 0; j < m.cols; ++j) {
         if (j > 0) os << ", ";
         os << m.data[i][j];
      }
      os << "]";
   }
   os << "]";
   return os;
}

// Equality comparison
bool operator==(const matrix& a, const matrix& b) {
   if (a.rows != b.rows || a.cols != b.cols) return false;

   for (size_t i = 0; i < a.rows; ++i) {
      for (size_t j = 0; j < a.cols; ++j) {
         if (a.data[i][j] != b.data[i][j]) return false;
      }
   }
   return true;
}

When to Prefer Member Functions

Member functions provide advantages when operations logically belong to the class interface, when asymmetry is natural or desired, or when the operation modifies the object. Unary operators like ++, --, and ! work naturally as members. Assignment operators must be members according to C++ rules. Functions that modify state belong as members to maintain clear ownership of mutation operations. Member functions also appear in IDE autocomplete and class documentation, making them more discoverable than friend functions.
class matrix {
public:
   // Operations that modify - prefer members
   matrix& operator+=(const matrix& other) {
      if (rows != other.rows || cols != other.cols) {
         throw std::invalid_argument("Dimension mismatch");
      }
      for (size_t i = 0; i < rows; ++i) {
         for (size_t j = 0; j < cols; ++j) {
            data[i][j] += other.data[i][j];
         }
      }
      return *this;
   }

   // Assignment must be member
   matrix& operator=(const matrix& other) {
      if (this != &other) {
         rows = other.rows;
         cols = other.cols;
         data = other.data;
      }
      return *this;
   }

   // Unary operators as members
   matrix operator-() const {
      matrix result(rows, cols);
      for (size_t i = 0; i < rows; ++i) {
         for (size_t j = 0; j < cols; ++j) {
            result.data[i][j] = -data[i][j];
         }
      }
      return result;
   }

   // Query operations - members are natural
   bool is_square() const { return rows == cols; }
   bool is_empty() const { return rows == 0 || cols == 0; }
};

Friend Classes

Declaring an entire class as a friend grants all its member functions access to private members. This broader privilege should be used sparingly, reserved for cases where two classes work together intimately, such as iterator classes accessing container internals or specialized helper classes that need deep access to implementation details. Friend class declarations are one-way and non-transitive—if A is a friend of B, B doesn't automatically access A's privates, and if B is friends with C, A doesn't gain access to C.
class matrix_iterator;

class matrix {
private:
   std::vector<std::vector<double>> data;
   size_t rows, cols;

   // Grant entire iterator class access
   friend class matrix_iterator;

public:
   matrix(size_t r, size_t c) : rows(r), cols(c) {
      data.resize(rows, std::vector<double>(cols, 0.0));
   }

   matrix_iterator begin();
   matrix_iterator end();
};

class matrix_iterator {
private:
   matrix* mat;
   size_t row, col;

public:
   matrix_iterator(matrix* m, size_t r, size_t c) 
      : mat(m), row(r), col(c) {}

   double& operator*() {
      // Can access private data because we're a friend class
      return mat->data[row][col];
   }

   matrix_iterator& operator++() {
      ++col;
      if (col >= mat->cols) {
         col = 0;
         ++row;
      }
      return *this;
   }

   bool operator!=(const matrix_iterator& other) const {
      return row != other.row || col != other.col;
   }
};

Modern C++20/23 Considerations

Modern C++ provides alternatives that sometimes reduce the need for friend functions. C++20's spaceship operator (<=>) can be a friend but often works as a member with defaulted implementation. Concepts enable constraining friend function templates. Hidden friend idiom—defining friend functions entirely within the class body—improves compilation times by limiting overload resolution search scope. C++23's explicit object parameters provide new ways to implement symmetric operations without friendship.
class matrix {
public:
   // C++20: Spaceship operator as friend
   friend auto operator<=>(const matrix& a, const matrix& b) = default;

   // Hidden friend idiom: define completely inside class
   friend matrix operator+(const matrix& a, const matrix& b) {
      if (a.rows != b.rows || a.cols != b.cols) {
         throw std::invalid_argument("Dimension mismatch");
      }
      matrix result(a.rows, a.cols);
      for (size_t i = 0; i < a.rows; ++i) {
         for (size_t j = 0; j < a.cols; ++j) {
            result.data[i][j] = a.data[i][j] + b.data[i][j];
         }
      }
      return result;
   }

   // Friend function template with C++20 concepts
   template<typename T>
   requires std::convertible_to<T, double>
   friend matrix operator*(T scalar, const matrix& m) {
      matrix result(m.rows, m.cols);
      for (size_t i = 0; i < m.rows; ++i) {
         for (size_t j = 0; j < m.cols; ++j) {
            result.data[i][j] = static_cast<double>(scalar) * m.data[i][j];
         }
      }
      return result;
   }

private:
   std::vector<std::vector<double>> data;
   size_t rows, cols;
};

Guidelines for Using Friend Functions

Use friend functions judiciously following clear guidelines. Prefer member functions unless friendship provides concrete benefits like symmetry, implicit conversions on all parameters, or simpler implementation for operations involving multiple classes. Limit friendship to specific functions rather than entire classes when possible. Use hidden friend idiom for operator overloads to improve compilation performance. Document why friendship was chosen over member functions when the decision isn't obvious. Consider that friendship creates coupling between classes—the friend function depends on implementation details that might change. Balance convenience against maintainability, choosing friendship when it significantly improves the interface or implementation without creating fragile dependencies.

Comparison Summary: Friend vs Member

Friend and member functions each excel in different scenarios. Choose friend functions for binary operators requiring symmetry, stream operators where the class appears as the right operand, operations needing implicit conversions on multiple parameters, or functions logically operating on multiple objects equally. Choose member functions for unary operators, compound assignment operators, operations that modify the object, operations logically belonging to the class interface, or situations where clear ownership of the operation matters. Both approaches access private members—friends through explicit declaration, members through class membership—with the choice driven by interface design and usage patterns rather than access requirements alone.

Conclusion

Friend functions provide a controlled mechanism for granting non-member functions access to class internals, enabling natural syntax for operations like operator overloading while maintaining encapsulation boundaries elsewhere. The matrix class demonstrates how friend and member implementations differ in symmetry, calling syntax, and conversion behavior, revealing trade-offs between interface clarity and implementation flexibility. Modern C++20/23 features including concepts, spaceship operators, and hidden friend idioms enhance friend function capabilities while reducing compilation overhead. Understanding when friendship improves design versus when it unnecessarily couples classes enables informed decisions that balance expressiveness, maintainability, and encapsulation in professional C++ code. The key lies not in avoiding friends categorically but in applying them strategically where they genuinely improve the interface or implementation without creating fragile dependencies on implementation details.

Friend Function - Exercise

Click the Exercise link below to write two add() functions for the matrix class:
  1. One as a friend function
  2. One as a member function
Compare their design trade-offs and explain when one approach is preferable over the other.
Friend Function - Exercise

[1]accessor functions: Member functions that are public are called accessor functions when they do not change the object's data.

SEMrush Software