Operator Overloading & friends

Operator Overloading & friends

Operator Overloading

Now we’re ready to take a look at operator overloading:

  • We’ll start with perhaps the most common: assignment
  • As we advance, we’ll see:
    • where and how this is used
    • where temporary objects become important
    • and lots of other things!

Initialization vs. Assignment

It is essential to understand the difference between initialization and assignment — they are two different things!

Initialization

Occurs in three contexts:

  • definitions with initializers
  • format function arguments
  • function return types

For example:

Complex a(1);	// 1-arg constructor
Complex b = a;	// Copy constructor
Complex c = Complex(1,2);
				// 2-arg constructor
				// + copy constructor

Assignment

Occurs in:

  • expressions (not definitions!) that use the = operator.

Initialization & Initializer Lists

An object of a class with:

  • no constructors, and
  • no private or protected members, and
  • no virtual functions, and 
  • no base classes

can be initialized using an initializer list:

//
//  Invoice.h
//  Initializer Lists
//
//  Created by Bryan Higgs on 8/20/24.
//

#ifndef Invoice_h
#define Invoice_h

class LineItem {
public:
  void Print();
  
  std::string m_description;
  int         m_quantity;
  double      m_unit_price;
};

class Invoice {
public:
  void Print();
  
  std::string m_bill_to;
  int         m_item_count;
  LineItem    m_item[20];
};

#endif /* Invoice_h */
//
//  Invoice.cpp
//  Initializer Lists
//
//  Created by Bryan Higgs on 8/20/24.
//

#include <iostream>

#include "Invoice.h"

void LineItem::Print()
{
  std::cout << "Line Item" << std::endl;
  std::cout << " Description: " << m_description << std::endl;
  std::cout << " Quantity:    " << m_quantity << std::endl;
  std::cout << " Unit Price:  " << m_unit_price << std::endl; 
}

void Invoice::Print()
{
  std::cout << "Invoice" << std::endl;
  std::cout << " Bill To: " << m_bill_to << std::endl;

  for (int i = 0; i < m_item_count; i++)
  {
    m_item[i].Print();
  }
}

Here’s a program to show how you can use an initializer list to initialize an instance of class Invoice:

//
//  main.cpp
//  Initializer Lists
//
//  Created by Bryan Higgs on 8/20/24.
//

#include <iostream>

#include "Invoice.h"

int main(int argc, const char * argv[]) 
{
  Invoice myInvoice =
  { "Fred Bloggs", 2,
    { {"Widgets", 3, 3.50 },
      {"Grommetts", 5, 5.60 } }
  }; // Initializer list
  
  myInvoice.Print();
  
  return 0;
}

Here’s what the program outputs

Invoice
 Bill To: Fred Bloggs
Line Item
 Description: Widgets
 Quantity:    3
 Unit Price:  3.5
Line Item
 Description: Grommetts
 Quantity:    5
 Unit Price:  5.6
Program ended with exit code: 0

Initialization and Constructors

An object of a class with a constructor must:

  • be initialized, or:
  • have a default constructor

The default constructor is used for objects that are not explicitly initialized:

//
//  Complex.hpp
//  Initialization & Constructors
//
//  Created by Bryan Higgs on 8/21/24.
//

#ifndef Complex_hpp
#define Complex_hpp

class Complex
{
public:
  Complex();
  Complex(double real, double imag = 0.0);
  
  double m_real, m_imag;
};

#endif /* Complex_hpp */
//
//  Complex.cpp
//  Initialization & Constructors
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "Complex.hpp"

Complex::Complex()
{
  std::cout << "In Default Complex constructor" << std::endl;
  m_real = 0.0;
  m_imag = 0.0;
}

Complex::Complex(double real, double imag)
{
  std::cout << "In Default Complex(real, imag) constructor" << std::endl;
  m_real = real;
  m_imag = imag;
}

Here’s a program that demonstrates when various Complex constructors are invoked:

//
//  main.cpp
//  Initialization & Constructors
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "Complex.hpp"

int main(int argc, const char * argv[]) 
{
  std::cout << "Complex c;" << std::endl;
  Complex c;
  
  std::cout << "Complex ca[10] = { 2.3, 5.4 /*...*/ };" << std::endl;
  Complex ca[10] = { 2.3, 5.4 /*...*/ };

  return 0;
}

… and here is what it produces:

Complex c;
In Default Complex constructor
Complex ca[10] = { 2.3, 5.4 /*...*/ };
In Default Complex(real, imag) constructor
In Default Complex(real, imag) constructor
In Default Complex constructor
In Default Complex constructor
In Default Complex constructor
In Default Complex constructor
In Default Complex constructor
In Default Complex constructor
In Default Complex constructor
In Default Complex constructor
Program ended with exit code: 0

