Source: LearnCpp.com by Alex

Basic Object-Oriented Programming

In C++, classes and structs are essentially the same. In fact, the following struct and class are effectively identical:

1
2
3
4
5
6
7
8
9
10
11
12
struct DateStruct {
int year;
int month;
int day;
};

class DateClass {
public:
int m_year;
int m_month;
int m_day;
}; // don't forget the semicolon!

Just like a struct declaration, a class declaration does not declare any memory. It only defines what the class looks like.

Using the m_ prefix for member variables helps distinguish member variables from function parameters or local variables inside member functions. This is useful for several reasons:

  • First, when we see an assignment to a variable with the “m_” prefix, we know that we are changing the state of the class.
  • Second, unlike function parameters or local variables, which are declared within the function, member variables are declared in the class definition. Consequently, if we want to know how a variable with the “m_” prefix is declared, we know that we should look in the class definition instead of within the function.

A note about structs in C++

Intro

In C, structs can only hold data, and do not have associated member functions. In C++, after designing classes (using the class keyword), Bjarne Stroustrup spent some amount of time considering whether structs (which were inherited from C) should be granted the ability to have member functions. Upon consideration, he determined that they should, in part to have a unified ruleset for both. So although we wrote the above programs using the class keyword, we could have used the struct keyword instead.

Many developers (including myself) feel this was the incorrect decision to be made, as it can lead to dangerous assumptions: For example, it’s fair to assume a class will clean up after itself (e.g. a class that allocates memory will deallocate it before being destroyed), but it’s not safe to assume a struct will. Consequently, we recommend using the struct keyword for data-only structures, and the class keyword for defining objects that require both data and functions to be bundled together.

Rule: Use the struct keyword for data-only structures. Use the class keyword for objects that have both data and functions.

Public vs private access specifiers

Members are public by default.

1
2
3
4
5
6
7
8
9
10
struct Date {
int month; // public by default, can be accessed by anyone
int day; // public by default, can be accessed by anyone
int year; // public by default, can be accessed by anyone
};
class Date { // members are private by default
int m_month; // private by default, can only be accessed by other members
int m_day; // private by default, can only be accessed by other members
int m_year; // private by default, can only be accessed by other members
};

Classes can (and almost always do) use multiple access specifiers to set the access levels of each of its members. There is no limit to the number of access specifiers you can use in a class.

In general, member variables are usually made private, and member functions are usually made public.

Some programmers prefer to list private members first, because the public members typically use the private ones, so it makes sense to define the private ones first. However, a good counterargument is that users of the class don’t care about the private members, so the public ones should come first. Either way is fine.

Access functions and encapsulation

Encapsulation

In object-oriented programming, Encapsulation (also called information hiding) is the process of keeping the details about how an object is implemented hidden away from users of the object. Instead, users of the object access the object through a public interface. In this way, users are able to use the object without having to understand how it is implemented.

In C++, we implement encapsulation via access specifiers. Typically, all member variables of the class are made private (hiding the implementation details), and most member functions are made public (exposing an interface for the user). Although requiring users of the class to use the public interface may seem more burdensome than providing public access to the member variables directly, doing so actually provides a large number of useful benefits that help encourage class re-usability and maintainability.

Note: The word encapsulation is also sometimes used to refer to the packaging of data and functions that work on that data together. We prefer to just call that object-oriented programming.

  • Rule: Only provide access functions when it makes sense for the user to be able to get or set a value directly.
  • Rule: Getters should usually return by value or const reference, not non-const reference.

Constructors

Unlike normal member functions, constructors have specific rules for how they must be named:

  • Constructors must have the same name as the class (with the same capitalization)
  • Constructors have no return type (not even void)

Remember: Fundamental variables aren’t initialized by default. they should be initialized in constructors.

1
2
3
4
5
6
7
8
Fraction(int, numerator, int denominator=1) {
m_numerator = numerator;
m_denominator = denominator;
}
int x(5);
Fraction fiveThirds1(5, 3);
Fraction fiveThirds2 {5, 3};
Fraction fiveThirds3(5);

Copy initialization using equals with classes

Much like with fundamental variables, it’s also possible to initialize classes using copy initialization:

1
2
3
4
int x = 6; // Copy initialize an integer
Fraction six = Fraction(6); // Copy initialize a Fraction, will call Fraction(6, 1)
Fraction seven = 7; // Copy initialize a Fraction.
// The compiler will try to find a way to convert 7 to a Fraction, which will invoke the Fraction(7, 1) constructor.

However, we recommend you avoid this form of initialization with classes, as it may be less efficient. Although direct initialization, uniform initialization, and copy initialization all work identically with fundamental types, copy-initialization does not work the same with classes (though the end-result is often the same). We’ll explore the differences in more detail in a future chapter.

Rule: Do not use copy initialization for your classes.

Reducing your constructors

In the above two-constructor declaration of the Fraction class, the default constructor is actually somewhat redundant. We could simplify this class as follows:

1
2
3
4
5
public:
// Default
Fraction(int numerator=0, int denominator=1) {
// foo
}

Although this constructor is still a default constructor, it has now been defined in a way that it can accept one or two user-provided values as well.

1
2
3
Fraction zero;   // will call Fraction(0, 1)
Fraction six(6); // will call Fraction(6, 1)
Fraction fiveThirds(5, 3); // will call Fraction(5, 3)

This may produce unexpected results for classes that have multiple default parameters of different types. Consider:

1
2
3
4
5
6
7
Something(int n = 0, double d = 1.2) { // allows us to construct a Something(int, double), Something(int), or Something()
// foo
}
// main
Something s1 { 1 };
Something s2;
Something s3 { 2.4 }; // will not compile, as there's no constructor to handle Something(double)

If we want to be able to construct a Something with only a double, we’ll need to add a second (non-default) constructor:

1
2
3
4
5
6
7
8
9
10
11
// Default constructor
Something(int n = 0, double d = 1.2) {
// allows us to construct:
// Something(int, double)
// Something(int)
// Something()
}
// non-default constructor
Something(double d) {
// foo
}

An implicitly generated default constructor

If your class has no other constructors, C++ will automatically generate a public default constructor for you. This is sometimes called an implicit constructor (or implicitly generated constructor).

If your class has any other constructors, the implicitly generated constructor will not be provided. For example:

1
2
3
4
5
6
public:
Date(int year, int month, int day) {
// foo
}
// main
Date data; // error: Can't instantiate object because default constructor doesn't exist and the compiler won't generate one.

Generally speaking, it’s a good idea to always provide at least one constructor in your class. This explicitly allows you to control how objects of your class are allowed to be created, and will prevent your class from potentially breaking later when you add other constructors.

Rule: Provide at least one constructor for your class, even if it’s an empty default constructor.

Many new programmers are confused about whether constructors create the objects or not. They do not (the code the compiler creates does that).

Constructors actually serve two purposes. The primary purpose is to initialize objects that have just been created. The secondary purpose is to determine whether creation of an object is allowed. That is, an object of a class can only be created if a matching constructor can be found. This means that a class without any public constructors can’t be created!

Constructors are only intended to be used for initialization when the object is created. You should not try to call a constructor to re-initialize an existing object. While it may compile, the results will not be what you intended (instead, the compiler will create a temporary object and then discard it).

Some types of data (e.g. const and reference variables) must be initialized on the line they are declared. Consider the following example:

1
2
3
4
5
6
7
8
9
class Something {
private:
const int m_value;

public:
Something() {
m_value = 1; // error: const vars cannot be assigned to
}
};

Member initializer lists

To solve this problem, C++ provides a method for initializing class member variables (rather than assigning values to them after they are created) via a member initializer list (often called a “member initialization list”). Do not confuse these with the similarly named initializer list that we can use to assign values to arrays.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Something {
private:
int m_value1;
double m_value2;
char m_value3;

public:
Something() : m_value1(1), m_value2(2.2), m_value3('c') {
// directly initialize our member variables
// No need for assignment here
}
};
// main
Something something;

// or more useful
Something(int value1, double value2, char value3='c')
: m_value1(value1), m_value2(value2), m_value3(value3) {
// directly initialize our member variables
// No need for assignment here
}
// main
Something something(1, 2.2);
// value = 1, value2 = 2.2, value3 gets default value 'c'

Const problem solved:

1
2
3
4
5
6
7
8
9
class Something {
private:
const int m_value;

public:
Something(): m_value(5) {
// directly initialize our const member variable
}
};

Rule: Use member initializer lists to initialize your class member variables instead of assignment.

Or even in C++11, we can use m_value {5}.

We strongly encourage you to begin using this new syntax (even if you aren’t using const or reference member variables) as initialization lists are required when doing composition and inheritance (subjects we will be covering shortly).

Rule: Favor uniform initialization over direct initialization if your compiler is C++11 compatible

