Last Updated:

C ++ : Overloading Operations with Examples

1. Sharing

If there are several different function declarations with the same name in a particular scope, that name is called shared. When such a name is used, the desired function is chosen by comparing the types of actual parameters with the types of formal parameters.

double abs(double x);
int    abs(int x);

abs(1);
abs(1.0); 

Because for every type T, types T and T& allow for matching sets of initializing values, functions with parameter types that differ only in this respect cannot have the same name.

Functions that differ only in return type cannot have the same name.

Different versions of a shared class member function can grant different access rights.


class Buffer
 { private:
    char *p;
    int size;
   protected:
    Buffer(int s, char *np) { size = s; p = np; }
   public:
    Buffer(int s) { p = new char[size = s]; }
   ...
  };

The process of finding a suitable function from a set of overloads is to find the best match between the types of formal and actual arguments. This is done by checking the set of criteria in the following order:

  1. exact type matching, i.e. full or match achieved by trivial type conversions (e.g., array name and pointer, function name and function pointer, types T and const T);
  2. correspondence achieved by "advancing" integral types (e.g. bool in intchar in intshort in int) and float in double;
  3. correspondence achieved by standard transformations (e.g. int to doubledouble to intdouble to long double, pointers to derived types to pointers to base types, pointers to arbitrary types in void*int to unsigned int);
  4. Compliance achieved through user-defined transformations
  5. Matching through ellipsises in the function declaration.

If matching can be obtained in two ways at the same criteria level, the call is considered ambiguous and rejected.

The result of the overload does not depend on the order in which the functions are declared.

An alternative to overloading is to use multiple functions with different names and different types of arguments. At the same time, you have to remember several names and how to use them correctly. In addition, if there is no overload, all standard transformations are applied to the function arguments, which can also lead to additional errors.

Functions declared in different scopes (not namespaces) are not overloaded.


void f(int);

void g()
 { void f(double); 
      
   f(1);
  } 

2. Overloading of operations

Each technical field – and most non-technical – has its own standard symbols to facilitate the presentation and discussion of common concepts. For example, due to constant use, the expression x + y * z is clearer to us than the phrase multiply y by z and add the result to x. It is difficult to overestimate the importance of a concise and expressive form of recording typical operations.

Like most other languages, C++ supports a set of operations for built-in types. However, most of the concepts for which operators are typically used are not built-in C++ types, so they must be represented as user-defined types. Defining (or overloading) operations for such classes allows the programmer to implement a more familiar and convenient form of record for manipulating objects than that available using only the basic functional form of the record.

Most C language operations can be shared (overloaded). To do this, in the class declaration, you must declare a function that has the following name:
<type> operator <operation> (<oprands>)

Cannot be shared operations . .* :: ?:

Both the unary and binary forms of operations + – * &  can be used together.

An operator function must either be a member function of a class or have at least one parameter of some class or a reference to a class. A prefix unary operation can be declared as a non-static class member function with no parameters, or as a non-member function with a single parameter. A binary operation can be declared either as a non-static member function with a single parameter, or as a regular function (not a member of a class) with two parameters.

To distinguish between the prefix and postfix variants of the operation, the function that implements the postfix variant is declared with an additional parameter of type int.

It is unacceptable and impossible to change the seniority, associativity and number of operands in the operation.

A shared operation cannot have parameters with defaults.

Only a few assumptions are made about user-defined operations. Specifically, operator=operator(),operator[], and operator-> must be non-static member functions of the class—this ensures that their first operand is the l-value of . The second parameter (index) of the operator[] function can be of any type. This makes it possible to define vectors, associative arrays, etc.

Identities that are valid for operations on major types (for example, ++a ~ a += 1) do not have to be performed on operations on "class" types. This relationship is not maintained in user-defined operations unless the user takes care of it. The compiler will not generate an X::operator+= definition from the X::operator+ and X::operator= definitions.

For historical reasons, the operators = (assignment), & (address taking), and , (sequence) have a predefined meaning when applied to objects of a class. This predefined meaning may become inaccessible if you make the operators private.

class X
 { private:
    void operator =(const X&);
    void operator &();
    void operator ,(const X&);
    ...
  };      

void f(X a, X b)
 { a = b;
   &a;
   a, b;
  } 

On the other hand, operators can be given new meaning by defining them accordingly.