Initialization vs. Assignment

Imagine we have a simple MyString class:

//
//  MyString.h
//  Initialization vs. Assignment
//
//  Created by Bryan Higgs on 8/21/24.
//

#ifndef MyString_h
#define MyString_h

#include <string.h>
#include <iostream>

class MyString
{
public:
  MyString(const char source[])
  {
    size = strlen(source);
    p = new char[size + 1];
    strcpy(p, source);
  }
  ~MyString();
  void Print();
  
private:
  size_t size;
  char   *p;
};

#endif /* MyString_h */
//
//  MyString.cpp
//  Initialization vs. Assignment
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "MyString.h"

MyString::~MyString()
{
  std::cout << "deleting MyString: " << (void*)p << std::endl;
  delete [] p;
}
void MyString::Print()
{
  std::cout << "MyString p = " << (void*) p
            << "; size = " << size
            << "; \"" << p << "\""
            << std::endl;
}

… and test it:

//
//  main.cpp
//  Initialization vs. Assignment
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "MyString.h"

int main(int argc, const char * argv[]) 
{
  MyString s1("Hello!");
  MyString s2("Goodbye");

  std::cout << "s1: "; s1.Print();
  std::cout << "s2: "; s2.Print();
  
  std::cout << std::endl;
  
  s1 = s2;    // Assign s2 to s1
  std::cout << "s1: "; s1.Print();
  std::cout << "s2: "; s2.Print();

  return 0;
}

Here is what it produces:

s1: MyString p = 0x600000008030; size = 6; "Hello!"
s2: MyString p = 0x600000008040; size = 7; "Goodbye"

s1: MyString p = 0x600000008040; size = 7; "Goodbye"
s2: MyString p = 0x600000008040; size = 7; "Goodbye"
deleting MyString: 0x600000008040
deleting MyString: 0x600000008040
malloc: *** error for object 0x600000008040: pointer being freed was not allocated

So what happened?

The assignment operation worked, right? After all, we didn’t get a compiler error, right?

But what happened in the destructor?

  • We have an attempt to do double deallocation!
  • We also didn’t deallocate something we should have!

Besides, how did the assignment get accomplished, in the first place?

The problems arose because:

  • The compiler automatically generated an assignment operator.
  • The generated assignment operator did a memberwise (‘shallow’) copy.
    • In other words, it copied the first level elements of the class, but not what the pointers pointed to.

To solve the problem for this class, we must write our own assignment operator.

The Assignment Operator

Here is an example of an assignment, for a class T:

T one, two;
one = two;

An assignment operator is a special class member function that, for a class T, looks like:

T &operator=(const T &rhs);

Note:

  • The right hand side of the assignment is the const T &rhs argument.
  • The left hand side of the assignment is *this .

The return type is T & , to support:

one = two = three;

which is equivalent to:

one.operator=(two.operator=(three))

Got that?

Implementing an Assignment Operator

So, to fix the problem, we add an explicit assignment operator to the class:

//
//  MyString.h
//  Initialization vs. Assignment
//
//  Created by Bryan Higgs on 8/21/24.
//

#ifndef MyString_h
#define MyString_h

#include <string.h>
#include <iostream>

class MyString
{
public:
  MyString(const char source[])
  {
    size = strlen(source);
    p = new char[size + 1];
    strcpy(p, source);
  }
  ~MyString();
  void Print();
  
  // Explicit assignment operator
  MyString &operator=(const MyString &s);
  
private:
  size_t size;
  char   *p;
};

#endif /* MyString_h */
//
//  MyString.cpp
//  Initialization vs. Assignment
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "MyString.h"

MyString::~MyString()
{
  std::cout << "deleting MyString: " << (void*)p << std::endl;
  delete [] p;
}
void MyString::Print()
{
  std::cout << "MyString p = " << (void*) p
            << "; size = " << size
            << "; \"" << p << "\""
            << std::endl;
}

// Explicit assignment operator
MyString &MyString::operator=(const MyString &s)
{
  std::cout << "MyString assignment operator" << std::endl;
  if (this != &s)  // Beware of x = x !!! WHY??
  {
    std::cout << "deleting: "
              << (void *)p << std::endl;
    delete [] p;
    p = new char[(size = s.size) + 1];
    strcpy(p, s.p);
  }
  return *this;  // Important!  WHY??
}

… and with no changes to the test program:

//
//  main.cpp
//  Initialization vs. Assignment
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "MyString.h"

int main(int argc, const char * argv[]) 
{
  MyString s1("Hello!");
  MyString s2("Goodbye");

  std::cout << "s1: "; s1.Print();
  std::cout << "s2: "; s2.Print();
  
  std::cout << std::endl;
  
  s1 = s2;    // Assign s2 to s1
  std::cout << "s1: "; s1.Print();
  std::cout << "s2: "; s2.Print();

  return 0;
}