Prior to C++11, you can only zero an array member via a member initialization list:

1
2
3
4
5
6
7
8
9
10
class Something {
private:
const int m_array[5];

public:
Something(): m_array {} {
// zero the member array
// If we want the array to have values, we'll have to use assignment here
}
};

However, in C++11, you can fully initialize a member array using uniform initialization:

1
2
3
4
5
6
7
8
9
class Something {
private:
const int m_array[5];

public:
Something() : m_array { 1, 2, 3, 4, 5 } {
// use uniform initialization to initialize our member array
}
};

A member initialization list can also be used to initialize members that are classes.

1
2
3
4
5
class B {
B(int y) : m_a(y-1) {
// call A(int) constructor to initialize member m_a
}
}

Formatting your initializer lists

C++ gives you a lot of flexibility in how to format your initializer lists, and it’s really up to you how you’d like to proceed. But here are some recommendations:

If the initializer list fits on the same line as the function name, then it’s fine to put everything on one line:

1
2
3
Something() : m_value1(1), m_value2(2.2), m_value3('c') {
// everything on one line
}

If the initializer list doesn’t fit on the same line as the function name, then it should go indented on the next line.

1
2
3
4
5
6
7
8
9
Something(int value1, double value2, char value3='c') { // this line already has a lot of stuff on it
: m_value1(value1), m_value2(value2), m_value3(value3)
// so we can put everything indented on next line
// or
// :m_value1(value1),
// m_value2(value2),
// m_value3(value3),
// m_value4(value4)
}

Initializer list order

Perhaps surprisingly, variables in the initializer list are not initialized in the order that they are specified in the initializer list. Instead, they are initialized in the order in which they are declared in the class.

For best results, the following recommendations should be observed:

  • Don’t initialize member variables in such a way that they are dependent upon other member variables being initialized first. In other words, ensure your member variables will properly initialize even if the initialization ordering is different).
  • Initialize variables in the initializer list in the same order in which they are declared in your class. This isn’t strictly required so long as the prior recommendation has been followed, but your compiler may give you a warning if you don’t do so and you have all warnings turned on.

Non-static member initialization

Starting with C++11, it’s possible to give normal class member variables (those that don’t use the static keyword) a default initialization value directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Rectangle {
private:
double m_length = 1.0; // m_length has a default value of 1.0
double m_width = 1.0; // m_width has a default value of 1.0

public:
Rectangle() {
// This constructor will use the default values above since they aren't overridden here
}
Rectangle(double length, double width)
: m_length(length), m_width(width) {
// m_length and m_width are initialized by the constructor (the default values aren't used)
}
};

Non-static member initialization (also called in-class member initializers) provides default values for your member variables that your constructors will use if the constructors do not provide initialization values for the members themselves (via the member initialization list).

Overlapping and delegating constructors

Overlapping functionality problem:

1
2
3
4
5
6
7
8
9
10
class Foo {
public:
Foo() {
// code to do A
}
Foo(int value) {
// code to do A
// code to do B
}
}

The obvious solution doesn’t work prior to C++11

The obvious solution would be to have the Foo(int) constructor call the Foo() constructor to do the A portion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo {
public:
Foo() {
// code to do A
}
Foo(int value) {
Foo(); // use the above constructor to do A (doesn't work)
// code to do B
}
// or
Foo(int value) : Foo() {
// code to do B
}
}

However, with a pre-C++11 compiler, if you try to have one constructor call another constructor, it will often compile, but it will not work as you expect, and you will likely spend a long time trying to figure out why, even with a debugger.

Note: Prior to C++11, calling a constructor explicitly from another constructor creates a temporary object, initializes the temporary object using the constructor, and then discards it, leaving your original object unchanged.

Using a separate function

1
2
3
4
5
6
7
8
9
10
Foo() {
Init();
}
Foo(int value) {
Init();
// do something with value
}
void Init() {
// code to init Foo
}

One small caveat: be careful when using Init() functions and dynamically allocated memory. Because Init() functions can be called by anyone at any time, dynamically allocated memory may or may not have already been allocated when Init() is called. Be careful to handle this situation appropriately – it can be slightly confusing, since a non-null pointer could be either dynamically allocated memory or an uninitialized pointer!

Delegating constructors in C++11

Starting with C++11, constructors are now allowed to call other constructors. This process is called delegating constructors (or constructor chaining).

1
2
3
4
5
6
7
8
9
10
11
class Foo {
public:
Foo() {
// code to do A
}

Foo(int value) : Foo() {
// use Foo() default constructor to do A
// code to do B
}
};

This works exactly as you’d expect. Make sure you’re calling the constructor from the member initializer list, not in the body of the constructor.

Here’s another example of using delegating constructor to reduce redundant code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <string>
#include <iostream>

class Employee
{
private:
int m_id;
std::string m_name;

public:
// delegated constructor
Employee(int id=0, const std::string &name="")
: m_id(id), m_name(name) {
std::cout << "Employee " << m_name << " created.\n";
}
// Use a delegating constructor to minimize redundant code
Employee(const std::string &name)
: Employee(0, name) {
// foo
}
};

Destructors

If your class object is holding any resources (e.g. dynamic memory, or a file or database handle), or if you need to do any kind of maintenance before the object is destroyed, the destructor is the perfect place to do so, as it is typically the last thing to happen before the object is destroyed.

Like constructors, destructors have specific naming rules:

  1. The destructor must have the same name as the class, preceded by a tilde (~).
  2. The destructor cannot take arguments.
  3. The destructor has no return type.

Generally you should not call a destructor explicitly (as it will be called automatically when the object is destroyed), since there are rarely cases where you’d want to clean up an object more than once. However, destructors may safely call other member functions since the object isn’t destroyed until after the destructor executes.

RAII (Resource Acquisition Is Initialization) is a programming technique whereby resource use is tied to the lifetime of objects with automatic duration (e.g. non-dynamically allocated objects). In C++, RAII is implemented via classes with constructors and destructors. A resource (such as memory, a file or database handle, etc…) is typically acquired in the object’s constructor (though it can be acquired after the object is created if that makes sense). That resource can then be used while the object is alive. The resource is released in the destructor, when the object is destroyed. The primary advantage of RAII is that it helps prevent resource leaks (e.g. memory not being deallocated) as all resource-holding objects are cleaned up automatically.

Under the RAII paradigm, objects holding resources should not be dynamically allocated. This is because destructors are only called when an object is destroyed. For objects allocated on the stack, this happens automatically when the object goes out of scope, so there’s no need to worry about a resource eventually getting cleaned up. However, for dynamically allocated objects, the user is responsible for deletion – if the user forgets to do that, then the destructor will not be called, and the memory for both the class object and the resource being managed will be leaked!

The IntArray class at the top of this lesson is an example of a class that implements RAII – allocation in the constructor, deallocation in the destructor. std::string and std::vector are examples of classes in the standard library that follow RAII – dynamic memory is acquired on initialization, and cleaned up automatically on destruction.

Rule: If your class dynamically allocates memory, use the RAII paradigm, and don’t allocate objects of your class dynamically.

A warning about the exit() function

Note that if you use the exit() function, your program will terminate and no destructors will be called. Be wary if you’re relying on your destructors to do necessary cleanup work (e.g. write something to a log file or database before exiting).

The hidden “this” pointer

1
2
3
4
5
6
7
8
9
void setID(int id) { m_id = id; }

// main
Simple simple(1);
simple.setID(2);
// compiler convert it to: setID(&simple, 2);
// void setID(Simple* const this, int id) {
// this->m_id = id;
// }

Putting it all together:

  1. When we call simple.setID(2), the compiler actually calls setID(&simple, 2).
  2. Inside setID(), the “this” pointer holds the address of object simple.
  3. Any member variables inside setID() are prefixed with “this->”. So when we say m_id = id, the compiler is actually executing this->m_id = id, which in this case updates simple.m_id to id.

Although using this->xxx is acceptable coding practice, we find using the “m_” prefix on all member variable names provides a better solution by preventing duplicate names altogether!

Some developers prefer to explicitly add this-> to all class members. We recommend that you avoid doing so, as it tends to make your code less readable for little benefit. Using the m_ prefix is a more readable way to differentiate member variables from non-member (local) variables.

Chaining member functions

1
std::cout << "Hello, " << userName;

In this case, std::cout is an object, and operator << is a member function that operates on that object. The compiler evaluates the above snippet like this:

1
2
3
(std::cout << "Hello, ") << userName;
// -> (std::cout) << userName;
// operator << returns *this, which in this context is just std::cout

Here is a version of Calc with “chainable” functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Calc {
private:
int m_value;
public:
Calc() { m_value = 0; }

Calc& add(int value) { m_value += value; return *this; }
Calc& sub(int value) { m_value -= value; return *this; }
Calc& mult(int value) { m_value *= value; return *this; }

int getValue() { return m_value; }
};
// main
Calc calc;
calc.add(5).sub(3).mult(4);