Let's consider an example of defining and using overloaded operators.


class Complex
 { private:
    double r, m;
   public:
    Complex(double nr = 0,  double nm = 0) : r(nr), m(nm) { } 

    Complex& operator ++();
    Complex  operator ++(int);
    Complex  operator + (const Complex& c) const;
    Complex& operator +=(const Complex& c);
    bool     operator ==(const Complex& c) const;
  };
  
Complex& Complex::operator ++()
 { ++r;  return *this; }
 
Complex Complex::operator ++(int)
 { Complex x = *this;
   r++;
   return x;
  }
  
Complex Complex::operator + (const Complex& c) const
 { return Complex(r + c.r, m + c.m); }
 
Complex& Complex::operator +=(const Complex& c)
 { r += c.r; m += c.m; return *this; }
 
bool Complex::operator ==(const Complex& c) const
 { return r == c.r && m == c.m; }
 
void main()
 { Complex a(0, 0), b(2, 2), c;
 
   ++a;
   a++;
   c = a + b;
   c = a + 2.5;
   c = a + ++b;
  }

The operation arguments were defined as constant references. This allows you to use expressions that include ordinary arithmetic operators without intensive copying. For the Complex class, this may not give a significant gain in time, but for large objects this is not the case. Pointers cannot be used as arguments because it is impossible to replace the meaning of the operator applied to the pointer.

Returning a link may also seem like an effective solution.

Complex& operator + (const Complex& c);

This is acceptable, but causes problems with memory allocation. Because the result reference will be returned to the called function as a reference to the value returned from the function, the value cannot be an automatic variable. Because the operator can be used more than once in an expression, the result cannot be a static local variable. The result will usually reside in free memory. Copying a return value is often cheaper (in terms of execution time, code size, and data) than allocating and freeing memory for an object. Plus, it's easier to program.

However, there are operations that return the object to which the member function of the class was applied. These include operations that change their parameter, namely, prefix increment and decrement operations, as well as simple and compound assignments. In this case, you can return a reference, because you get a reference to the object that was created in the calling function, and it, unlike the local variables of the function, will not be destroyed when the function is terminated.

It is preferable to minimize the number of functions that directly manipulate the representation of the object. This can be achieved by declaring in the body of the class itself only those operations that must modify the value of the first argument, such as +=. Operations that simply emit a new value based on their arguments, such as +, are defined outside the class and use operations that have access to the view.


class Complex
 { private:
    double r, m;
   public:
    Complex(double nr = 0,  double nm = 0) : r(nr), m(nm) { }       
    Complex operator + (const Complex& c);     Requires access to a view 
  };
  
Complex Complex::operator += (const Complex& c)
 { r += c.r;
   m += c.m;
   return *this;
  }

Complex operator + (const Complex&c1, const Complex& c2)
 { Complex x = c1;      
   return x += c2;
  }     Access the view using += 

void main()
 { Complex a(0, 0), b(2, 2), c(7, -5);
 
   Complex r1 = a + b + c;
   
   Complex r2 = a;
   r2 += b;
   r2 += c;
  }

Except for the time difference, the calculations are equivalent. r1r2

Compound assignment operators such as += and *=are usually easier to define than their "simple" + and * parts. This is surprising at first, but this conclusion follows from the fact that the + operation involves three objects (two operands and a result), and the += operation has only two. In the latter case, getting rid of temporary variables improves performance at run time. In addition, such functions (in simple cases) are easily made embeddable.

3. Example

The Vector class that you are developing is a vector with a variable number of elements. The operations of assignment, addition, subtraction, scalar product, and comparison are defined over the vectors. For easy access to the elements of the vector, the "index expression" operation is also defined.

#include <malloc.h>