Here is what it now produces:

s1: MyString p = 0x60000000c040; size = 6; "Hello!"
s2: MyString p = 0x60000000c050; size = 7; "Goodbye"

MyString assignment operator
deleting: 0x60000000c040
s1: MyString p = 0x60000000c040; size = 7; "Goodbye"
s2: MyString p = 0x60000000c050; size = 7; "Goodbye"
deleting MyString: 0x60000000c050
deleting MyString: 0x60000000c040
Program ended with exit code: 0

Voila!

No double deallocation! All instances deallocated properly!

In this way, the assignment operator may be overloaded for a class.

Other Operators

You can overload the following operators in C++:

+*/%^&|~
!=<>+=-=*=/=%=
^=&=|=<<>><<=>>===!=
<=>=&&||++,->*->
()[]newdeletenew[]delete[]
Where () is the function call operator, and [] is the subscript operator.

You can overload both the unary and binary forms of the following operators:

+*&

You cannot overload the following operators:

..*::?:

Operator functions are not usually called directly, but they can be, using the following syntax:

Complex z = a.operator+(b);

Here are its restrictions:

  • An overloaded operator may not have default arguments.
  • The operators for built-in types may not be overridden.
  • You cannot introduce an operator that is not already part of C++.
  • You cannot change the precedence or the associativity of the operators.

Unary and Binary Operators

A unary operator@‘ (the @ represents the actual operator) may be declared:

As a non-static member function taking zero arguments:

x.operator@()

As a non-member function taking one argument:

operator@(x)

A binary operator@‘ may be declared:

  • As a non-static member function taking one argument:
x.operator@(y)
  • As a non-member function taking two arguments:
operator@(x,y)

When a non-static member function is used, no user-defined conversions will be applied to x for matching purposes
They will be applied for non-member functions.

Member or Non-Member Operator?

Here’s how to decide whether to use a member or non-member operator overload:

  • Do you have access to the source for the class, and can you change it? (i.e. does it belong to you alone?)
    • If not, the operator must be a non-member function.
  • If the operator is one of:    =  []  ()  ->
    • It must be a member function.
  • If it’s a non-member function:
    • it can’t use this, explicitly or implicitly.
    • it has no access to the private parts of the class (but see later…) 
  • If it’s a member-function:
    • implicit conversions will not be applied to the left-hand side operand.

Here, operator- is a non-member:

Here’s an example of an operator- implementation as a non-member:

//
//  Point.h
//  Member or Non-Member Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#ifndef Point_h
#define Point_h

class Location
{
public:
  Location(int xvalue, int yvalue);
  
  int x, y;
};

class Point
{
public:
  Point(int xvalue, int yvalue);
  Point(const Location &loc);
  
  int x, y;
};

// non-member function (reverses the y coordinate)
extern Point operator-(const Point &rhs);

#endif /* Point_h */
//
//  Point.cpp
//  Member or Non-Member Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#include "Point.h"

Location::Location(int xvalue, int yvalue)
{
  x = xvalue;
  y = yvalue;
}

Point::Point(int xvalue, int yvalue)
{
  x = xvalue;
  y = yvalue;
}

Point::Point(const Location &loc)
{
  x = loc.x; y = loc.y;
}

// non-member function (reverses the y-coordinate)
Point operator-(const Point &rhs)
{
  return Point(rhs.x, -rhs.y); // Reverse y coordinate
}

… and a test program:

//
//  main.cpp
//  Member or Non-Member Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "Point.h"

int main(int argc, const char * argv[]) 
{
  Location loc(50, 25);
  Point p(-loc);
  std::cout << "p.(x,y): (" << p.x
            << ',' << p.y << ')'
            << std::endl;

  return 0;
}

which produces the following output:

p.(x,y): (50,-25)
Program ended with exit code: 0

In other words, it did what it was supposed to do – reverse the value of the y coordinate.

Now, let’s implement operator- as a member:

Here’s an example of an operator- implementation as a member:

//
//  Point.h
//  Member or Non-Member Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#ifndef Point_h
#define Point_h

class Location
{
public:
  Location(int xvalue, int yvalue);
  
  int x, y;
};

class Point
{
public:
  Point(int xvalue, int yvalue);
  Point(const Location &loc);
  
  Point operator-(); // member function
  
  int x, y;
};

#endif /* Point_h */
//
//  Point.cpp
//  Member or Non-Member Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#include "Point.h"

Location::Location(int xvalue, int yvalue)
{
  x = xvalue;
  y = yvalue;
}