Class code and header files

Defining member functions outside the class definition

C++ provides a way to separate the “declaration” portion of the class from the “implementation” portion. This is done by defining the class member functions outside of the class definition. To do so, simply define the member functions of the class as if they were normal functions, but prefix the class name to the function using the scope resolution operator (::) (same as for a namespace).

Here is our Date class with the Date constructor and setDate() function defined outside of the class definition. Note that the prototypes for these functions still exist inside the class definition, but the actual implementation has been moved outside:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Calc {
private:
int m_value = 0;
public:
Calc(int value=0);

Calc& add(int value);
Calc& sub(int value);
Calc& mult(int value);

int getValue() { return m_value; }
};

Calc::Calc(int value) : m_value(value) {
// foo
}

Calc& Calc::add(int value) {
m_value += value;
return *this;
}

// ...

Putting class definitions in a header file

Date.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef DATE_H
#define DATE_H

class Date {
private:
int m_year;
// ...
public:
Date(int year, int month, int day);
// ...
};

#endif

Date.cpp:

1
2
3
4
5
6
7
8
9
10
11
#include "Date.h"

Date::Date(int year, int month, int day) {
SetDate(year, month, day);
}

void Date::SetDate(int year, int month, int day) {
m_month = month;
m_day = day;
m_year = year;
}

Doesn’t defining member functions in the header violate the one-definition rule?

It depends. Member functions defined inside the class definition are considered implicitly inline. Inline functions are exempt from the one definition per program part of the one-definition rule. This means there is no problem defining trivial member functions (such as access functions) inside the class definition itself.

Member functions defined outside the class definition are treated like normal functions, and are subject to the one definition per program part of the one-definition rule. Therefore, those functions should be defined in a code file, not inside the header. The one exception for this is for template functions, which we’ll cover in a future chapter.

So what should I define in the .h file vs the .cpp file, and what inside the class definition vs outside?

You might be tempted to put all of your member function definitions into the header file, inside the class. While this will compile, there are a couple of downsides to doing so.

  • First, as mentioned above, this clutters up your class definition.
  • Second, functions defined inside the class are implicitly inline. For larger functions that are called from many places, this can bloat your code.
  • Third, if you change anything about the code in the header, then you’ll need to recompile every file that includes that header. This can have a ripple effect, where one minor change causes the entire program to need to recompile (which can be slow). If you change the code in a .cpp file, only that .cpp file needs to be recompiled!

Therefore, we recommend the following:

  • For classes that are used in only one file and aren’t generally reusable, define them directly in the single .cpp file they’re used in.
  • For classes used in multiple files, or intended for general reuse, define them in a .h file that has the same name as the class.
  • Trivial member functions (trivial constructors or destructors, access functions, etc…) can be defined inside the class.
  • Non-trivial member functions should be defined in a .cpp file that has the same name as the class.

Default parameters

Default parameters for member functions should be declared in the class definition (in the header file), where they can be seen by whomever #includes the header.

Libraries

Separating the class definition and class implementation is very common for libraries that you can use to extend your program. Throughout your programs, you’ve #included headers that belong to the standard library, such as iostream, string, vector, array, and other. Notice that you haven’t needed to add iostream.cpp, string.cpp, vector.cpp, or array.cpp into your projects. Your program needs the declarations from the header files in order for the compiler to validate you’re writing programs that are syntactically correct. However, the implementations for the classes that belong to the C++ standard library is contained in a precompiled file that is linked in at the link stage. You never see the code.

Outside of some open source software (where both .h and .cpp files are provided), most 3rd party libraries provide only header files, along with a precompiled library file. There are several reasons for this:

  1. It’s faster to link a precompiled library than to recompile it every time you need it.
  2. A single copy of a precompiled library can be shared by many applications, whereas compiled code gets compiled into executable copies that uses it (inflating file sizes).
  3. Intellectual property reasons (you don’t want people stealing your code).

Having your own files separated into declaration (header) and implementation (code file) is not only good form, it also makes creating your own custom libraries easier. Creating your own libraries is beyond the scope of these tutorials, but separating your declaration and implementation is a prerequisite to doing so.

Const class objects and member functions

1
2
3
const Date date1; // initialize using default constructor
const Date date2(2020, 10, 16); // initialize using parameterized constructor
const Date date3 { 2020, 10, 16 }; // initialize using parameterized constructor (C++11)

Once a const class object has been initialized via constructor, any attempt to modify the member variables of the object is disallowed, as it would violate the const-ness of the object. This includes both changing member variables directly (if they are public), or calling member functions that set the value of member variables. Consider the following class:

1
2
3
const Something something;
something.m_value = 5; // Not allowed
something.setValue(5); // Not allowed

Now, consider the following line of code:

1
std::cout << something.getValue();

Perhaps surprisingly, this will also cause a compile error, even though getValue() doesn’t do anything to change a member variable! It turns out that const class objects can only explicitly call const member functions, and getValue() has not been marked as a const member function.

A const member function is a member function that guarantees it will not modify the object or call any non-const member functions (as they may modify the object).

To make getValue() a const member function, we simply append the const keyword to the function prototype, after the parameter list, but before the function body:

1
2
3
public:
int getValue() const { return m_value; }
// note addition of const keyword after parameter list, but before function body

Futhermore, any const member function that attempts to change a member variable or call a non-const member function will cause a compiler error to occur.

Note that constructors cannot be marked as const. This is because constructors need to be able to initialize their member variables, and a const constructor would not be able to do so. Consequently, the language disallows const constructors.

Const references

Although instantiating const class objects is one way to create const objects, a more common way is by passing an object to a function by const reference.

We covered the merits of passing class arguments by const reference instead of by value. To recap, passing a class argument by value causes a copy of the class to be made (which is slow) – most of the time, we don’t need a copy, a reference to the original argument works just fine, and is more performant because it avoids the needless copy. We typically make the reference const in order to ensure the function does not inadvertently change the argument, and to allow the function to work with R-values (e.g. literals), which can be passed as const references, but not non-const references.

1
2
3
4
5
6
7
8
void printDate(const Date &date) {
std::cout << date.getYear() << "/" << date.getMonth() << "/" << date.getDay() << '\n';
}

// const is necessary; otherwise it will cause a compile error
int getYear() const { return m_year; }
int getMonth() const { return m_month; }
int getDay() const { return m_day; }

Overloading const and non-const function

Finally, although it is not done very often, it is possible to overload a function in such a way to have a const and non-const version of the same function:

1
2
3
4
5
6
7
8
9
10
11
12
class Something {
private:
std::string m_value;

public:
Something(const std::string &value="") { m_value = value; }

const std::string& getValue() const { return m_value; }
// getValue() for const objects
std::string& getValue() { return m_value; }
// getValue() for non-const objects
};
  • The non-const version of getValue() will only work with non-const objects, but is more flexible in that we can use it to both read and write m_value (which we do by assigning the string “Hi”).

  • The const version of getValue() will work with either const or non-const objects, but returns a const reference, to ensure we can’t modify the const object’s data.

Because passing objects by const reference is common, your classes should be const-friendly. That means making any member function that does not modify the state of the class object const!

Static member variables

C++ introduces two more uses for the static keyword when applied to classes: static member variables, and static member functions. Fortunately, these uses are fairly straightforward. We’ll talk about static member variables in this lesson, and static member functions in the next.

Member variables of a class can be made static by using the static keyword. Unlike normal member variables, static member variables are shared by all objects of the class. Consider the following program, similar to the above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Something {
public:
static int s_value;
// Note that the static member definition is not subject to access controls: you can define and initialize the value even if it's declared as private (or protected) in the class
};

int Something::s_value = 1;

int main() {
Something first;
Something second;
first.s_value = 2;

std::cout << first.s_value << '\n';
std::cout << second.s_value << '\n';
// but we'd better use Something::s_value!

return 0;
}

If the class is defined in a .h file, the static member definition is usually placed in the associated code file for the class (e.g. Something.cpp). If the class is defined in a .cpp file, the static member definition is usually placed directly underneath the class. Do not put the static member definition in a header file (much like a global variable, if that header file gets included more than once, you’ll end up with multiple definitions, which will cause a compile error).

Static member functions

What if static member variables are declared private:

1
2
3
4
5
6
7
8
9
10
11
class Something {
private:
static int s_value;
};

int Something::s_value = 1;
// initializer, this is okay even though s_value is private since it's a definition

int main() {
// Something::s_value can't be accessed
}

Normally we access private members through public member functions. While we could create a normal public member function to access s_value, we’d then need to instantiate an object of the class type to use the function! We can do better. It turns out that we can also make functions static.

