Chapter 12 - Notes

In addition to encapsulating data in the form of member variables, and pieces of logic in the form of member functions, classes can also encapsulate operators that make it easy to operate on instances of a class.

C++ Operators

Operators in C++ are the symbols that you use to manipulate one or more variables. The ones you are familiar with are the + and - operators.

Declaring Operators

Operators in C++ are not too different to that of a function, the only difference is the use of the operator keyword.

The declaration of an operator is similar to that of a function declaration:

return_type operator operator_symbol( ... parameters...);

The operator_symbol in this case could be any of the operator types available.

When to Overload Operators

Operators are intuitive and easy to use. Consider a utility class Date that encapsulates the day, month, and year:

Date holiday (12, 25, 2016); // initialized to Dec 25, 2016

If you want to add a day to the holiday variable such that it should then contain Dec 26, 2016, it would be far more intuitive to be able to do:

holiday++;

As opposed to

holiday.Increment();

Likewise implementing the operator < would allow us to compare two instances of Date:

if (date1 < date2) {
    // ...
} else {
    // ...
}

Types of Operators

Operators in C++ can be classified into two types, unary operators and binary operators.

Unary Operators

Unary operators are operators that function on a single operand.

A unary operator can be either declared

  • in the global namespace

  • as a static member function

return_type operator operator_type (parameter_type);
  • as a non-static member function

return_type operator operator_type ();

It is similar to the above, but without the parameter, because the parameter that is being operation on is the instance of the class itself, referred to by the this pointer.

Types of Unary Operators

Operator

Name

++

Increment

--

Decrement

*

Pointer dereference

->

Member selection

!

Logical NOT

&

Address-of

~

One's complement

+

Unary plus

-

Unary negation

Convertion operators

Implicit conversion to other types

Example - Programming a Unary Increment / Decrement Operator

Why does the postfix increment/decrement operator have an int parameter?

Is it purely to distinguish between the prefix and postfix operators.

These explanations are needed because the mechanism is unique, and therefore a bit of a wart. Given a choice I would probably have introduced the prefix and postfix keywords, but that didn't appear feasible at the time.

Conversion Operators

Consider the below usage of the Date class declared above:

std::cout << holiday << std::endl;

Compiling the above code would yield a bunch of compiler errors that look like the following:

note: candidate function template not viable: no known conversion from 
'Date' to 'char' for 2nd argument

The above error indicates that the compiler is trying to convert the type that we provided, Date, to a char, which is a type that std::cout accepts.

We receive multiple of these lines, each of which is an attempt by the compiler to convert Date into another type that is accepted by std::cout.

Eventually it finds that it is not able to convert Date into any possible type, and returns to us a compiler error.

We know that std::cout can accept and print a const char* type:

std::cout << "Hello world!\n";  // const char* works!

So getting std::cout to work with an instance of Date is simply adding an operator that can implicitly convert a Date to a const char*:

operator const char*() {
    // ... 
}

Now the instance of Date can be used with std::cout, taking advantage of the fact that std::cout can accept const char* types. The compiler will try to find a suitable conversion operator that returns a type that can be used with std::cout, and implicitly invokes it for you.

Explicit Conversion

To avoid implicit conversions, use the keyword explicit at the start of the operator declaration:

explicit operator const char*() {
    // ...
}

This forces the programmer to assert the intention to convert using a cast:

std::cout << static_cast<const char*>(holiday) << std::endl;

Example - Programming Dereference Operator (*) and Member Selection Operator (->)

The secret of std::unique_ptr is that it implements operator* and operator-> to do clever things.

Binary Operators

Binary operators are operators that function on two operands.

A binary operator can be declared as

  • a global function

  • a static member function

return_type operator operator_type (parameter1, parameter2);
  • a class member function

return_type operator operator_type (parameter);

The reason the class member function version only accepts one parameter is that the second parameter is the instance of the class itself, accessed through the this pointer.

Types of Binary Operators

Operator

Name

,

Comma

!=

Inequality

%

Modulus

%=

Modulus assignment

&

Bitwise AND

&&

Logical AND

&=

Bitwise AND assignment

*

Multiplication

*=

Multiplication assignment

+

Addition

+=

Addition assignment

-

Subtraction

-=

Subtraction assignment

->*

Pointer-to-member selection

/

Division

/=

Division assignment

<

Less than

<<

Left shift

<<=

Left shift assignment

<=

Less than or equal to

=

Assignment / copy assignment / move assignment

==

Equal to

>

Greater than

>=

Greater than or equal to

>>

Right shift

>>=

Right shift assignment

^

Exclusive OR

^=

Exclusive OR assignment

|

Bitwise inclusive OR

|=

Bitwise inclusive OR assignment

||

Logical OR

[]

Subscript operator

Example - Programming Binary Addition and Subtraction Operators

Example - Programming Addition Assignment and Subtraction Assignment Operators

Example - Programming Equality and Inequality Operators

In the absence of an equality operator ==, the compiler simply performs a binary comparison of the two objects and returns true when they are exactly identical.

This binary comparison will work for instances of classes containing simple data types like the Date class, but it will not work if the class in question has a non-static string member such as char*.

When two instances of such classes are compared, a binary comparison of the member attributes would compare the pointer values of the char* member variables. These would not be equal even if the values the pointers point to are equal, and would therefore return false consistently.

This problem can be solved by defining comparison operators.

bool operator==(const ClassType& other) {
    // ...
}

bool operator!=(const ClassType& other) {
    // ...
}

(Bonus: The other comparison operators - <, >, <=, and >= are implemented the same way; have a go at implementing them)

Subscript Operator

The subscript operator is one that allows an array-style ([]) access to a class. The syntax for a subscript operator is:

return_type& operator[](subscript_type& index);

Using the VectorInt class from Question 1 of Worked Exercises 1 as an example, a subscript operator would make it really easy to access individual elements.

class VectorInt {
    int* values;
    
    // ...
public:
    int& operator[](int index) {
        return values[index];
    }
};

int main() {
    auto ints = VectorInt();
    // add some elements to it 
    for (int i = 0; i < ints.size(); i++) {
        std::cout << ints[i] << std::endl;
    }
}

The subscript operator now helps us iterate through the contents contained in a VectorInt object using normal array semantics.

Because the value returned is a reference, we can also modify the values returned, and it will be reflected in the VectorInt object.

The const keyword can change the behaviour of the subscript operator. For example, returning a const reference:

const int& operator[](int index);

You prevent one from modifying the values directly via operator[].

Likewise defining the function as const denotes that it actually does not modify any member attributes:

const int& operator[](int index) const;

Furthermore, you are allowed to define both subscript operators, and the compiler will invoke the const function for reading, and the non-const version for writing.

Function Operator

The operator() makes objects behave like a function. Their usage can include parameters, giving them the name unary or binary predicates, depending on the number of operands they work on.

Objects who implement an operator() overload are also called function objects or functors (portmanteau of 'function pointers').

Move Constructor & Move Assignment Operator

Move constructors and move assignment operators are performance optimisation features implemented as part of the C++11 standard.

These are similar to the copy constructor and copy assignment operator, except temporary values (rvalues that don't exist beyond the statement) are not wastefully copied.

This is especially useful for handling classes with dynamically allocated resources, such as a dynamic array or string class.

Motivation

Last updated

Was this helpful?