Point::Point(int xvalue, int yvalue)
{
  x = xvalue;
  y = yvalue;
}

Point::Point(const Location &loc)
{
  x = loc.x; y = loc.y;
}

// member function (reverses the y-coordinate)
Point Point::operator-()
{
  return Point(x, -y); // Reverse y coordinate
}

… and the exact same test program:

//
//  main.cpp
//  Member or Non-Member Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "Point.h"

int main(int argc, const char * argv[]) 
{
  Location loc(50, 25);
  Point p(-loc);
  std::cout << "p.(x,y): (" << p.x
            << ',' << p.y << ')'
            << std::endl;

  return 0;
}

Now gives a compile time error at line 14

main.cpp:14:11 Invalid argument type 'Location' to unary expression

In other words, a member function implementation does not work for this case.

For unary operators, this can be a good thing!  So:

Recommendation: implement unary operators as member functions, if possible.

For binary operators, it depends on whether you wish the left-hand operand to be subject to conversion:

Complex c;
c += 5;				    // Convert c?

Complex d = c + 1.0;
Complex e = 1.0 + c;	// Convert 1.0?

Summary of Recommendations

OperatorRecommendation
All unary operatorsmember
= [] () ->must be member
+= -= /= *= ^= &=
|= ~= %= >>= <<=
member
All other binary operatorsnon-member

Why the Assignment Operator is Special

The (binary) assignment operator, operator=, is special, because:

  • It must be a (non-static) member function of its class.
  • It is the only operator that is not inherited:
    “Assignment has a useful and necessary generalization across all classes (memberwise copy).  No other operator has that.  Assignment resembles constructors and destructors more than it resembles operators such as + and += .” 
    [C++ ARM, p. 335]

If you do not write an operator= function for a class, the compiler will generate one for that class.  The generated function will do a memberwise (shallow) copy.

Subscripting, or operator[]

Subscripting:

mumble[5]

is considered to be a binary operator. The expression:

x[y]

is interpreted as:

x.operator[](y)

Looks a bit strange, doesn’t it?

In order for subscripting to be valid on both sides of an assignment operator:

x[5] = y[3];

operator[] must return a reference to the appropriate type.

  • NOTE: operator[] must be a non-static class member function.

Example: A Vector Class

Here’s an example of an operator[] implementation as a member(required):

//
//  Vector.h
//  Subscript Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#ifndef Vector_h
#define Vector_h

class Vector // A vector of ints
{
public:
  // Constructors & destructors
  explicit Vector(const int elements = 10);
  Vector(const Vector &source);
  Vector(const int source[],
         const int elements);
  ~Vector() { delete [] m_p; }

  int getElementCount() { return m_elems; }
  
  // Subscript operator overload
  int &operator[](const int i);
  
private:
  int *m_p;    // Ptr to array
  int  m_elems;  // Alloc. count
};

#endif /* Vector_h */
//
//  Vector.cpp
//  Subscript Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "Vector.h"

Vector::Vector(const int elements)
{
  // Allocate space for elements
  m_p = new int[m_elems = elements];
  // Set all elements to 0
  for (int i = 0; i < m_elems; i++)
    m_p[i] = 0;
}

Vector::Vector(const Vector &source)
{
  // Allocate space for elements
  m_p = new int[m_elems = source.m_elems];
  // Copy values from source elements
  for (int i = 0; i < m_elems; i++)
    m_p[i] = source.m_p[i];
}

Vector::Vector(const int source[],
               const int elements)
{
  // Allocate space for elements
  m_p = new int[m_elems = elements];
  // Copy values from source elements
  // (assuming that source is properly sized)
  for (int i = 0; i < m_elems; i++)
    m_p[i] = source[i];
}

// Subscript operator overload
int &Vector::operator[](const int i)
{
  // Do bounds checking...
  if ( (i < 0) || (i >= m_elems) )
  {
    std::cerr << "Illegal Vector index: "
              << i << " (max: "
              << (m_elems - 1) << ')'
              << std::endl;
    exit(1);  // Abrupt exit!
  }
  // We're within bounds
  return m_p[i];
}

… and a test program:

//
//  main.cpp
//  Subscript Operator
//
//  Created by Bryan Higgs on 8/21/24.
//

#include <iostream>
#include "Vector.h"

int main(int argc, const char * argv[])
{
  Vector v1;
  Vector v2;
  int i;

  for (i = 0; i < v1.getElementCount(); i++)
    v1[i] = i;
  for (i = 0; i < v2.getElementCount(); i++)
    v2[i] = i*i;
  for (i = 0; i < v1.getElementCount(); i++)
    std::cout << v1[i] << ",";
  std::cout << std::endl;
  for (i = 0; i < v2.getElementCount(); i++)
    std::cout << i << " squared = "
              << v2[i] << std::endl;
  return 0;
}