1
2
public:
static int getValue() { return s_value; }

*Static member functions have no this pointer

C++ does not support static constructors

If initializing your static member variable requires executing code (e.g. a loop), there are many different, somewhat obtuse ways of doing this. The following code presents one of the better methods. However, it is a little tricky, and you’ll probably never need it, so feel free to skip the remainder of this section if you desire.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
private:
static std::vector<char> s_mychars;
public:
class _init {
// we're defining a nested class named _init
public:
_init() {
s_mychars.push_back('a');
s_mychars.push_back('e');
}
};
private:
static _init s_initializer;
// we'll use this static object to ensure the _init constructor is called
};

std::vector<char> MyClass::s_mychars;
// define our static member variable
MyClass::_init MyClass::s_initializer;
// define our static initializer, which will call the _init constructor, which will initialize s_mychars

Friend functions and classes

A friend function is a function that can access the private members of a class as though it were a member of that class. In all other regards, the friend function is just like a normal function. A friend function may be either a normal function, or a member function of another class. To declare a friend function, simply use the friend keyword in front of the prototype of the function you wish to be a friend of the class. It does not matter whether you declare the friend function in the private or public section of the class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Accumulator {
private:
int m_value;
public:
Accumulator() { m_value = 0; }
void add(int value) { m_value += value; }
// Make the reset() function a friend of this class
friend void reset(Accumulator &accumulator);
};

// reset() is now a friend of the Accumulator class
void reset(Accumulator &accumulator) {
// And can access the private data of Accumulator objects
accumulator.m_value = 0;
}

int main() {
Accumulator acc;
acc.add(5); // add 5 to the accumulator
reset(acc); // reset the accumulator to 0

return 0;
}

Note that we have to pass an Accumulator object to reset(). This is because reset() is not a member function. It does not have a *this pointer, nor does it have an Accumulator object to work with, unless given one.

Note: A function can be a friend of more than one class at the same time.

It is also possible to make an entire class a friend of another class. This gives all of the members of the friend class access to the private members of the other class. Here is an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Storage {
private:
int m_nValue;
double m_dValue;
public:
Storage(int nValue, double dValue) {
m_nValue = nValue;
m_dValue = dValue;
}
// Make the Display class a friend of Storage
friend class Display;
};

class Display {
private:
bool m_displayIntFirst;

public:
Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }

void displayItem(Storage &storage) {
if (m_displayIntFirst)
std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
else // display double first
std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
};

Friend member functions

Instead of making an entire class a friend, you can make a single member function a friend. This is done similarly to making a normal function a friend, except using the name of the member function with the className:: prefix included (e.g. Display::displayItem).

However, in actuality, this can be a little trickier than expected. Let’s convert the previous example to make Display::displayItem a friend member function. You might try something like this:

Note: But it is not a member function! (member func -> a friend)

1
2
3
4
5
6
7
8
9
10
11
class Storage {
private:
// foo
public:
// foo
friend void Display::displayItem(Storage& storage);
// error: Storage hasn't seen the full definition of class Display
};
class Display {
// foo
};

However, it turns out this won’t work. In order to make a class’s member function a friend, the compiler has to have seen the full definition for the class of the friend member function (not just a forward declaration). Since class Storage hasn’t seen the full definition for class Display yet, the compiler will error at the point where we try to make the member function a friend.

Fortunately, this is easily resolved simply by moving the definition of class Display before the definition of class Storage.

1
2
3
4
5
6
7
8
9
// forward declaration
// class Storage; // forward declaration for Storage
class Display {
// foo
// if displayItem() here uses Storage as a reference, we can add forward declaration before "class Display { ... };"
};
class Storage {
// goo
};

If this seems like a pain – it is. Fortunately, this dance is only necessary because we’re trying to do everything in a single file. A better solution is to put each class definition in a separate header file, with the member function definitions in corresponding .cpp files. That way, all of the class definitions would have been visible immediately in the .cpp files, and no rearranging of classes or functions is necessary!

Summary

A friend function or class is a function or class that can access the private members of another class as though it were a member of that class. This allows the friend or class to work intimately with the other class, without making the other class expose its private members (e.g. via access functions).

Friending is uncommonly used when two or more classes need to work together in an intimate way, or much more commonly, when defining overloading operators (which we’ll cover in chapter 9).

Note that making a specific member function a friend requires the full definition for the class of the member function to have been seen first.

Anonymous objects

Although our prior examples have been with built-in data types, it is possible to construct anonymous objects of our own class types as well. This is done by creating objects like normal, but omitting the variable name.

1
2
Cents cents(5);
Cents(7); // anonymous object

More specifically,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Cents {
private:
int m_cents;
public:
Cents(int cents) { m_cents = cents; }
int getCents() const { return m_cents; }
}; // end of the class

void print(const Cents &cents) {
std::cout << cents.getCents() << " cents" << "\n";
}

Cents add(const Cents &c1, const Cents &c2) {
return Cents(c1.getCents() + c2.getCents());
// return anonymous Cents value
}

int main() {
print(Cents(6));
std::cout << add(Cents(6), Cents(8)).getCents() << "\n";
return 0;
}

It is worth noting that anonymous objects are treated as rvalues (not lvalues, which have an address). This means anonymous objects can only be passed or returned by value or const reference. Otherwise, a named variable must be used instead.

Nested types in classes

Unlike functions, which can’t be nested inside each other, in C++, types can be defined (nested) inside of a class. To do this, you simply define the type inside the class, under the appropriate access specifier.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Fruit {
public:
// Note: we've moved FruitType inside the class
enum FruitType {
APPLE,
BANANA,
CHERRY
};
};

int main() {
// Here, we can access the enum by Fruit::FruitType::APPLE
}

Classes essentially act as a namespace for any nested types. In the prior example, we were able to access enumerator APPLE directly, because the APPLE enumerator was placed into the global scope (we could have prevented this by using an enum class instead of an enum, in which case we’d have accessed APPLE via Fruit::FruitType::APPLE instead). Now, because FruitType is considered to be part of the class, we access the APPLE enumerator by prefixing it with the class name: Fruit::APPLE.

Note that because enum classes also act like namespaces, if we’d nested FruitType inside Fruit as an enum class instead of an enum, we’d access the APPLE enumerator via Fruit::FruitType::APPLE.

Although enumerations are probably the most common type that is nested inside a class, C++ will let you define other types within a class, such as typedefs, type aliases, and even other classes!

Like any normal member of a class, nested classes have the same access to members of the enclosing class that the enclosing class does. However, the nested class does not have any special access to the “this” pointer of the enclosing class.

Defining nested classes isn’t very common, but the C++ standard library does do so in some cases, such as with iterator classes.

Timing your code (Timing API)

One easy way is to time your code to see how long it takes to run. C++11 comes with some functionality in the chrono library to do just that. However, using the chrono library is a bit arcane. The good news is that we can easily encapsulate all the timing functionality we need into a class that we can then use in our own programs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <chrono>

class Timer {
private:
// Type aliases to make accessing nested type easier
using clock_t = std::chrono::high_resolution_clock;
using second_t = std::chrono::duration<double, std::ratio<1> >;

std::chrono::time_point<clock_t> m_beg;

public:
Timer() : m_beg(clock_t::now()) {}
void reset() {
m_beg = clock_t::now();
}
double elapsed() const {
return std::chrono::duration_cast<second_t>(clock_t::now() - m_beg).count();
}
};

const int g_arrayElements = 10000;

// usage
int main() {
std::array<int, g_arrayElements> array;
for (int i = 0; i < g_arrayElements; ++i)
array[i] = g_arrayElements - i;

Timer t;

std::sort(array.begin(), array.end());

std::cout << "Time taken: " << t.elapsed() << " seconds\n";
}

Timing is straightforward, but your results can be significantly impacted by a number of things, and it’s important to be aware of what those things are.

  1. Make sure you’re using a release build target, not a debug build target. Debug build targets typically turn optimization off, and that optimization can have a significant impact on the results.
  2. For best results, make sure your system isn’t doing anything CPU or memory intensive.
  3. When doing comparisons between two sets of code, be wary of what may change between runs that could impact timing. Your system may have kicked off an antivirus scan in the background, or maybe you’re streaming music now when you weren’t previously. Randomization can also impact timing.

Operator Overloading

In C++, operators are implemented as functions. By using function overloading on the operator functions, you can define your own versions of the operators that work with different data types (including classes that you’ve written). Using function overloading to overload operators is called operator overloading.

1
2
x + y // translated as follow
operator+(x, y) // where operator+ is the name of the function

Resolving overloaded operators