class Vector
 { private:
    int    size;
    double *v;
   public:
    explicit Vector(int n = 0);
    Vector(const Vector& vector);
    ~Vector();
    int GetSize() const { return size; }
    int SetSize(int n);
    Vector&  operator =  (const Vector& vector);
    double&  operator [](int n);
    Vector  operator -  () const;
    int     operator == (const Vector& vector) const;
    Vector  operator +  (const Vector& vector) const;
    Vector& operator += (const Vector& vector);
    Vector  operator -  (const Vector& vector) const;
    Vector& operator -= (const Vector& vector);
    Vector  operator +  (double value) const;
    Vector& operator += (double value);
    Vector  operator -  (double value) const;
    Vector& operator -= (double value);
    double  operator *  (const Vector& vector) const;
  };       Vector size // Array address for a vector // Default constructor // Copy constructor // Destructor // Obtaining the size of a vector // Changing the size of a vector // Assignment operation // Index expression // Unary minus // Comparison // Addition // Compound assignment // Subtraction // Compound assignment // Addition to a number // Compound assignment // Subtraction of a number // Subtracting a number // Composite product 
 Vector::Vector(int n) { if (n < 0) n = 0; size = n; v = NULL; if (size) if ((v = (double *)malloc(size * sizeof(double))) == NULL) size = 0; } Vector::Vector(const Vector& vector) { size = vector.size; v = NULL; if (size) if ((v = (double *)malloc(size * sizeof(double))) == NULL) size = 0; else for (int i = 0; i < size; i++) *(v + i) = vector[i]; } Vector::~Vector() {  if (v) free(v); } int Vector::SetSize(int n) { if (n < 0) n = 0; size = n; if (size) if ((v = (double *)realloc(v, size * sizeof(double))) == NULL) size = 0; return size;  Equality 0 will be a sign of an error
  }
 Vector& Vector::operator = (const Vector& vector) { if (this == &vector)  Self-mapping check  return *this; size = vector.size; if (size)      
    if ((v = (double *)realloc(v, size * sizeof(double))) == NULL)  Here, resource cleanup is done by using the realloc function
  
 size = 0; else for (int i = 0; i < size; i++) *(v + i) = vector[i]; return *this;  Return the assigned value } double& Vector::operator [] (int n) { if (n < 0) n = 0; if (n >= size) n = size - 1;  The result of the index expression operation is a reference for return  * (this->v + n);  so that you can not only get the value of the vector element, but also change it
  }      
 Vector Vector::operator - () const { Vector res(size); for (int i = 0; i < size, i++) *(res.v + i) = -*(this->v + i); return res; } int Vector::operator == (const Vector& vector) const { if (size != vector.size)  return 0; for (int i = 0; i < size; i++) if (*(this->v + i) != *(vector.v + i)) return 0; return 1; } Vector Vector::operator + (const Vector& vector) const { Vector res(size); if (size != vector.size)  return res; for (int i = 0, i < size, i++) *(res.v + i) = *(this->v + i) + *(vector.v + i); return res; } Vector& Vector::operator += (const Vector& vector) { if (size != vector.size)  return *this; for (int i = 0; i < size, i++) *(this->v + i) += *(vector.v + i); return *this; } Vector Vector::operator - (const Vector& vector) const { Vector res(size); if (size != vector.size)  return res; for (int i = 0, i < size, i++) *(res.v + i) = *(this->v + i) - *(vector.v + i); return res; } Vector& Vector::operator -= (const Vector& vector) { if (size != vector.size)  return *this; for (int i = 0, i < size, i++) *(this->v + i) -= *(vector.v + i); return *this; } Vector Vector::operator + (double value) const { Vector res(size); for (int i = 0, i < size, i++) *(res.v + i) = *(this->v + i) + value; return res; } Vector& Vector::operator += (double value) { for (int i = 0; i < size; i++) *(this->v + i) += value; return *this; } Vector Vector::operator - (double value) const { Vector res(size); for (int i = 0, i < size, i++) *(res.v + i) = *(this->v + i) - value; return res; } Vector& Vector::operator -= (double value) { for (int i = 0; i < size; i++) *(this->v + i) -= value; return *this; } double Vector::operator * (const Vector& vector) const {  double res = 0; if (size != vector.size)  return res; for (int i = 0; i < size; i++) res += *(this->v + i) * *(vector.v + i); return res; } void main() { Vector v1(3), v2, v3(2), v4; int i; double r; v2. SetSize(3); for (i = 0; i < v1. GetSize(); i++)  Specify the elements of vectors v1 and v2 { v1[i] = i + 1; v2[i] = i + 5; } v3 = v1 + v2; v4 = v1 - v2 + v3; v4 = -v4; v3 += v1; r = v1 * v2; v4 = v1 * v2 + v3;  Since it is impossible to change the priority of operations, the operation *. // must first be performed, but since the operation of adding a number with a class object is not defined, the // expression cannot be calculated v4 = v1 + v2 * v3;  Correct
  }