which produces the following output:

0,1,2,3,4,5,6,7,8,9,
0 squared = 0
1 squared = 1
2 squared = 4
3 squared = 9
4 squared = 16
5 squared = 25
6 squared = 36
7 squared = 49
8 squared = 64
9 squared = 81
Program ended with exit code: 0

YAY!

Function calls: operator()

A function call:

expression(expression-list)

can be interpreted as a binary operator with the expression as the left operand and the expression-list as the right operand. For example:

f(x,y,z)

can be interpreted as:

f.operator()(x,y,z)

Is this syntax getting weirder, or what?

Note that operator() must be a non-static member function.

Uses for operator()

Here’s an example.

A Matrix class that uses operator() as a convenient way of extracting a value from a matrix, given its row and column:

//
//  Matrix.hpp
//  Function Call Overloads
//
//  Dynamic matrix of double values
//
//  Created by Bryan Higgs on 8/22/24.
//

#ifndef Matrix_hpp
#define Matrix_hpp

class Matrix
{
public:
  // Constructors
  Matrix(int rows, int cols, double *data[] = 0);
  // Destructor
  ~Matrix();
  // Overloaded operators
  Matrix& operator=(const Matrix& m);
  Matrix& operator+=(Matrix& m);
  
  // overloaded function operator
  double& operator()(int i, int j);
  
  int getRows() { return m_rows; }
  int getColumns() { return m_cols; }
  
  void Print();
private:
  // Copy constructor (private, and not implemented)
  Matrix(const Matrix& src);

  int m_cols, m_rows;
  double **m_p;
};

#endif /* Matrix_hpp */

… and a test program:

//
//  main.cpp
//  Function Call Overloads
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include "Matrix.hpp"

int main(int argc, const char * argv[]) 
{
  double data1[] = { 1, 2, 3, 4, 5 };
  double data2[] = { 6, 7, 8, 9, 10 };
  double data3[] = { 11, 12, 13, 14, 15 };
  double *data[] = { data1, data2, data3 };
  
  Matrix m1(3, 5, data);
  std::cout << "m1 contains: " << std::endl;
  m1.Print();
  std::cout << "element (1,3) of m1 is: "
            << m1(1,3) << std::endl;

  Matrix m2(8, 3);
  for (int row = 0; row < m2.getRows(); row++)
  {
    for (int col = 0; col < m2.getColumns(); col++)
    {
      m2(row, col) = row*col;
    }
  }
  std::cout << "m2 contains: " << std::endl;
  m2.Print();
  return 0;
}

It produces the following output:

m1 contains: 
[0]: 1 2 3 4 5
[1]: 6 7 8 9 10
[2]: 11 12 13 14 15
element (1,3) of m1 is: 9
m2 contains: 
[0]: 0 0 0
[1]: 0 1 2
[2]: 0 2 4
[3]: 0 3 6
[4]: 0 4 8
[5]: 0 5 10
[6]: 0 6 12
[7]: 0 7 14
Program ended with exit code: 0
//
//  Matrix.cpp
//  Function Call Overloads
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include <assert.h>
#include "Matrix.hpp"

// Constructors
Matrix::Matrix(int rows, int cols,
               double *data[])
{
  m_cols = cols;
  m_rows = rows;
  m_p = new double*[rows];
  assert(m_p != 0);
  for (int row = 0; row < rows; row++)
  {
    m_p[row] = new double[cols];
    assert(m_p[row] != 0);
    for (int col = 0; col < cols; col++)
    {
      if (data != 0)
        m_p[row][col] = data[row][col];
      else
        m_p[row][col] = 0.0;
    }
  }
}

// Destructor
Matrix::~Matrix()
{
  for (int row = 0; row < m_rows; row++)
  {
    delete [] m_p[row];
  }
  delete [] m_p;
}

// Overloaded operators
// =
Matrix& Matrix::operator=(const Matrix& src)
{
  if (this != &src)
  {
    int row;
    // Deallocate the existing matrix
    for (row = 0; row < m_rows; row++)
    {
      delete [] m_p[row];
    }
    delete [] m_p;
    // Allocate space for the new matrix
    // and copy values in from source
    m_cols = src.m_cols;
    m_rows = src.m_rows;
    m_p = new double*[m_rows];
    for (row = 0; row < m_rows; row++)
    {
      m_p[row] = new double[m_cols];
      assert(m_p[row] != 0);
      for (int col = 0; col < m_cols; col++)
      {
        m_p[row][col] = src.m_p[row][col];
      }
    }
  }
  return *this;
}