When evaluating an expression containing an operator, the compiler uses the following rules:

  • If all of the operands are fundamental data types, the compiler will call a built-in routine if one exists. If one does not exist, the compiler will produce a compiler error.
  • If any of the operands are user data types (e.g. one of your classes, or an enum type), the compiler looks to see whether the type has a matching overloaded operator function that it can call. If it can’t find one, it will try to convert one or more of the user-defined type operands into fundamental data types so it can use a matching built-in operator (via an overloaded typecast, which we’ll cover later in this chapter). If that fails, then it will produce a compile error.

Note:

  • First, almost any existing operator in C++ can be overloaded. The exceptions are: conditional (?:), sizeof, scope (::), member selector (.), and member pointer selector (.*).

  • Second, you can only overload the operators that exist. You cannot create new operators or rename existing operators. For example, you could not create an operator ** to do exponents.

  • Third, at least one of the operands in an overloaded operator must be a user-defined type. This means you can not overload the plus operator (+) to work with one integer and one double. However, you could overload the plus operator to work with an integer and a MyString.

  • Fourth, it is not possible to change the number of operands an operator supports.

  • Finally, all operators keep their default precedence and associativity (regardless of what they’re used for) and this cannot be changed.

    • One example: Some new programmers attempt to overload the bitwise XOR operator (^) to do exponentiation. However, in C++, operator^ has a lower precedence level than the basic arithmetic operators, which causes expressions to evaluate incorrectly.

Rule: When overloading operators, it’s best to keep the function of the operators as close to the original intent of the operators as possible.

Rule: If the meaning of an operator when applied to a custom class is not clear and intuitive, use a named function instead.

Overload the arithmetic operators using friend functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// addCents example
class Cents {
private:
int m_cents;
public:
friend Cents operator+(const Cents &c1, const Cents &c2);
// if we declare it as a member function, we need an Cents object to use this function!
};

Cents operator+(const Cents &c1, const Cents &c2) {
// note: this function is not a member function!
// we can access m_cents directly because this is a friend function
return Cents(c1.m_cents + c2.m_cents);
}

Friend functions can be defined inside the class:

Even though friend functions are not members of the class, they can still be defined inside the class if desired:

1
2
3
4
5
public:
// but it is not a member function
friend Cents operator+(const Cents &c1, const Cents &c2) {
return Cents(c1.m_cents + c2.m_cents);
}

We generally don’t recommend this, as non-trivial (important) function definitions are better kept in a separate .cpp file, outside of the class definition. However, we will use this pattern in future tutorials to keep the examples concise.

Overloading operators for operands of different types

When C++ evaluates the expression x + y, x becomes the first parameter, and y becomes the second parameter. When x and y have the same type, it does not matter if you add x + y or y + x – either way, the same version of operator+ gets called. However, when the operands have different types, x + y does not call the same function as y + x.

For example, Cents(4) + 6 would call operator+(Cents, int), and 6 + Cents(4) would call operator+(int, Cents).

Therefore, we need to write two functions separately:

1
2
3
4
5
6
7
// add Cents + int using a friend function
friend Cents operator+(const Cents &c1, int value);

// add int + Cents using a friend function
friend Cents operator+(int value, const Cents &c1) {
return c1 + value;
}

It is often possible to define overloaded operators by calling other overloaded operators. You should do so if and when doing so produces simpler code.

Overload operators using normal functions

Using a friend function to overload an operator is convenient because it gives you direct access to the internal members of the classes you’re operating on. In the initial Cents example above, our friend function version of operator+ accessed member variable m_cents directly.

However, if you don’t need that access, you can write your overloaded operators as normal non-member functions. Note that the Cents class above contains an access function (getCents()) that allows us to get at m_cents without having to have direct access to private members. Because of this, we can write our overloaded operator+ as a non-friend:

One-file version:

1
2
3
4
5
6
// note: this function is not a member function nor a friend function!
Cents operator+(const Cents &c1, const Cents &c2) {
// use the Cents constructor and operator+(int, int)
// we don't need direct access to private members here
return Cents(c1.getCents() + c2.getCents());
}

Two-file version:

  • Cents.h:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Cents {
    private:
    int m_cents;
    public:
    Cents(int cents) { m_cents = cents; }
    int getCents() const { return m_cents; }
    };

    // Need to explicitly provide prototype for operator+ so uses of operator+ in other files know this overload exists
    Cents operator+(const Cents &c1, const Cents &c2);
  • Cents.cpp

    1
    2
    3
    4
    5
    6
    7
    #include "Cents.h"
    // note: this function is not a member function nor a friend function!
    Cents operator+(const Cents &c1, const Cents &c2) {
    // use the Cents constructor and operator+(int, int)
    // we don't need direct access to private members here
    return Cents(c1.getCents() + c2.getCents());
    }

In general, a normal function should be preferred over a friend function if it’s possible to do so with the existing member functions available (the less functions touching your classes’ internals, the better). However, don’t add additional access functions just to overload an operator as a normal function instead of a friend function!

Rule: Prefer overloading operators as normal functions instead of friends if it’s possible to do so without adding additional functions.

Overload the I/O operators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// std::ostream is the type for object std::cout
friend std::ostream& operator<<(std::ostream &out, const Point &point);

std::ostream& operator<<(std::ostream &out, const Point &point) {
// Since operator<< is a friend of the Point class, we can access Point's members directly.
out << "Point(" << point.m_x << ", " << point.m_y << ")";
return out;
}

int main() {
Point point(1, 2);
std::cout << point << "\n";
return 0;
}

The trickiest part here is the return type. With the arithmetic operators, we calculated and returned a single answer by value (because we were creating and returning a new result). However, if you try to return std::ostream by value, you’ll get a compiler error. This happens because std::ostream specifically disallows being copied.

In this case, we return the left-hand parameter as a reference. This not only prevents a copy of std::ostream from being made, it also allows us to “chain” output commands together, such as std::cout << point << std::endl;.

Overloading operator>>

It is also possible to overload the input operator. This is done in a manner analogous to overloading the output operator. The key thing you need to know is that std::cin is an object of type std::istream. Here’s our Point class with an overloaded operator>>:

1
2
3
4
5
6
std::istream& operator>> (std::istream &in, Point &point) {
in >> point.m_x;
in >> point.m_y;
}
// Point point;
// std::cin >> point;

In conclusion, overloading operator<< and operator>> make it extremely easy to output your class to screen and accept user input from the console.

Overload operators using member functions

Overloading operators using a member function is very similar to overloading operators using a friend function. When overloading an operator using a member function:

  • The overloaded operator must be added as a member function of the left operand.
  • The left operand becomes the implicit *this object
  • All other operands become function parameters.

Converting a friend overloaded operator to a member overloaded operator is easy:

  1. The left parameter is removed, because that parameter now becomes the implicit *this object.
  2. Inside the function body, all references to the left parameter can be removed (e.g. cents.m_cents becomes m_cents, which implicitly references the *this object).
1
2
3
4
5
6
7
8
9
10
11
12
// friend version
friend Cents operator+(const Cents &cents, int value);

// member version
Cents operator+(int value) {
return Cents(m_cents + value);
}

// main
Cents cents2 = cents1 + 2;
// friend version: operator+(cents1, 2)
// member version: cents1.operator+(2) --implicitly--> operator+(&cents1, 2)

So if we can overload an operator as a friend or a member, which should we use? In order to answer that question, there’s a few more things you’ll need to know.

Not everything can be overloaded as a friend function:

The assignment (=), subscript ([]), function call (()), and member selection (->) operators must be overloaded as member functions, because the language requires them to be.

Not everything can be overloaded as a member function:

In lesson 9.3 – Overloading the I/O operators, we overloaded operator<< for our Point class using the friend function method.

However, we are not able to overload operator<< as a member function. Why not? Because the overloaded operator must be added as a member of the left operand. In this case, the left operand is an object of type std::ostream. std::ostream is fixed as part of the standard library. We can’t modify the class declaration to add the overload as a member function of std::ostream. This necessitates that operator<< be overloaded as a friend.

Similarly, although we can overload operator+(Cents, int) as a member function (as we did above), we can’t overload operator+(int, Cents) as a member function, because int isn’t a class we can add members to.

Typically, we won’t be able to use a member overload if the left operand is either not a class (e.g. int), or it is a class that we can’t modify (e.g. std::ostream).

When do we use a normal, friend, or member function overload?

One of the two is usually a better choice than the other.

  • When dealing with binary operators that don't modify the left operand (e.g. operator+), the normal or friend function version is typically preferred, because it works for all parameter types (even when the left operand isn’t a class object, or is a class that is not modifiable). The normal or friend function version has the added benefit of “symmetry”, as all operands become explicit parameters (instead of the left operand becoming *this and the right operand becoming an explicit parameter).

  • When dealing with binary operators that do modify the left operand (e.g. operator+=), the member function version is typically preferred. In these cases, the leftmost operand will always be a class type, and having the object being modified become the one pointed to by *this is natural. Because the rightmost operand becomes an explicit parameter, there’s no confusion over who is getting modified and who is getting evaluated.

  • Unary operators are usually overloaded as member functions as well, since the member version has no parameters.