// +=
Matrix& Matrix::operator+=(Matrix& rhs)
{
  // += only meaningful on identically sized matrices
  assert( rhs.m_rows == m_rows
       && rhs.m_cols == m_cols );
  for (int row = 0; row < m_rows; row++)
  {
    for (int col = 0; col < m_cols; col++)
    {
      m_p[row][col] += rhs.m_p[row][col];
    }
  }
  return *this;
}

// Overloaded function operator
// ()
double& Matrix::operator()(int i, int j)
{
  assert( i >= 0 && i < m_rows
       && j >= 0 && j < m_cols);
  return m_p[i][j];
}

// Print method
void Matrix::Print()
{
  for (int row = 0; row < m_rows; row++)
  {
    std::cout << '[' << row << "]:";
    for (int col = 0; col < m_cols; col++)
    {
      std::cout << ' ' << m_p[row][col];
    }
    std::cout << std::endl;
  }
}

Functors

One common use of the overloaded operator() operator is to define a class as a ‘Functor’.

A Functor class is one that is designed to act like a function.

Here’s a class that encapsulates the abs operation on an int – that is, converting an int value to its absolute value:

//
//  main.cpp
//  Function Objects ('Functors')
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>

// A class designed simply to support a function call
// (aka a 'function object', or 'functor')
class AbsInt
{
public:
  // Overloaded function call operator
  int operator()(int value)
  {
    return value < 0 ? -value : value;
  }
};

int main(int argc, const char * argv[]) 
{
  AbsInt abs;
  unsigned int absvalue = abs(-42);
  std::cout << "absvalue = " << absvalue << std::endl;
  return 0;
}

That program produces the following output:

absvalue = 42
Program ended with exit code: 0

In other words, the call to abs (the instance of AbsInt), supplying the integer value -42 has returned its absolute value, 42.

The C++ Standard Library uses functors quite a bit. For example, it contains several function objects in the <functional> header file.

However, be aware that this area is definitely advanced C++!

Other Uses for operator()

Other popular uses for operator() are:

  • To implement substring capability for strings.
  • As an ‘iterator’ operator, to iterate through various ‘containers’.
  • As a ‘call’ operator for objects that act as functions. 

The C++ Standard Template Library (STL) makes heavy use of the latter two styles.

We’ll be talking about the C++ Standard Template Library much later. Because it uses a number of advanced C++ features, and some advanced concepts, it takes a while to figure the STL out!

The Operator: operator->

The -> operator is considered to be a unary operator.

An expression:

x->m

is interpreted as:

(x.operator->())->m

for a class object of type T if :

T::operator->()

exists.

So, operator->() must return:

  • a pointer to a class that has a member m, or:
  • an object of, or reference to, a class for which operator->() is defined.

More on this later…

The Operators: operator++ and operator–

The prefix and postfix increment and decrement operators are implemented by the functions:

operator++
operator--

For example:

class Foo
{
public:
  Foo &operator++();	   // ++a
  Foo &operator++(int);  // a++
  Foo &operator--();	   // --a
  Foo &operator--(int);  // a--
  // ...  (Why return Foo & ???)
};

The presence of the int argument is C++’s way of distinguishing between the postfix and prefix versions. 
The value passed in this int will be 0.

The Addition Operator: operator+

Let’s see how to override the (binary) addition operator:

Here’s a Complex class that implements the addition operator, operator+ :

//
//  Complex.hpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#ifndef Complex_hpp
#define Complex_hpp

class Complex
{
public:
  Complex(double real = 0.0, double imag = 0.0);
  Complex operator+(const Complex &rhs);
  ~Complex();
  
  void Print();
private:
  double m_real, m_imag;
};

#endif /* Complex_hpp */
//
//  Complex.cpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include "Complex.hpp"

Complex::Complex(double real, double imag)
{
  std::cout << "In constructor for " << this << std::endl;
  m_real = real;
  m_imag = imag;
}

Complex Complex::operator+(const Complex &rhs)
{
  std::cout << "Assigning values from " << &rhs << " to " << this << std::endl;
  return Complex(m_real + rhs.m_real,
                 m_imag + rhs.m_imag);
}

Complex::~Complex()
{
  std::cout << "In Destructor for " << this << std::endl;
}

void Complex::Print()
{
  std::cout << "Complex: ["
            << m_real << ", " << m_imag << "]" << std::endl;
}

… and a test program:

//
//  main.cpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include "Complex.hpp"

int main(int argc, const char * argv[]) 
{
  std::cout << "Complex x(2.0, 3.0);" << std::endl;
  Complex x(2.0, 3.0);  // ???
  x.Print();
  std::cout << "Complex y(1.0, 2.0);" << std::endl;
  Complex y(1.0, 2.0);  // ???
  y.Print();
  std::cout << "Complex z;" << std::endl;
  Complex z;            // ???
  z.Print();
  
  std::cout << "Operations..." << std::endl;
  std::cout << "z = y;" << std::endl;
  z = y;        // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = 1.0;" << std::endl;
  z = 1.0;        // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = x + y;" << std::endl;
  z = x + y;      // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = y + 3;" << std::endl;
  z = y + 3.0;      // ???
  std::cout << "z: "; z.Print();

  return 0;
}

which produces the following output:

Complex x(2.0, 3.0);
In constructor for 0x7ff7bfeff0c0
Complex: [2, 3]
Complex y(1.0, 2.0);
In constructor for 0x7ff7bfeff0a0
Complex: [1, 2]
Complex z;
In constructor for 0x7ff7bfeff090
Complex: [0, 0]
Operations...
z = y;
z: Complex: [1, 2]
z = 1.0;
In constructor for 0x7ff7bfeff080
In Destructor for 0x7ff7bfeff080
z: Complex: [1, 0]
z = x + y;
Assigning values from 0x7ff7bfeff0a0 to 0x7ff7bfeff0c0
In constructor for 0x7ff7bfeff070
In Destructor for 0x7ff7bfeff070
z: Complex: [3, 5]
z = y + 3;
In constructor for 0x7ff7bfeff050
Assigning values from 0x7ff7bfeff050 to 0x7ff7bfeff0a0
In constructor for 0x7ff7bfeff060
In Destructor for 0x7ff7bfeff060
In Destructor for 0x7ff7bfeff050
z: Complex: [4, 2]
In Destructor for 0x7ff7bfeff090
In Destructor for 0x7ff7bfeff0a0
In Destructor for 0x7ff7bfeff0c0
Program ended with exit code: 0

Given this, can you deduce what member functions are invoked in the program?

What if we reverse the expressions of the addition?

Here’s the test program, with an additional line at the end:

//
//  main.cpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include "Complex.hpp"

int main(int argc, const char * argv[]) 
{
  std::cout << "Complex x(2.0, 3.0);" << std::endl;
  Complex x(2.0, 3.0);  // ???
  x.Print();
  std::cout << "Complex y(1.0, 2.0);" << std::endl;
  Complex y(1.0, 2.0);  // ???
  y.Print();
  std::cout << "Complex z;" << std::endl;
  Complex z;            // ???
  z.Print();
  
  std::cout << "Operations..." << std::endl;
  std::cout << "z = y;" << std::endl;
  z = y;        // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = 1.0;" << std::endl;
  z = 1.0;        // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = x + y;" << std::endl;
  z = x + y;      // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = y + 3;" << std::endl;
  z = y + 3.0;      // ???
  std::cout << "z: "; z.Print();

  z = 3.0 + y;
  
  return 0;
}

Now, we get a compile-time error:

main.cpp:37:11 Invalid operands to binary expression ('double' and 'Complex')

What’s wrong? How can we fix it?

Let’s try using a non-member function…

Here’s a Complex class that implements the addition operator, operator+ as a non-member function:

//
//  Complex.hpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#ifndef Complex_hpp
#define Complex_hpp

class Complex
{
public:
  Complex(double real = 0.0, double imag = 0.0);
  ~Complex();
  
  void Print();
private:
  double m_real, m_imag;
};

extern Complex operator+(const Complex &lhs,
                         const Complex &rhs);

#endif /* Complex_hpp */
//
//  Complex.cpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include "Complex.hpp"

Complex::Complex(double real, double imag)
{
  std::cout << "In constructor for " << this << std::endl;
  m_real = real;
  m_imag = imag;
}

// Non-member operator+
Complex operator+(const Complex &lhs, const Complex &rhs)
{
  std::cout << "Assigning values from " << &rhs << " to " << &lhs << std::endl;
  return Complex(lhs.m_real + rhs.m_real,
                 lhs.m_imag + rhs.m_imag);
}

Complex::~Complex()
{
  std::cout << "In Destructor for " << this << std::endl;
}

void Complex::Print()
{
  std::cout << "Complex: ["
            << m_real << ", " << m_imag << "]" << std::endl;
}

But, now we have more compile-time errors:

//
//  Complex.cpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include "Complex.hpp"

Complex::Complex(double real, double imag)
{
  std::cout << "In constructor for " << this << std::endl;
  m_real = real;
  m_imag = imag;
}

// Non-member operator+
Complex operator+(const Complex &lhs, const Complex &rhs)
{
  std::cout << "Assigning values from " << &rhs << " to " << &lhs << std::endl;
  return Complex(lhs.m_real + rhs.m_real,
                 lhs.m_imag + rhs.m_imag);
}