The following rules of thumb can help you determine which form is best for a given situation:

  • If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a member function.
  • If you’re overloading a unary operator, do so as a member function.
  • If you’re overloading a binary operator that modifies its left operand (e.g. operator+=), do so as a member function if you can.
  • If you’re overloading a binary operator that does not modify its left operand (e.g. operator+), do so as a normal function or friend function. (if left operands of basic types change, they are new variables, not the original ones)

Also, friend or normal?

  • Depending on whether you’re going to access the private members of the class.
  • But in the normal version, you can still call public getter and setter to access private members.

Overload unary operators +, -, and !

1
2
3
4
5
public:
// const! member function
Cents operator-() const {
return Cents(-m_cents);
}

Note that there’s no confusion between the negative operator- and the
minus operator- since they have a different number of parameters.

1
2
3
4
5
public:
// const! (member function)
bool operator!() const {
return (m_x == 0.0 && m_y == 0.0);
}

Overload the comparison operators

1
2
3
4
// normal or friend version (don't need const)
bool operator==(const Car &c1, const Car &c2) {
return (c1.m_make == c2.m_make && c1.m_model == c2.m_model);
}

What about operator< and operator>? What would it mean for a Car to be greater or less than another Car? We typically don’t think about cars this way. Since the results of operator< and operator> would not be immediately intuitive, it may be better to leave these operators undefined.

Recommendation: Don’t define overloaded operators that don’t make sense for your class.

However, there is one common exception to the above recommendation. What if we wanted to sort a list of Cars? In such a case, we might want to overload the comparison operators to return the member (or members) you’re most likely to want to sort on. For example, an overloaded operator< for Cars might sort based on make and model alphabetically.

Overload the increment and decrement operators

Because the increment and decrement operators are both unary operators and they modify their operands, they’re best overloaded as member functions. We’ll tackle the prefix versions first because they’re the most straightforward.

1
2
3
4
5
6
7
8
9
public:
// prefix version
Digit& operator++ () {
if (m_digit == 9)
m_digit = 0;
else
++m_digit;
return *this;
}

Overloading postfix increment and decrement

Normally, functions can be overloaded when they have the same name but a different number and/or different types of parameters. However, consider the case of the prefix and postfix increment and decrement operators. Both have the same name (eg. operator++), are unary, and take one parameter of the same type. So how it is possible to differentiate the two when overloading?

dummy = fake, false

The answer is that C++ uses a dummy variable or dummy argument for the postfix operators. This argument is a fake integer parameter that only serves to distinguish the postfix version of increment/decrement from the prefix version.

  • ++i: i is the (this), so the 1st parameter is not needed.

  • i++: i corresponds the 2nd parameter “int”, but we don’t use it; instead we’ll use the implicit, hidden 1st parameter (this).

I don’t know if it is correct. Too confusing. Maybe it just belongs to syntax stuff.

Here is the above Digit class with both prefix and postfix overloads:

1
2
3
4
5
6
7
8
9
10
11
12
public:
// prefix | ++i
Digit& operator++(); // member function
// postfix | i++
Digit operator++(int) { // member function
// Create a temp
Digit temp(m_digit);
// apply the prefix operator above, cut down duplicate code!
++(*this);
// Return temp result
return temp;
}

There are a few interesting things going on here.

  • First, note that we’ve distinguished the prefix from the postfix operators by providing an integer dummy parameter on the postfix version.

  • Second, because the dummy parameter is not used in the function implementation, we have not even given it a name. This tells the compiler to treat this variable as a placeholder, which means it won’t warn us that we declared a variable but never used it.

  • Third, note that the prefix and postfix operators do the same job – they both increment or decrement the object. The difference between the two is in the value they return. The overloaded prefix operators return the object after it has been incremented or decremented. Consequently, overloading these is fairly straightforward. We simply increment or decrement our member variables, and then return *this.

  • The postfix operators, on the other hand, need to return the state of the object before it is incremented or decremented. This leads to a bit of a conundrum – if we increment or decrement the object, we won’t be able to return the state of the object before it was incremented or decremented. On the other hand, if we return the state of the object before we increment or decrement it, the increment or decrement will never be called.

The typical way this problem is solved is to use a temporary variable that holds the value of the object before it is incremented or decremented. Then the object itself can be incremented or decremented. And finally, the temporary variable is returned to the caller. In this way, the caller receives a copy of the object before it was incremented or decremented, but the object itself is incremented or decremented. Note that this means the return value of the overloaded operator must be a non-reference, because we can’t return a reference to a local variable that will be destroyed when the function exits. Also note that this means the postfix operators are typically less efficient than the prefix operators because of the added overhead of instantiating a temporary variable and returning by value instead of reference.

1
2
// Note that there are two different objects!
C > C++ // true

Overload the subscript operator

The subscript operator is one of the operators that must be overloaded as a member function. An overloaded operator[] function will always take one parameter (unary): the subscript that the user places between the hard braces. In our IntList case, we expect the user to pass in an integer index, and we’ll return an integer value back as a result.

1
2
3
4
5
6
7
8
9
10
11
class IntList {
private:
int m_list[10];
public:
int& operator[](const int index);
};
// including setter and getter!
int& IntList::operator[](const int index) {
assert(index >= 0 && index < 10); // error checking
return m_list[index];
}

This is both easy syntactically and from a comprehension standpoint. When list[2] evaluates, the compiler first checks to see if there’s an overloaded operator[] function. If so, it passes the value inside the hard braces (in this case, 2) as an argument to the function.

Note that although you can provide a default value for the function parameter, actually using operator[] without a subscript inside is not considered a valid syntax, so there’s no point.

Why operator[] returns a reference

Let’s take a closer look at how list[2] = 3 evaluates. Because the subscript operator has a higher precedence than the assignment operator, list[2] evaluates first. list[2] calls operator[], which we’ve defined to return a reference to list.m_list[2]. Because operator[] is returning a reference, it returns the actual list.m_list[2] array element. Our partially evaluated expression becomes list.m_list[2] = 3, which is a straightforward integer assignment.

Note: Because the result of operator[] can be used on the left-hand side of an assignment, the return value of operator[] must be l-value, because you can only take a reference of variables that have memory addresses.

Dealing with const objects

In the above IntList example, operator[] is non-const, and we can use it as an l-value to change the state of non-const objects. However, what if our IntList object was const? In this case, we wouldn’t be able to call the non-const version of operator[] because that would allow us to potentially change the state of a const object.

The good news is that we can define a non-const and a const version of operator[] separately. The non-const version will be used with non-const objects, and the const version with const-objects.

1
2
3
public:
int& operator[](const int index);
const int& operator[](const int index); // add const?

Pointers to objects and overloaded operator[] don’t mix

If you try to call operator[] on a pointer to an object, C++ will assume you’re trying to index an array of objects of that type.

1
2
3
4
5
IntList *list = new IntList;
list[2] = 3; // error: this will assume we're accessing index 2 of an array of IntLists
// proper syntax:
(*list)[2] = 3; // operator[]
delete list;

Because we can’t assign an integer to an IntList, this won’t compile. However, if assigning an integer was valid, this would compile and run, with undefined results.

Rule: Make sure you’re not trying to call an overloaded operator[] on a pointer to an object.

The function parameter does not need to be an integer

As mentioned above, C++ passes what the user types between the hard braces as an argument to the overloaded function. In most cases, this will be an integer value. However, this is not required – and in fact, you can define that your overloaded operator[] take a value of any type you desire. You could define your overloaded operator[] to take a double, a std::string, or whatever else you like.

1
2
3
4
5
6
7
8
9
class Stupid {
public:
void operator[](std::string index);
};

// It does not make sense to print sth., but it is the easiest way to show that the function parameter can be a non-integer
void Stupid::operator[](std::string index) {
std::cout << index;
}

Overload the parenthesis operator

Why this one is special

All of the overloaded operators you have seen so far let you define the type of the operator’s parameters, but not the number of parameters (which is fixed based on the type of the operator). For example, operator== always takes two parameters, whereas operator! always takes one. The parenthesis operator() is a particularly interesting operator in that it allows you to vary both the type AND number of parameters it takes.

There are two things to keep in mind:

  • First, the parenthesis operator must be implemented as a member function.
  • Second, in non-object-oriented C++, the () operator is used to call functions. In the case of classes, operator() is just a normal operator that calls a function (named operator()) like any other overloaded operator.

The following content is about Matrix.

Operator[] is limited to a single parameter, it is not sufficient to let us index a two-dimensional array.