Complex::~Complex()
{
  std::cout << "In Destructor for " << this << std::endl;
}

void Complex::Print()
{
  std::cout << "Complex: ["
            << m_real << ", " << m_imag << "]" << std::endl;
}

Now, we get a compile-time error:

'm_real' is a private member of 'Complex'
'm_imag' is a private member of 'Complex'

We need to allow the non-member function to be able to access private data in the Complex class.

We could make the data public, but that violates data abstraction. It’s not a good idea.

A Class Needs its friends

So, we’d like to write it this way, but in order to do so, it looks like we’d have to make the class’ data public!

But wait!  There’s a C++ feature called friends of a class:

What is a friend?

A friend to a class has access to that class’ private parts.  

A rather special relationship, and not one to be granted lightly!

A friend can be:

  • A non-member function.
friend int ally(double cross);

A member function of another class.

friend int X::pal(int token);

An entire other class.

friend class Chum;

Note that a function declared as a friend within a class does not become a member function (method) of that class.

Example using the previous program:

We make the operator+ function a friend of the Complex class:

//
//  Complex.hpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#ifndef Complex_hpp
#define Complex_hpp

class Complex
{
public:
  Complex(double real = 0.0, double imag = 0.0);
  ~Complex();
  
  friend Complex operator+(const Complex &lhs,
                           const Complex &rhs);
  
  void Print();
private:
  double m_real, m_imag;
};

#endif /* Complex_hpp */

No changes were made to the Complex.cpp file.

Here’s the slightly modified main program:

//
//  main.cpp
//  Addition Operator
//
//  Created by Bryan Higgs on 8/22/24.
//

#include <iostream>
#include "Complex.hpp"

int main(int argc, const char * argv[]) 
{
  std::cout << "Complex x(2.0, 3.0);" << std::endl;
  Complex x(2.0, 3.0);  // ???
  x.Print();
  std::cout << "Complex y(1.0, 2.0);" << std::endl;
  Complex y(1.0, 2.0);  // ???
  y.Print();
  std::cout << "Complex z;" << std::endl;
  Complex z;            // ???
  z.Print();
  
  std::cout << "Operations..." << std::endl;
  std::cout << "z = y;" << std::endl;
  z = y;        // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = 1.0;" << std::endl;
  z = 1.0;        // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = x + y;" << std::endl;
  z = x + y;      // ???
  std::cout << "z: "; z.Print();
  std::cout << "z = y + 3;" << std::endl;
  z = y + 3.0;      // ???
  std::cout << "z: "; z.Print();
  
  std::cout << "z = 3.0 + y;" << std::endl;
  z = 3.0 + x;
  std::cout << "z: "; z.Print();
  
  return 0;
}

Here’s the output from the program:

Complex x(2.0, 3.0);
In constructor for 0x7ff7bfeff0c0
Complex: [2, 3]
Complex y(1.0, 2.0);
In constructor for 0x7ff7bfeff0a0
Complex: [1, 2]
Complex z;
In constructor for 0x7ff7bfeff090
Complex: [0, 0]
Operations...
z = y;
z: Complex: [1, 2]
z = 1.0;
In constructor for 0x7ff7bfeff080
In Destructor for 0x7ff7bfeff080
z: Complex: [1, 0]
z = x + y;
Assigning values from 0x7ff7bfeff0a0 to 0x7ff7bfeff0c0
In constructor for 0x7ff7bfeff070
In Destructor for 0x7ff7bfeff070
z: Complex: [3, 5]
z = y + 3;
In constructor for 0x7ff7bfeff050
Assigning values from 0x7ff7bfeff050 to 0x7ff7bfeff0a0
In constructor for 0x7ff7bfeff060
In Destructor for 0x7ff7bfeff060
In Destructor for 0x7ff7bfeff050
z: Complex: [4, 2]
z = 3.0 + x;
In constructor for 0x7ff7bfeff030
Assigning values from 0x7ff7bfeff0c0 to 0x7ff7bfeff030
In constructor for 0x7ff7bfeff040
In Destructor for 0x7ff7bfeff040
In Destructor for 0x7ff7bfeff030
z: Complex: [5, 3]
In Destructor for 0x7ff7bfeff090
In Destructor for 0x7ff7bfeff0a0
In Destructor for 0x7ff7bfeff0c0
Program ended with exit code: 0

Who is Whose friend, Anyway?

Who picks the friends of a class?

  • The class does!
  • Otherwise, any amorous function could come along and declare undying friendship to any class it had a yen for…

The C++ language does not (yet) have a gigolo keyword!

Summary

Well, here are the topics we have just learned about:

  • Operator Overloading
  • Friends

However, there are quite a few more topics relating to classes still to learn…

Our saga continues!

Index