However, because the () operator can take as many parameters as we want it to have, we can declare a version of operator() that takes two integer index parameters, and use it to access our two-dimensional array. Here is an example of this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private:
double data[4][4];
public:
double& operator()(int row, int col) {
// assert
return data[row][col];
}
const double& operator()(int row, int col) const {
// assert
return data[row][col];
}
void operator() () {
// reset all elements of the matrix to 0.0
}

// main
Matrix matrix;
matrix(1, 2) = 4.5;
std::cout << matrix(1, 2);
matrix(); // erase matrix

Because the () operator is so flexible, it can be tempting to use it for many different purposes. However, this is strongly discouraged, since the () symbol does not really give any indication of what the operator is doing. In our example above, it would be better to have written the erase functionality as a function called clear() or erase(), as matrix.erase() is easier to understand than matrix() (which could do anything!).

Having fun with functors

Operator() is also commonly overloaded to implement functors (or function object), which are classes that operate like functions. The advantage of a functor over a normal function is that functors can store data in member variables (since they are classes).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Accumulator {
private:
int m_counter = 0;

public:
Accumulator() { }
int operator() (int i) {
return (m_counter += i);
}
};

int main() {
Accumulator acc;
std::cout << acc(10) << std::endl; // prints 10
std::cout << acc(20) << std::endl; // prints 30
return 0;
}

Note that using our Accumulator looks just like making a normal function call, but our Accumulator object is storing an accumulated value.

You may wonder why we couldn’t do the same thing with a normal function and a static local variable to preserve data between function calls. We could, but because functions only have one global instance, we’d be limited to using it for one thing at a time. With functors, we can instantiate as many separate functor objects as we need and use them all simultaneously.

Overload typecasts

C++ already knows how to convert between the built-in data types. However, it does not know how to convert any of our user-defined classes. That’s where overloading the typecast operators comes into play.

1
2
3
4
5
6
7
8
9
10
void printInt(int value) {
std::cout << value;
}

int main() {
Cents cents(7);
printInt(cents.getCents()); // print 7

return 0;
}

If we have already written a lot of functions that take integers as parameters, our code will be littered with calls to getCents(), which makes it more messy than it needs to be.

To make things easier, we can overload the int typecast, which will allow us to cast our Cents class object directly into an int. The following example shows how this is done:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cents {
private:
int m_cents;
public:
Cents(int cents=0) {
m_cents = cents;
}
// Overloaded int typecast
operator int() { return m_cents; } // no return type!

int getCents() { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};

int main() {
Cents cents(7);
printInt(cents);
return 0;
}

There are two things to note:

  • To overload the function that casts our class to an int, we write a new function in our class called operator int(). Note that there is a space between the word operator and the type we are casting to.
  • Casting operators do not have a return type. C++ assumes you will be returning the correct type.

We can now also explicitly cast our Cents variable to an int by static_cast<int>():

1
2
Cents cents(7);
int c = static_cast<int>(cents);

You can overload cast operators for any data type you wish, including your own user-defined data types!

Here’s a new class called Dollars that provides an overloaded Cents cast operator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dollars {
private:
int m_dollars;
public:
Dollars(int dollars=0) {
m_dollars = dollars;
}

// Allow us to convert Dollars into Cents
operator Cents() {
return Cents(m_dollars * 10);
}
};

// main
Dollars dollars(9);
printCents(dollars); // dollars will be implicitly cast to a Cents here
Cents c = static_cast<Cents>(dollars); // or use static_cast<Cents>

The copy constructor

Recap the types of initialization that C++ supports:

  • direct initialization
  • uniform initialization
  • copy initialization

With direct and uniform initialization, the object being created is directly initialized. However, copy initialization is a little more complicated. We’ll explore copy initialization in more detail in the next lesson. But in order to do that effectively, we need to take a short detour.

1
2
3
4
int main() {
Fraction fiveThirds(5, 3); // Direct initialize a Fraction, calls Fraction(int, int) constructor
Fraction fCopy(fiveThirds); // with what constructor??
}

The answer is that this line is calling Fraction’s copy constructor. A copy constructor is a special type of constructor used to create a new object as a copy of an existing object. And much like a default constructor, if you do not provide a copy constructor for your classes, C++ will create a public copy constructor for you. Because the compiler does not know much about your class, by default, the created copy constructor utilizes a method of initialization called memberwise initialization. Memberwise initialization simply means that each member of the copy is initialized directly from the member of the class being copied. In the above example, fCopy.m_numerator would be initialized from fiveThirds.m_numerator, etc.

Just like we can explicitly define a default constructor, we can also explicitly define a copy constructor. The copy constructor looks just like you’d expect it to:

1
2
3
4
5
// Copy constructor
Fraction(const Fraction &fraction)
: m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator) {
std::cout << "Copy constructor called\n";
}

One interesting note: You’ve already seen a few examples of overloaded operator<<, where we’re able to access the private members of parameter f1 because the function is a friend of the Fraction class. Similarly, member functions of a class can access the private members of parameters of the same class type. Since our Fraction copy constructor takes a parameter of the class type (to make a copy of), we’re able to access the members of parameter fraction directly, even though it’s not the implicit object.

Preventing copies

We can prevent copies of our classes from being made by making the copy constructor private:

1
2
3
4
5
private:
Fraction(const Fraction &fraction)
: m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator) {
std::cout << "Testing" << "\n";
}

The copy constructor may be elided

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public:
Fraction(const Fraction &fraction)
: m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator) {
std::cout << "Testing" << "\n";
}

// main
// original version
Fraction fiveThirds(5, 3);
Fraction fCopy(fiveThirds);
// output: Testing

// Elided version
Fraction fiveThirds(Fraction(5, 3));
// output: nothing!

Why didn’t our copy constructor get called?

Note that initializing an anonymous object and then using that object to direct initialize our defined object takes two steps (one to create the anonymous object using the default constructor, one to call the copy constructor). However, the end result is essentially identical to just doing a direct initialization, which only takes one step.

For this reason, in such case, the compiler is allowed to opt out of calling the copy constructor and just do a direct initialization instead. This process is called elision.

So although you wrote:

1
Fraction fiveThirds(Fraction(5, 3));

The compiler may change this to:

1
Fraction fiveThirds(5, 3);

which only requires one constructor call (to Fraction(int, int)). Note that in cases where elision is used, any statements in the body of the copy constructor are not executed, even if they would have produced side effects (like printing to the screen)!

Prior to C++17, copy elision is an optimization the compiler can make. As of C++17, some cases of copy elision (including the example above) have been made mandatory.

Finally, note that if you make the copy constructor private, any initialization that would use the copy constructor will cause a compile error, even if the copy constructor is elided! Jesus!

1
2
// copy constructor is private
Fraction fiveThirds(Fraction(5, 3)); // error

Copy initialization

Consider the following line of code:

1
int x = 5;

This statement uses copy initialization to initialize newly created integer variable x to the value of 5. However, classes are a little more complicated, since they use constructors for initialization.

1
2
3
4
5
Fraction six = oldSix; // evaluated as Fraction six(oldSix);
// so
Fraction six = Fraction(6);
// it is evaluated the same way as the following
Fraction six(Fraction(6));

And as you learned in the previous lesson, this can potentially make calls to both Fraction(int, int) and the Fraction copy constructor (which may be elided for performance reasons). However, because eliding isn’t guaranteed (prior to C++17, where elision in this particular case is now mandatory), it’s better to avoid copy initialization for classes, and use uniform initialization instead.

Other places copy initialization is used

There are a few other places copy initialization is used, but two of them are worth mentioning explicitly. When you pass or return a class by value, that process uses copy initialization.

1
2
3
4
5
Fraction makeNegative(Fraction f) {
// ideally we should do this by const reference
f.setNumerator(-f.getNumerator());
return f;
}

In the above case, both the argument passed by value and the return value cannot be elided. However, in other cases, if the argument or return value meet specific criteria, the compiler may opt to elide the copy constructor. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Something {

};

Something foo() {
Something s;
return s;
}

Something s = foo();

Something goo(Something s) {
// ...
}

goo(Something()); // default constructor

In this case, the compiler will probably elide the copy constructor, even though variable s is returned by value.

Convert constructors, explicit, and delete

By default, C++ will treat any constructor as an implicit conversion operator. Consider the following case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public:
// Default constructor
Fraction(int numerator=0, int denominator=1)
: m_numerator(numerator), m_denominator(denominator) {
assert(denominator != 0);
}

Fraction makeNegative(Fraction f) {
f.setNumerator(-f.getNumerator());
return f;
}

int main() {
std::cout << makeNegative(6); // note the integer here
return 0;
}

Although function makeNegative() is expecting a Fraction, we’ve given it the integer literal 6 instead. Because Fraction has a constructor willing to take a single integer, the compiler will implicitly convert the literal 6 into a Fraction object. It does this by copy-initializing makeNegative() parameter f using the Fraction(int, int) constructor. And in this case, copy initialization is elided.

Note: This implicit conversion works for all kinds of initialization (direct, uniform, and copy).

Constructors eligible to be used for implicit conversions are called converting constructors (or conversion constructors). Prior to C++11, only constructors taking one parameter could be converting constructors. However, with the new uniform initialization syntax in C++11, this restriction was lifted, and constructors taking multiple parameters can now be converting constructors.

The explicit keyword

While doing implicit conversions makes sense in the Fraction case. In other cases, this may be undesirable, or lead to unexpected behaviors:

1
2
3
4
5
6
7
8
9
public:
MyString(int x) { // allocate string of size x
m_string.resize(x);
}
MyString(const char *string) { // allocate string to hold string value
m_string = string;
}
// main
MyString mine = 'x'; // use copy initialization for MyString

In the above example, the user is trying to initialize a string with a char. Because chars are part of the integer family, the compiler will use the converting constructor MyString(int) constructor to implicitly convert the char to a MyString. The program will then print this MyString, to unexpected results.

One way to address this issue is to make constructors (and conversion functions) explicit via the explicit keyword, which is placed in front of the constructor’s name. Constructors and conversion functions made explicit will not be used for implicit conversions or copy initialization:

1
2
3
4
5
6
7
public:
explicit MyString(int x) {
m_string.resize(x);
}

// main
MyString mine = 'x'; // compile error, since MyString(int) is now explicit and nothing will match this

However, note that making a constructor explicit only prevents implicit conversions. Explicit conversions (via casting) are still allowed:

1
2
std::cout << static_cast<MyString>(5);
// Allowed: explicit cast of 5 to MyString(int)

Direct or uniform initialization will also still convert parameters to match (uniform initialization will not do narrowing conversions, but it will happily do other types of conversions).

I am losing myself! Complicated!

1
2
MyString str('x');
// Allowed: initialization parameters may still be implicitly converted to match

Rule: Consider making your constructors and user-defined conversion member functions explicit to prevent implicit conversion errors.

In C++11, the explicit keyword can also be used with conversion operators.

The delete keyword

In our MyString case, we really want to completely disallow ‘x’ from being converted to a MyString (whether implicit or explicit, since the results aren’t going to be intuitive). One way to partially do this is to add a MyString(char) constructor, and make it private:

1
2
3
4
private:
MyString(char) {
// Objects of type MyString(char) can't be constructed from outside the class
}

However, this constructor can still be used from inside the class (private access only prevents non-members from calling this function).

A better way to resolve the issue is to use the delete keyword (introduced in C++11) to delete the function:

1
2
3
4
5
public:
myString(char) = delete;
/* any use of this constructor is an error */

MyString mine('x'); // compile error, since MyString(char) is deleted

When a function has been deleted, any use of that function is considered a compile error.

Note that the copy constructor and overloaded operators may also be deleted in order to prevent those functions from being used.

Overload the assignment operator

Assignment vs Copy constructor

The purpose of the copy constructor and the assignment operator are almost equivalent – both copy one object to another. However, the copy constructor initializes new objects, whereas the assignment operator replaces the contents of existing objects.

The difference between the copy constructor and the assignment operator causes a lot of confusion for new programmers, but it’s really not all that difficult. Summarizing:

It depends on whether the new object has been created or not.

  • If a new object has to be created before the copying can occur, the copy constructor is used (note: this includes passing or returning objects by value).
  • If a new object does not have to be created before the copying can occur, the assignment operator is used.

Overloading the assignment operator (operator=) is fairly straightforward, with one specific caveat that we’ll get to. The assignment operator must be overloaded as a member function.

If operator= is not overloaded, copy constructor is always used.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public:
// Copy constructor
Fraction(const Fraction &copy)
: m_numerator(copy.m_numerator), m_denominator(copy.m_denominator) {
// foo
}
// Overloaded assignment
Fraction& operator=(const Fraction &fraction) {
// do the copy
m_numerator = fraction.m_numerator;
m_denominator = fraction.m_denominator;
return *this; // support chained assignment
}
// main
Fraction f1(5, 3);
Fraction f2(7, 2);
Fraction f3(9, 5);
f1 = f2 = f3; // chained assignment: all uses assignment operator=
// only assign member variables

Issues due to self-assignment

1
2
3
4
5
6
7
int main() {
Fraction f1(5,3);
f1 = f1; // self assignment
/* this will call f1.operator=(f1) */

return 0;
}

This will call f1.operator=(f1), and under the simplistic implementation above, all of the members will be assigned to themselves. In this particular example, the self-assignment causes each member to be assigned to itself, which has no overall impact, other than wasting time. In most cases, a self-assignment doesn’t need to do anything at all!

Solution: detecting and handling self-assignment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// A better implementation of operator=
Fraction& Fraction::operator=(const Fraction &fraction) {
// self-assignment guard
if (this == &fraction) {
return *this;
}

// do the copy
m_numerator = fraction.m_numerator;
m_denominator = fraction.m_denominator;

// return the existing object so we can chain this operator
return *this;
}

By checking if our implicit object is the same as the one being passed in as a parameter, we can have our assignment operator just return immediately without doing any other work.

Note that there is no need to check for self-assignment in a copy-constructor. This is because the copy constructor is only called when new objects are being constructed, and there is no way to assign a newly created object to itself in a way that calls to copy constructor.

Default assignment operator

Unlike other operators, the compiler will provide a default public assignment operator for your class if you do not provide one. This assignment operator does memberwise assignment (which is essentially the same as the memberwise initialization that default copy constructors do).

Shallow vs. deep copying

Because C++ does not know much about your class, the default copy constructor and the default assignment operator it provides use a copying method known as a memberwise copy (also known as a shallow copy). This means that C++ copies each member of the class individually (using the assignment operator for overloaded operator=, and direct initialization for the copy constructor). When classes are simple (e.g. do not contain any dynamically allocated memory), this works very well.

However, when designing classes that handle dynamically allocated memory, memberwise (shallow) copying can get us in a lot of trouble! This is because shallow copies of a pointer just copy the address of the pointer – it does not allocate any memory or copy the contents being pointed to.

1
2
3
4
5
class MyString {
private:
char *m_data;
int m_length;
}

If we do not provide specific constructors, C++ will provide a default copy constructor and default assignment operator that do a shallow copy. The copy constructor will look something like this:

1
2
3
4
5
6
7
MyString::MyString(const MyString &source)
: m_length(source.m_length), m_data(source.m_data) {
// foo
}
~MyString() {
delete[] m_data;
}

Note that m_data is just a shallow pointer copy of source.m_data, meaning they now both point to the same thing.

1
2
3
4
5
6
7
8
9
10
int main() {
MyString hello("Hello, world!");
{
MyString copy = hello; // use default copy constructor
} // copy is a local variable, so it gets destroyed here. The destructor deletes copy's string, which leaves hello with a dangling pointer

std::cout << hello.getString() << "\n"; // this will have undefined behavior

return 0;
}

Deep copying

One answer to this problem is to do a deep copy on any non-null pointers being copied. A deep copy allocates memory for the copy and then copies the actual value, so that the copy lives in distinct memory from the source. This way, the copy and source are distinct and will not affect each other in any way. Doing deep copies requires that we write our own copy constructors and overloaded assignment operators.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Copy constructor
MyString::MyString(const MyString& source) {
// because m_length is not a pointer, we can shallow copy it
m_length = source.m_length;

// m_data is a pointer, so we need to deep copy it if it is non-null
if (source.m_data) {
// allocate memory for our copy
m_data = new char[m_length];
// do the copy
for (int i = 0; i < m_length; ++i) {
m_data[i] = source.m_data[i];
}
} else {
m_data = 0;
}
}

// Assignment operator
MyString& MyString::operator= (const MyString & source) {
// check for self-assignment - difference
if (this == &source)
return *this;

// explicitly delete the original value - difference
// first we need to deallocate any value that this string is holding!
delete[] m_data;

// because m_length is not a pointer, we can shallow copy it
m_length = source.m_length;

// m_data is a pointer, so we need to deep copy it if it is non-null
if (source.m_data) {
// allocate memory for our copy
m_data = new char[m_length];

// do the copy
for (int i=0; i < m_length; ++i) {
m_data[i] = source.m_data[i];
}
} else {
m_data = 0;
}

return *this; // - difference
}

A better solution

Classes in the standard library that deal with dynamic memory, such as std::string and std::vector, handle all of their memory management, and have overloaded copy constructors and assignment operators that do proper deep copying. So instead of doing your own memory management, you can just initialize or assign them like normal fundamental variables! That makes these classes simpler to use, less error-prone, and you don’t have to spend time writing your own overloaded functions!