Object-oriented programming (OOP) provides us with the ability to create objects that tie together both properties and behaviors into a self-contained, reusable package. This leads to code that looks more like this:
you.driveTo(work);
This not only reads more clearly, it also makes it clearer who the subject is (you) and what behavior is being invoked (driving somewhere). Rather than being focused on writing functions, we’re focused on defining objects that have a well-defined set of behaviors. This is why the paradigm is called “object-oriented”.
Note that OOP doesn’t replace traditional programming methods. Rather, it gives you additional tools in your programming tool belt to manage complexity when needed.
In the world of object-oriented programming, we often want our types to not only hold data, but provide functions that work with the data as well. In C++, this is typically done via the class keyword. The class keyword defines a new program-defined type called a class.
Just like with structs, one of the easiest mistakes to make in C++ is to forget the semicolon at the end of a class declaration. This will cause a compiler error on the next line of code. Modern compilers like Visual Studio 2010 will give you an indication that you may have forgotten a semicolon, but older or less sophisticated compilers may not, which can make the actual error hard to find.
Initialize the member variables of a class at the point of declaration.
So when we call “today.print()”, the compiler interprets m_day as today.m_day, m_month as today.m_month, and m_year as today.m_year. If we called “tomorrow.print()”, m_day would refer to tomorrow.m_day instead.
In this way, the associated object is essentially implicitly passed to the member function. For this reason, it is often called the implicit object.
Name your classes starting with a capital letter.
With member functions, this limitation doesn’t apply:
class foo
{
public:
void x() { y(); } // okay to call y() here, even though y() isn't defined until later in this class
void y() { };
};
In addition to member variables and member functions, classes can have member types or nested types (including type aliases).
Use the struct keyword for data-only structures. Use the class keyword for objects that have both data and functions.
The class keyword lets us create a custom type in C++ that can contain both member variables and member functions. Classes form the basis for Object-oriented programming, and we’ll spend the rest of this chapter and many of the future chapters exploring all they have to offer!
Public members are members of a struct or class that can be accessed directly by anyone, including from code that exists outside the struct or class.
The code outside of a struct or class is sometimes called the public: the public is only allowed to access the public members of a struct or class, which makes sense.
class DateClass // 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
};
int main()
{
DateClass date;
date.m_month = 10; // error
date.m_day = 14; // error
date.m_year = 2020; // error
return 0;
}
Private members are members of a class that can not be accessed by the public. Private members can only be accessed by other members of the class (or by friends of the class).
Make member variables private, and member functions public, unless you have a good reason not to.
The group of public members of a class are often referred to as a public interface.
Consider the following program:
#include
class DateClass // 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
public:
void setDate(int month, int day, int year)
{
m_month = month;
m_day = day;
m_year = year;
}
void print()
{
std::cout << m_month << '/' << m_day << '/' << m_year;
}
// Note the addition of this function
void copyFrom(const DateClass& d)
{
// Note that we can access the private members of d directly
m_month = d.m_month;
m_day = d.m_day;
m_year = d.m_year;
}
};
int main()
{
DateClass date;
date.setDate(10, 14, 2020); // okay, because setDate() is public
DateClass copy {};
copy.copyFrom(date); // okay, because copyFrom() is public
copy.print();
std::cout << '\n';
return 0;
}
One nuance of C++ that is often missed or misunderstood is that access control works on a per-class basis, not a per-object basis. This means that when a function has access to the private members of a class, it can access the private members of any object of that class type that it can see.
In the above example, copyFrom() is a member of DateClass, which gives it access to the private members of DateClass. This means copyFrom() can not only directly access the private members of the implicit object it is operating on (copy), it also means it has direct access to the private members of DateClass parameter d! If parameter d were some other type, this would not be the case.
This can be particularly useful when we need to copy members from one object of a class to another object of the same class. We’ll also see this topic show up again when we talk about overloading operator<< to print members of a class in the next chapter.
Now let’s try something a little more complex. Let’s write a class that implements a simple stack from scratch. Review lesson 12.2 – The stack and the heap if you need a refresher on what a stack is.
For similar reasons, the separation of implementation and interface is useful in programming.
Getters should return by value or const reference.
As you can see, encapsulation provides a lot of benefits for just a little bit of extra effort. The primary benefit is that encapsulation allows us to use a class without having to know how it was implemented. This makes it a lot easier to use classes we’re not familiar with.
When all members of a class (or struct) are public, we can use aggregate initialization to initialize the class (or struct) directly using list-initialization:
class Foo
{
public:
int m_x {};
int m_y {};
};
int main()
{
Foo foo { 6, 7 }; // list-initialization
return 0;
}
However, as soon as we make any member variables private, we’re no longer able to initialize classes in this way. It does make sense: if you can’t directly access a variable (because it’s private), you shouldn’t be able to directly initialize it.
So then how do we initialize a class with private member variables? The answer is through constructors.
In the above program, we initialized our class object using value-initialization:
Fraction frac {}; // Value initialization using empty set of braces
We can also initialize class objects using default-initialization:
Fraction frac; // Default-initialization, calls default constructor
Favor value-initialization over default-initialization for class objects.
Favor brace initialization to initialize class objects.
If you have constructors in your class and need a default constructor that does nothing (e.g. because all your members are initialized using non-static member initialization), use = default.
Always initialize all member variables in your objects.
This produces code similar to the following:
const int m_value; // error: const vars must be initialized with a value
m_value = 5; // error: const vars can not be assigned to
Assigning values to const or reference member variables in the body of the constructor is clearly not possible in some cases.
Use member initializer lists to initialize your class member variables instead of assignment.
Const member variables must be initialized.
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:
Member initializer lists allow us to initialize our members rather than assign values to them. This is the only way to initialize members that require values upon initialization, such as const or reference members, and it can be more performant than assigning values in the body of the constructor. Member initializer lists work both with fundamental types and members that are classes themselves.
When writing a class that has multiple constructors (which is most of them), having to specify default values for all members in each constructor results in redundant code. If you update the default value for a member, you need to touch each constructor.
It’s possible to give normal class member variables (those that don’t use the static keyword) a default initialization value directly:
#include
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:
void print()
{
std::cout << "length: " << m_length << ", width: " << m_width << '\n';
}
};
int main()
{
Rectangle x{}; // x.m_length = 1.0, x.m_width = 1.0
x.print();
return 0;
}
This program produces the result:
length: 1.0, width: 1.0
However, note that constructors still determine what kind of objects may be created. Consider the following case:
#include
class Rectangle
{
private:
double m_length{ 1.0 };
double m_width{ 1.0 };
public:
// note: No default constructor provided in this example
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)
}
void print()
{
std::cout << "length: " << m_length << ", width: " << m_width << '\n';
}
};
int main()
{
Rectangle x{}; // will not compile, no default constructor exists, even though members have default initialization values
return 0;
}
Even though we’ve provided default values for all members, no default constructor has been provided, so we are unable to create Rectangle objects with no arguments.
Note that initializing members using non-static member initialization requires using either an equals sign, or a brace (uniform) initializer – the parenthesis initialization form doesn’t work here:
class A
{
int m_a = 1; // ok (copy initialization)
int m_b{ 2 }; // ok (brace initialization)
int m_c(3); // doesn't work (parenthesis initialization)
};
Favor use of non-static member initialization to give default values for your member variables.
When you instantiate a new object, the object’s constructor is called implicitly. It’s not uncommon to have a class with multiple constructors that have overlapping functionality. Consider the following class:
class Foo
{
public:
Foo()
{
// code to do A
}
Foo(int value)
{
// code to do A
// code to do B
}
};
This class has two constructors: a default constructor, and a constructor that takes an integer. Because the “code to do A” portion of the constructor is required by both constructors, the code is duplicated in each constructor.
As you’ve (hopefully) learned by now, having duplicate code is something to be avoided as much as possible, so let’s take a look at some ways to address this.
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
}
};
Constructors are allowed to call other constructors from the same class. This process is called delegating constructors (or constructor chaining)
.To have one constructor call another, simply call the constructor in the member initializer list. This is one case where calling another constructor directly is acceptable. Applied to our example above:
class Foo
{
private:
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.
A few additional notes about delegating constructors. First, a constructor that delegates to another constructor is not allowed to do any member initialization itself. So your constructors can delegate or initialize, but not both.
Second, it’s possible for one constructor to delegate to another constructor, which delegates back to the first constructor. This forms an infinite loop, and will cause your program to run out of stack space and crash. You can avoid this by ensuring all of your constructors resolve to a non-delegating constructor.
If you have multiple constructors that have the same functionality, use delegating constructors to avoid duplicate code.
Constructors are allowed to call non-constructor member functions (and non-member functions), so a better solution is to use a normal (non-constructor) member function to handle the common setup tasks, like this:
#include
class Foo
{
private:
const int m_value { 0 };
void setup() // setup is private so it can only be used by our constructors
{
// code to do some common setup tasks (e.g. open a file or database)
std::cout << "Setting things up...\n";
}
public:
Foo()
{
setup();
}
Foo(int value) : m_value { value } // we must initialize m_value since it's const
{
setup();
}
};
int main()
{
Foo a;
Foo b{ 5 };
return 0;
}
In this case, we’ve created a setup() member function to handle various setup tasks that we need, and both of our constructors call setup(). We’ve made this function private so we can ensure that only members of our class can call it.
Of course, setup() isn’t a constructor, so it can’t initialize members. By the time the constructor calls setup(), the members have already been created (and initialized if an initialization value was provided). The setup() function can only assign values to members or do other types of setup tasks that can be done through normal statements (e.g. open files or databases). The setup() function can’t do things like bind a member reference or set a const value (both of which must be done on initialization), or assign values to members that don’t support assignment.
While this works, it violates the DRY principle, as we have our “default” values in two places: once in the non-static member initializers, and again in the body of reset(). There is no way for the reset() function to get the default values from the non-static initializer.
However, if the class is assignable (meaning it has an accessible assignment operator), we can create a new class object, and then use assignment to overwrite the values in the object we want to reset:
#include
class Foo
{
private:
int m_a{ 5 };
int m_b{ 6 };
public:
Foo()
{
}
Foo(int a, int b)
: m_a{ a }, m_b{ b }
{
}
void print()
{
std::cout << m_a << ' ' << m_b << '\n';
}
void reset()
{
// consider this a bit of magic for now
*this = Foo{}; // create new Foo object, then use assignment to overwrite our implicit object
}
};
int main()
{
Foo a{ 1, 2 };
a.reset();
a.print();
return 0;
}
A destructor is another special kind of class member function that is executed when an object of that class is destroyed. Whereas constructors are designed to initialize a class, destructors are designed to help clean up.
A class can only have a single destructor.
However, destructors may safely call other member functions since the object isn’t destroyed until after the destructor executes.
In lesson 11.17 – An introduction to std::vector, we note that parentheses based initialization should be used when initializing an array/container/list class with a length (as opposed to a list of elements). For this reason, we initialize IntArray using IntArray ar ( 10 );.
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.
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.
Note that if you use the std::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).
As you can see, when constructors and destructors are used together, your classes can initialize and clean up after themselves without the programmer having to do any special work! This reduces the probability of making an error, and makes classes easier to use.
The good news is that all of this happens automatically, and it doesn’t really matter whether you remember how it works or not. All you need to remember is that all non-static member functions have a “this” pointer that refers to the object the function was called on.
Because “this” is just a function parameter, it doesn’t add any memory usage to your class (just to the member function call, since that parameter needs to be passed to the function and stored in memory).
Although this is acceptable coding practice, we find using the “m_” prefix on all member variable names provides a better solution by preventing duplicate names altogether!
class Calc
{
private:
int m_value{};
public:
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; }
};
#include
int main()
{
Calc calc{};
calc.add(5).sub(3).mult(4);
std::cout << calc.getValue() << '\n';
return 0;
}
The “this” pointer is a hidden parameter implicitly added to any non-static member function. Most of the time, you will not need to access it directly, but you can if needed. It’s worth noting that “this” is a const pointer – you can change the value of the underlying object it points to, but you can not make it point to something else!
By having functions that would otherwise return void return *this instead, you can make those functions chainable. This is most often used when overloading operators for your classes (something we’ll talk about more in chapter 14).
It shouldn’t. If your header file has proper header guards, it shouldn’t be possible to include the class definition more than once into the same file.
Types (which include classes), are exempt from the part of the one-definition rule that says you can only have one definition per program. Therefore, there isn’t an issue #including class definitions into multiple code files (if there was, classes wouldn’t be of much use).
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.
Make any member function that does not modify the state of the class object const, so that it can be called by const objects.
Can you figure out what’s wrong with the following code?
#include
class Date
{
private:
int m_year {};
int m_month {};
int m_day {};
public:
Date(int year, int month, int day)
{
setDate(year, month, day);
}
void setDate(int year, int month, int day)
{
m_year = year;
m_month = month;
m_day = day;
}
int getYear() { return m_year; }
int getMonth() { return m_month; }
int getDay() { return m_day; }
};
// note: We're passing date by const reference here to avoid making a copy of date
void printDate(const Date& date)
{
std::cout << date.getYear() << '/' << date.getMonth() << '/' << date.getDay() << '\n';
}
int main()
{
Date date{2016, 10, 16};
printDate(date);
return 0;
}
The answer is that inside of the printDate function, date is treated as a const object. And with that const date, we’re calling functions getYear(), getMonth(), and getDay(), which are all non-const. Since we can’t call non-const member functions on const objects, this will cause a compile error.
The fix is simple: make getYear(), getMonth(), and getDay() const:
#include
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 (returns const reference)
std::string& getValue() { return m_value; } // getValue() for non-const objects (returns non-const reference)
};
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!
Access static members by class name (using the scope resolution operator) rather than through an object of the class (using the member selection operator).
Because static member variables are not part of the individual class objects (they are treated similarly to global variables, and get initialized when the program starts), you must explicitly define the static member outside of the class, in the global scope.
In the example above, we do so via this line:
int Something::s_value{ 1 }; // defines the static member variable
This line serves two purposes: it instantiates the static member variable (just like a global variable), and optionally initializes it. In this case, we’re providing the initialization value 1. If no initializer is provided, C++ initializes the value to 0.
Note that this static member definition is not subject to access controls: you can define and initialize the variable even if it’s declared as private (or protected) in the class.
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 linker error).
class Whatever
{
public:
static const int s_value{ 4 }; // a static const int can be declared and initialized directly
};
#include
class Whatever
{
public:
static constexpr double s_value{ 2.2 }; // ok
static constexpr std::array<int, 3> s_array{ 1, 2, 3 }; // this even works for classes that support constexpr initialization
};
Finally, as of C++17, we can also initialize non-const static members in the class definition by declaring them inline:
class Whatever
{
public:
static inline int s_value{ 4 }; // a static inline int can be declared and initialized directly (C++17)
};
Prefer initializing static constexpr members at the point of definition.
Prefer making static non-constexpr members inline and initializing them at the point of definition.
Why use static variables inside classes? One useful example is to assign a unique ID to every instance of the class.
Static member variables can also be useful when the class needs to utilize an internal lookup table (e.g. an array used to store a set of pre-calculated values). By making the lookup table static, only one copy exists for all objects, rather than making a copy for each object instantiated. This can save substantial amounts of memory.
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()
{
// how do we access Something::s_value since it is private?
}
Like static member variables, static member functions are not attached to any particular object. Here is the above example with a static member function accessor:
#include
class Something
{
private:
static int s_value;
public:
static int getValue() { return s_value; } // static member function
};
int Something::s_value{ 1 }; // initializer
int main()
{
std::cout << Something::getValue() << '\n';
}
Like static member variables, they can also be called through objects of the class type, though this is not recommended.
Be careful when writing classes with all static members. Although such “pure static classes” (also called “monostates”) can be useful, they also come with some potential downsides.
And while some modern languages do support static constructors for precisely this purpose, C++ is unfortunately not one of them.
If your static variable can be directly initialized, no constructor is needed: you can initialize the static member variable at the point of definition (even if it is private). We do this in the IDGenerator example above. Here’s another example:
class MyClass
{
public:
static std::vector<char> s_mychars;
};
std::vector<char> MyClass::s_mychars{ 'a', 'e', 'i', 'o', 'u' }; // initialize static variable at point of definition
Static member functions can be used to work with static member variables in the class. An object of the class is not required to call them.
Classes can be created with all static member variables and static functions. However, such classes are essentially the equivalent of declaring functions and global variables in a globally accessible namespace, and should generally be avoided unless you have a particularly good reason to use them.
A friend function is a function that can access the private members of a class as though it was a member of that class.
Here’s another example:
#include
class Value
{
private:
int m_value{};
public:
Value(int value)
: m_value{ value }
{
}
friend bool isEqual(const Value& value1, const Value& value2);
};
bool isEqual(const Value& value1, const Value& value2)
{
return (value1.m_value == value2.m_value);
}
int main()
{
Value v1{ 5 };
Value v2{ 6 };
std::cout << std::boolalpha << isEqual(v1, v2);
return 0;
}
A function can be a friend of more than one class at the same time.
For example, consider the following example:
#include
class Humidity;
class Temperature
{
private:
int m_temp {};
public:
Temperature(int temp=0)
: m_temp { temp }
{
}
friend void printWeather(const Temperature& temperature, const Humidity& humidity);
};
class Humidity
{
private:
int m_humidity {};
public:
Humidity(int humidity=0)
: m_humidity { humidity }
{
}
friend void printWeather(const Temperature& temperature, const Humidity& humidity);
};
void printWeather(const Temperature& temperature, const Humidity& humidity)
{
std::cout << "The temperature is " << temperature.m_temp <<
" and the humidity is " << humidity.m_humidity << '\n';
}
int main()
{
Humidity hum{10};
Temperature temp{12};
printWeather(temp, hum);
return 0;
}
There are two things worth noting about this example. First, because printWeather is a friend of both classes, it can access the private data from objects of both classes. Second, note the following line at the top of the example:
class Humidity;
However, unlike functions, classes have no return types or parameters, so class prototypes are always simply class ClassName, where ClassName is the name of the class.
A few additional notes on friend classes. First, even though Display is a friend of Storage, Display has no direct access to the *this pointer of Storage objects (because *this is a function parameter of Storage member functions, not a member of Storage). Second, just because Display is a friend of Storage, that does not mean Storage is also a friend of Display. If you want two classes to be friends of each other, both must declare the other as a friend. Finally, if class A is a friend of B, and B is a friend of C, that does not mean A is a friend of C.
Be careful when using friend functions and classes, because it allows the friend function or class to violate encapsulation. If the details of the class change, the details of the friend will also be forced to change. Consequently, limit your use of friend functions and classes to a minimum.
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).
Fortunately, this is also fixable in a couple of simple steps. First, we can add class Storage as a forward declaration. Second, we can move the definition of Display::displayItem() out of the class, after the full definition of Storage class.
Here’s what this looks like:
#include
class Storage; // forward declaration for class Storage
class Display
{
private:
bool m_displayIntFirst {};
public:
Display(bool displayIntFirst)
: m_displayIntFirst { displayIntFirst }
{
}
void displayItem(const Storage& storage); // forward declaration above needed for this declaration line
};
class Storage // full definition of Storage class
{
private:
int m_nValue {};
double m_dValue {};
public:
Storage(int nValue, double dValue)
: m_nValue { nValue }, m_dValue { dValue }
{
}
// Make the Display::displayItem member function a friend of the Storage class (requires seeing the full definition of class Display, as above)
friend void Display::displayItem(const Storage& storage);
};
// Now we can define Display::displayItem, which needs to have seen the full definition of class Storage
void Display::displayItem(const 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';
}
int main()
{
Storage storage(5, 6.7);
Display display(false);
display.displayItem(storage);
return 0;
}
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!
A friend function or class is a function or class that can access the private members of another class as though it was a member of that class. This allows the friend function or friend class to work intimately with the other class, without making the other class expose its private members (e.g. via access functions).
Friending is commonly used when defining overloaded operators (which we’ll cover in the next chapter), or less commonly, when two or more classes need to work together in an intimate way.
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.
An anonymous object is essentially a value that has no name. Because they have no name, there’s no way to refer to them beyond the point where they are created. Consequently, they have “expression scope”, meaning they are created, evaluated, and destroyed all within a single expression.
Here is the add() function rewritten using an anonymous object:
#include
int add(int x, int y)
{
return x + y; // an anonymous object is created to hold and return the result of x + y
}
int main()
{
std::cout << add(5, 3) << '\n';
return 0;
}
When the expression x + y is evaluated, the result is placed in an anonymous object. A copy of the anonymous object is then returned to the caller by value, and the anonymous object is destroyed.
#include
void printValue(int value)
{
std::cout << value;
}
int main()
{
printValue(5 + 3);
return 0;
}
In this case, the expression 5 + 3 is evaluated to produce the result 8, which is placed in an anonymous object. A copy of this anonymous object is then passed to the printValue() function, (which prints the value 8) and then is destroyed.
Note how much cleaner this keeps our code – we don’t have to litter the code with temporary variables that are only used once.
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.
Cents cents{ 5 }; // normal variable
Cents{ 7 }; // anonymous object
In fact, because cents1 and cents2 are only used in one place, we can anonymize this even further:
#include
class Cents
{
private:
int m_cents{};
public:
Cents(int cents)
: m_cents { cents }
{}
int getCents() const { return m_cents; }
};
Cents add(const Cents& c1, const Cents& c2)
{
return { c1.getCents() + c2.getCents() }; // return anonymous Cents value
}
int main()
{
std::cout << "I have " << add(Cents{ 6 }, Cents{ 8 }).getCents() << " cents.\n"; // print anonymous Cents value
return 0;
}
In C++, anonymous objects are primarily used either to pass or return values without having to create lots of temporary variables to do so. Memory allocated dynamically is also done so anonymously (which is why its address must be assigned to a pointer, otherwise we’d have no way to refer to it).
It is also worth noting that because anonymous objects have expression scope, they can only be used once (unless bound to a constant l-value reference, which will extend the lifetime of the temporary object to match the lifetime of the reference). If you need to reference a value in multiple expressions, you should use a named variable instead.
#include
class Fruit
{
public:
// Note: we've moved FruitType inside the class, under the public access specifier
// We've also changed it from an enum class to an enum
enum FruitType
{
apple,
banana,
cherry
};
private:
FruitType m_type {};
int m_percentageEaten { 0 };
public:
Fruit(FruitType type) :
m_type { type }
{
}
FruitType getType() const { return m_type; }
int getPercentageEaten() const { return m_percentageEaten; }
};
int main()
{
// Note: we access the FruitType via Fruit now
Fruit apple { Fruit::apple };
if (apple.getType() == Fruit::apple)
std::cout << "I am an apple";
else
std::cout << "I am not an apple";
return 0;
}
First, note that FruitType is now defined inside the class. Second, note that we’ve defined it under the public access specifier, so the type definition can be accessed from outside the class.
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 enumeration via a Fruit::FruitType:: scope qualifier. This double-scoping is unnecessary, so we’ve used a normal enum.
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!
Defining nested classes isn’t very common, but the C++ standard library does do so in some cases, such as with iterator classes.
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.
Here’s the class:
#include // for std::chrono functions
class Timer
{
private:
// Type aliases to make accessing nested type easier
using Clock = std::chrono::steady_clock;
using Second = std::chrono::duration<double, std::ratio<1> >;
std::chrono::time_point<Clock> m_beg { Clock::now() };
public:
void reset()
{
m_beg = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
}
};
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.
Finally, note that results are only valid for your machine’s architecture, OS, compiler, and system specs. You may get different results on other systems that have different strengths and weaknesses.
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.
Consider the following example:
int x { 2 };
int y { 3 };
std::cout << x + y << '\n';
When you see the expression x + y, you can translate this in your head to the function call operator+(x, y)
(where operator+ is the name of the function).
First, almost any existing operator in C++ can be overloaded. The exceptions are: conditional (?, sizeof, scope (:, member selector (.), pointer member selector (.*), typeid, and the casting operators.
Finally, all operators keep their default precedence and associativity (regardless of what they’re used for) and this can not be changed.
When overloading operators, it’s best to keep the function of the operators as close to the original intent of the operators as possible.
Furthermore, because operators don’t have descriptive names, it’s not always clear what they are intended to do. For example, operator+ might be a reasonable choice for a string class to do concatenation of strings. But what about operator-? What would you expect that to do? It’s unclear.
If the meaning of an overloaded operator is not clear and intuitive, use a named function instead.
It turns out that there are three different ways to overload operators: the member function way, the friend function way, and the normal function way.
The following example shows how to overload operator plus (+) in order to add two “Cents” objects together:
#include
class Cents
{
private:
int m_cents {};
public:
Cents(int cents) : m_cents{ cents } { }
// add Cents + Cents using a friend function
friend Cents operator+(const Cents& c1, const Cents& c2);
int getCents() const { return m_cents; }
};
// note: this function is not a member function!
Cents operator+(const Cents& c1, const Cents& c2)
{
// use the Cents constructor and operator+(int, int)
// we can access m_cents directly because this is a friend function
return c1.m_cents + c2.m_cents;
}
int main()
{
Cents cents1{ 6 };
Cents cents2{ 8 };
Cents centsSum{ cents1 + cents2 };
std::cout << "I have " << centsSum.getCents() << " cents.\n";
return 0;
}
We generally don’t recommend this, as non-trivial 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.
In the above example, note that we defined operator+(int, MinMax) by calling operator+(MinMax, int) (which produces the same result). This allows us to reduce the implementation of operator+(int, MinMax) to a single line, making our code easier to maintain by minimizing redundancy and making the function simpler to understand.
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. In cases where the implementation is trivial (e.g. a single line) it may or may not be worth doing this.
However, if you don’t need that access, you can write your overloaded operators as normal 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:
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’s internals, the better). However, don’t add additional access functions just to overload an operator as a normal function instead of a friend function!
Prefer overloading operators as normal functions instead of friends if it’s possible to do so without adding additional functions.
Overloading operator<< is similar to overloading operator+ (they are both binary operators), except that the parameter types are different.
#include
class Point
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Point(double x=0.0, double y=0.0, double z=0.0)
: m_x{x}, m_y{y}, m_z{z}
{
}
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 << ", " << point.m_z << ')'; // actual output done here
return out; // return std::ostream so we can chain calls to operator<<
}
int main()
{
const Point point1{2.0, 3.0, 4.0};
std::cout << point1 << '\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;
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>>:
#include
class Point
{
private:
double m_x{};
double m_y{};
double m_z{};
public:
Point(double x=0.0, double y=0.0, double z=0.0)
: m_x{x}, m_y{y}, m_z{z}
{
}
friend std::ostream& operator<< (std::ostream& out, const Point& point);
friend std::istream& operator>> (std::istream& in, 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 << ", " << point.m_z << ')';
return out;
}
std::istream& operator>> (std::istream& in, Point& point)
{
// Since operator>> is a friend of the Point class, we can access Point's members directly.
// note that parameter point must be non-const so we can modify the class members with the input values
in >> point.m_x;
in >> point.m_y;
in >> point.m_z;
return in;
}
Overloading operator<< and operator>> make it extremely easy to output your class to screen and accept user input from the console.
#include
class Cents
{
private:
int m_cents {};
public:
Cents(int cents)
: m_cents { cents } { }
// Overload Cents + int
Cents operator+ (int value);
int getCents() const { return m_cents; }
};
// note: this function is a member function!
// the cents parameter in the friend version is now the implicit *this parameter
Cents Cents::operator+ (int value)
{
return Cents { m_cents + value };
}
int main()
{
Cents cents1 { 6 };
Cents cents2 { cents1 + 2 };
std::cout << "I have " << cents2.getCents() << " cents.\n";
return 0;
}
In the member function version, the expression cents1 + 2 becomes function call cents1.operator+(2). Note that there is now only one explicit function parameter, and cents1 has become an object prefix. However, in lesson 13.10 – The hidden “this” pointer, we mentioned that the compiler implicitly converts an object prefix into a hidden leftmost parameter named *this. So in actuality, cents1.operator+(2)
becomes operator+(¢s1, 2)
, which is almost identical to the friend version.
The assignment (=), subscript ([]), function call (()), and member selection (->) operators must be overloaded as member functions, because the language requires them to be.
In lesson 14.4 – Overloading the I/O operators, we overloaded operator<< for our Point class using the friend function method. Here’s a reminder of how we did that:
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 normal function (preferred) or a friend.
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:
Because they only operate on the object they are applied to, typically unary operator overloads are implemented as member functions.
Only define overloaded operators that make intuitive sense for your class.
#include
class Cents
{
private:
int m_cents;
public:
Cents(int cents)
: m_cents{ cents }
{}
friend bool operator== (const Cents& c1, const Cents& c2) { return c1.m_cents == c2.m_cents; };
friend bool operator!= (const Cents& c1, const Cents& c2) { return !(operator==(c1, c2)); };
friend bool operator< (const Cents& c1, const Cents& c2) { return c1.m_cents < c2.m_cents; };
friend bool operator> (const Cents& c1, const Cents& c2) { return operator<(c2, c1); };
friend bool operator<= (const Cents& c1, const Cents& c2) { return !(operator>(c1, c2)); };
friend bool operator>= (const Cents& c1, const Cents& c2) { return !(operator<(c1, c2)); };
};
int main()
{
Cents dime{ 10 };
Cents nickel{ 5 };
if (nickel > dime)
std::cout << "a nickel is greater than a dime.\n";
if (nickel >= dime)
std::cout << "a nickel is greater than or equal to a dime.\n";
if (nickel < dime)
std::cout << "a dime is greater than a nickel.\n";
if (nickel <= dime)
std::cout << "a dime is greater than or equal to a nickel.\n";
if (nickel == dime)
std::cout << "a dime is equal to a nickel.\n";
if (nickel != dime)
std::cout << "a dime is not equal to a nickel.\n";
return 0;
}
This way, if we ever need to change something, we only need to update operator== and operator< instead of all six comparison operators!
C++20
C++20 introduces the spaceship operator (operator<=>), which allows us to reduce the number of comparison functions we need to write down to 2 at most, and sometimes just 1!
We intend to add a new lesson on this topic soon. Until then, consider this something to pique your interest – but you’ll have to go off-site to discover more.
Note that we return *this. The overloaded increment and decrement operators return the current implicit object so multiple operators can be “chained” together.
If the overloaded operator has an int parameter, the operator is a postfix overload. If the overloaded operator has no parameter, the operator is a prefix overload.
Here is the above Digit class with both prefix and postfix overloads:
class Digit
{
private:
int m_digit;
public:
Digit(int digit=0)
: m_digit{digit}
{
}
Digit& operator++(); // prefix has no parameter
Digit& operator--(); // prefix has no parameter
Digit operator++(int); // postfix has an int parameter
Digit operator--(int); // postfix has an int parameter
friend std::ostream& operator<< (std::ostream& out, const Digit& d);
};
// No parameter means this is prefix operator++
Digit& Digit::operator++()
{
// If our number is already at 9, wrap around to 0
if (m_digit == 9)
m_digit = 0;
// otherwise just increment to next number
else
++m_digit;
return *this;
}
// No parameter means this is prefix operator--
Digit& Digit::operator--()
{
// If our number is already at 0, wrap around to 9
if (m_digit == 0)
m_digit = 9;
// otherwise just decrement to next number
else
--m_digit;
return *this;
}
// int parameter means this is postfix operator++
Digit Digit::operator++(int)
{
// Create a temporary variable with our current digit
Digit temp{*this};
// Use prefix operator to increment this digit
++(*this); // apply operator
// return temporary result
return temp; // return saved state
}
// int parameter means this is postfix operator--
Digit Digit::operator--(int)
{
// Create a temporary variable with our current digit
Digit temp{*this};
// Use prefix operator to decrement this digit
--(*this); // apply operator
// return temporary result
return temp; // return saved state
}
std::ostream& operator<< (std::ostream& out, const Digit& d)
{
out << d.m_digit;
return out;
}
int main()
{
Digit digit(5);
std::cout << digit;
std::cout << ++digit; // calls Digit::operator++();
std::cout << digit++; // calls Digit::operator++(int);
std::cout << digit;
std::cout << --digit; // calls Digit::operator--();
std::cout << digit--; // calls Digit::operator--(int);
std::cout << digit;
return 0;
}
Finally, note that we’ve written the post-increment and post-decrement in such a way that it calls the pre-increment and pre-decrement to do most of the work. This cuts down on duplicate code, and makes our class easier to modify in the future.
class IntList
{
private:
int m_list[10]{};
public:
int& operator[] (int index);
};
int& IntList::operator[] (int index)
{
return m_list[index];
}
C++23 will add support for overloading operator[] with multiple subscripts.
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.
#include
class IntList
{
private:
int m_list[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // give this class some initial state for this example
public:
int& operator[] (int index);
int operator[] (int index) const; // could also return const int& if returning a non-fundamental type
};
int& IntList::operator[] (int index) // for non-const objects: can be used for assignment
{
return m_list[index];
}
int IntList::operator[] (int index) const // for const objects: can only be used for access
{
return m_list[index];
}
int main()
{
IntList list{};
list[2] = 3; // okay: calls non-const version of operator[]
std::cout << list[2] << '\n';
const IntList clist{};
clist[2] = 3; // compile error: calls const version of operator[], which returns by value. Cannot assign to this because it is an rvalue.
std::cout << clist[2] << '\n';
return 0;
}
#include // for assert()
#include // for std::size()
class IntList
{
private:
int m_list[10]{};
public:
int& operator[] (int index);
};
int& IntList::operator[] (int index)
{
assert(index >= 0 && index < std::size(m_list));
return m_list[index];
}
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.
Consider the following example:
#include // for assert()
#include // for std::size()
class IntList
{
private:
int m_list[10]{};
public:
int& operator[] (int index);
};
int& IntList::operator[] (int index)
{
assert(index >= 0 && index < std::size(m_list));
return m_list[index];
}
int main()
{
IntList* list{ new IntList{} };
list [2] = 3; // error: this will assume we're accessing index 2 of an array of IntLists
delete list;
return 0;
}
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.
Make sure you’re not trying to call an overloaded operator[] on a pointer to an object.
The proper syntax would be to dereference the pointer first (making sure to use parenthesis since operator[] has higher precedence than operator*), then call operator[]:
int main()
{
IntList* list{ new IntList{} };
(*list)[2] = 3; // get our IntList object, then call overloaded operator[]
delete list;
return 0;
}
This is ugly and error prone. Better yet, don’t set pointers to your objects if you don’t have to.
As a ridiculous example, just so you can see that it works:
#include
#include // C++17
class Stupid
{
private:
public:
void operator[] (std::string_view index);
};
// It doesn't make sense to overload operator[] to print something
// but it is the easiest way to show that the function parameter can be a non-integer
void Stupid::operator[] (std::string_view index)
{
std::cout << index;
}
int main()
{
Stupid stupid{};
stupid["Hello, world!"];
return 0;
}
As you would expect, this prints:
Hello, world!
Overloading operator[] to take a std::string parameter can be useful when writing certain kinds of classes, such as those that use words as indices.
The subscript operator is typically overloaded to provide direct access to individual elements from an array (or other similar structure) contained within a class. Because strings are often implemented as arrays of characters, operator[] is often implemented in string classes to allow the user to access a single character of the string.
A map is a class that stores elements as a key-value pair. The key must be unique, and is used to access the associated pair. In this quiz, we’re going to write an application that lets us assign grades to students by name, using a simple map class. The student’s name will be the key, and the grade (as a char) will be the value.
Now, let’s overload the () operator again, this time in a way that takes no parameters at all:
#include // for assert()
class Matrix
{
private:
double m_data[4][4]{};
public:
double& operator()(int row, int col);
double operator()(int row, int col) const;
void operator()();
};
double& Matrix::operator()(int row, int col)
{
assert(col >= 0 && col < 4);
assert(row >= 0 && row < 4);
return m_data[row][col];
}
double Matrix::operator()(int row, int col) const
{
assert(col >= 0 && col < 4);
assert(row >= 0 && row < 4);
return m_data[row][col];
}
void Matrix::operator()()
{
// reset all elements of the matrix to 0.0
for (int row{ 0 }; row < 4; ++row)
{
for (int col{ 0 }; col < 4; ++col)
{
m_data[row][col] = 0.0;
}
}
}
And here’s our new example:
#include
int main()
{
Matrix matrix{};
matrix(1, 2) = 4.5;
matrix(); // erase matrix
std::cout << matrix(1, 2) << '\n';
return 0;
}
which produces the result:
0
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!).
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).
Here’s a simple functor:
#include
class Accumulator
{
private:
int m_counter{ 0 };
public:
int operator() (int i) { return (m_counter += i); }
};
int main()
{
Accumulator acc{};
std::cout << acc(10) << '\n'; // prints 10
std::cout << acc(20) << '\n'; // 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.
Operator() is sometimes overloaded with two parameters to index multidimensional arrays, or to retrieve a subset of a one dimensional array (with the two parameters defining the subset to return). Anything else is probably better written as a member function with a more descriptive name.
Operator() is also often overloaded to create functors. Although simple functors (such as the example above) are fairly easily understood, functors are typically used in more advanced programming topics, and deserve their own lesson.
User-defined conversions allow us to convert our class into another data type.
To make things easier, we can provide a user-defined conversion by overloading the int typecast. This will allow us to cast our Cents class directly into an int. The following example shows how this is done:
class Cents
{
private:
int m_cents;
public:
Cents(int cents=0)
: m_cents{ cents }
{
}
// Overloaded int cast
operator int() const { return m_cents; }
int getCents() const { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};
There are three things to note:
Now in our example, we can call printInt() like this:
#include
int main()
{
Cents cents{ 7 };
printInt(cents); // print 7
std::cout << '\n';
return 0;
}
We can now also explicitly cast our Cents variable to an int:
Cents cents{ 7 };
int c{ static_cast<int>(cents) };
You can provide user-defined conversions 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 conversion:
class Dollars
{
private:
int m_dollars;
public:
Dollars(int dollars=0)
: m_dollars{ dollars }
{
}
// Allow us to convert Dollars into Cents
operator Cents() const { return Cents{ m_dollars * 100 }; }
};
This allows us to convert a Dollars object directly into a Cents object! This allows you to do something like this:
#include
class Cents
{
private:
int m_cents;
public:
Cents(int cents=0)
: m_cents{ cents }
{
}
// Overloaded int cast
operator int() const { return m_cents; }
int getCents() const { return m_cents; }
void setCents(int cents) { m_cents = cents; }
};
class Dollars
{
private:
int m_dollars;
public:
Dollars(int dollars=0)
: m_dollars{ dollars }
{
}
// Allow us to convert Dollars into Cents
operator Cents() const { return Cents { m_dollars * 100 }; }
};
void printCents(Cents cents)
{
std::cout << cents; // cents will be implicitly cast to an int here
}
int main()
{
Dollars dollars{ 9 };
printCents(dollars); // dollars will be implicitly cast to a Cents here
std::cout << '\n';
return 0;
}
Consequently, this program will print the value:
900
which makes sense, since 9 dollars is 900 cents!
Overloaded typecasts and converting constructors perform similar roles: an overloaded typecast allows us to define a function that converts some program-defined type A into some other type B. A converting constructor allows us to define a function that creates some program-defined type A from some other type B. So when should you use each?
In general, a converting constructor should be preferred to an overloaded typecast, as it allows the type being constructed to own the construction.
There are a few cases where an overloaded typecast should be used instead:
A copy constructor is a special type of constructor used to create a new object as a copy of an existing object (of the same type). 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.
#include
#include
class Fraction
{
private:
int m_numerator{};
int m_denominator{};
public:
// Default constructor
Fraction(int numerator=0, int denominator=1)
: m_numerator{numerator}, m_denominator{denominator}
{
assert(denominator != 0);
}
// Copy constructor
Fraction(const Fraction& fraction)
: m_numerator{fraction.m_numerator}, m_denominator{fraction.m_denominator}
// Note: We can access the members of parameter fraction directly, because we're inside the Fraction class
{
// no need to check for a denominator of 0 here since fraction must already be a valid Fraction
std::cout << "Copy constructor called\n"; // just to prove it works
}
friend std::ostream& operator<<(std::ostream& out, const Fraction& f1);
};
std::ostream& operator<<(std::ostream& out, const Fraction& f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
int main()
{
Fraction fiveThirds { 5, 3 }; // Direct initialize a Fraction, calls Fraction(int, int) constructor
Fraction fCopy { fiveThirds }; // Direct initialize -- with Fraction copy constructor
std::cout << fCopy << '\n';
}
When this program is run, you get:
Copy constructor called
5/3
The copy constructor we defined in the example above uses memberwise initialization, and is functionally equivalent to the one we’d get by default, except we’ve added an output statement to prove the copy constructor is being called.
Unlike with default constructors, it’s fine to use the default copy constructor if it meets your needs.
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.
It is a requirement that the parameter of a copy constructor be a (const) reference. This makes sense: if the argument were passed by value, then we’d need the copy constructor to copy the argument into the parameter of the copy constructor (which would result in an infinite recursion).
We can prevent copies of our classes from being made by making the copy constructor private:
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, one to call the copy constructor). However, the end result of initializing our defined object is essentially identical to just doing a direct initialization, which only takes one step.
For this reason, in such cases, the compiler is allowed to opt out of calling the copy constructor and just do a direct initialization instead. The process of omitting certain copy (or move) steps for performance purposes is called elision.
So although you wrote:
Fraction fiveThirds { Fraction{ 5, 3 } };
The compiler may change this to:
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, compilers are permitted (but not required) to perform copy elision in certain cases. In such cases, a copy constructor must be accessible (e.g. non-private), even if the actual copy is elided.
As of C++17, some cases of copy elision (including the example above) have been made mandatory. In these mandatory elision cases, the copy constructor does not need to be accessible (or even present) since it is guaranteed not to be needed!
The rule of three is a well known C++ principle that states that if a class requires a user-defined destructor, copy constructor, or copy assignment operator, then it probably requires all three. Not following the rule of three is likely to lead to malfunctioning code.
We discuss the copy assignment operator in lesson 14.15 – Overloading the assignment operator.
The rule of zero states, “Classes that have custom destructors, copy/move constructors or copy/move assignment operators should deal exclusively with ownership. Other classes should not have custom destructors, copy/move constructors or copy/move assignment operators.” (cite). In this context, “custom” means “user-defined”.
The best way to avoid having to implement these special functions is to use existing types that already support copy semantics (aka. not raw pointers). That way, the compiler-generated defaults for these special functions will behave as expected.
the rule of zero强调了类的单一职能原则,如果一个类专门用于资源管理,则这个类应该提供5个特殊函数,其他普通类则不需要。特殊函数 = default的写法等价于根本不定义这个函数,原因在于一个类中如果没有定义特殊函数,在大多数情况下编译器自动提供一个默认的特殊函数给这个类。
Consider the following line of code:
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. This lesson will examine topics related to copy initialization for classes.
Avoid using copy initialization, and use uniform initialization instead.
Fraction makeNegative(Fraction f) // ideally we should do this by const reference
{
f.setNumerator(-f.getNumerator());
return f;
}
int main()
{
Fraction fiveThirds(5, 3);
std::cout << makeNegative(fiveThirds);
return 0;
}
In the above program, function makeNegative takes a Fraction by value and also returns a Fraction by value. When we run this program, we get:
Copy constructor called
Copy constructor called
-5/3
The first copy constructor call happens when fiveThirds is passed as an argument into makeNegative() parameter f. The second call happens when the return value from makeNegative() is passed back to main().
In the above case, both the argument passed by value and the return value can not 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:
#include
class Something
{
public:
Something() = default;
Something(const Something&)
{
std::cout << "Copy constructor called\n";
}
};
Something foo()
{
return Something(); // copy constructor normally called here
}
Something goo()
{
Something s;
return s; // copy constructor normally called here
}
int main()
{
std::cout << "Initializing s1\n";
Something s1 = foo(); // copy constructor normally called here
std::cout << "Initializing s2\n";
Something s2 = goo(); // copy constructor normally called here
}
The above program would normally call the copy constructor 4 times – however, due to copy elision, it’s likely that your compiler will elide most or all of the cases. Visual Studio 2019 elides 3 (it doesn’t elide the case where goo() is returned), and GCC elides all 4.
By default, C++ will treat any constructor as an implicit conversion operator. Consider the following case:
This implicit conversion works for all kinds of initialization (direct and copy).
Constructors eligible to be used for implicit conversions are called converting constructors (or conversion constructors).
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 function’s name. Constructors and conversion functions made explicit will not be used for implicit conversions or copy initialization:
#include
#include
class MyString
{
private:
std::string m_string;
public:
// explicit keyword makes this constructor ineligible for implicit conversions
explicit 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;
}
friend std::ostream& operator<<(std::ostream& out, const MyString& s);
};
std::ostream& operator<<(std::ostream& out, const MyString& s)
{
out << s.m_string;
return out;
}
void printString(const MyString& s)
{
std::cout << s;
}
int main()
{
MyString mine = 'x'; // compile error, since MyString(int) is now explicit and nothing will match this
std::cout << mine;
printString('x'); // compile error, since MyString(int) can't be used for implicit conversions
return 0;
}
The above program will not compile, since MyString(int) was made explicit, and an appropriate converting constructor could not be found to implicitly convert ‘x’ to a MyString.
However, note that making a constructor explicit only prevents implicit conversions. Explicit conversions (via casting) are still allowed:
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).
MyString str{'x'}; // Allowed: initialization parameters may still be implicitly converted to match
Consider making your constructors and user-defined conversion member functions explicit to prevent implicit conversion errors.
A better way to resolve the issue is to use the “delete” keyword to delete the function:
#include
#include
class MyString
{
private:
std::string m_string;
public:
MyString(char) = delete; // any use of this constructor is an error
// explicit keyword makes this constructor ineligible for implicit conversions
explicit 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;
}
friend std::ostream& operator<<(std::ostream& out, const MyString& s);
};
std::ostream& operator<<(std::ostream& out, const MyString& s)
{
out << s.m_string;
return out;
}
int main()
{
MyString mine('x'); // compile error, since MyString(char) is deleted
std::cout << mine;
return 0;
}
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.
The copy assignment operator (operator=) is used to copy values from one object to another already existing object.
As of C++11, C++ also supports “Move assignment”. We discuss move assignment in lesson M.3 – Move constructors and move assignment.
The purpose of the copy constructor and the copy 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 copy assignment operator causes a lot of confusion for new programmers, but it’s really not all that difficult. Summarizing:
Overloading the copy assignment operator (operator=) is fairly straightforward, with one specific caveat that we’ll get to. The copy assignment operator must be overloaded as a member function.
// A simplistic implementation of operator= (see better implementation below)
Fraction& Fraction::operator= (const Fraction& fraction)
{
// 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;
}
This should all be pretty straightforward by now. Our overloaded operator= returns *this, so that we can chain multiple assignments together:
int main()
{
Fraction f1 { 5, 3 };
Fraction f2 { 7, 2 };
Fraction f3 { 9, 5 };
f1 = f2 = f3; // chained assignment
return 0;
}
However, in cases where an assignment operator needs to dynamically assign memory, self-assignment can actually be dangerous:
// A simplistic implementation of operator= (do not use)
MyString& MyString::operator= (const MyString& str)
{
// if data exists in the current string, delete it
if (m_data) delete[] m_data;
m_length = str.m_length;
m_data = nullptr;
// copy the data from str to the implicit object
if (m_length)
m_data = new char[static_cast<std::size_t>(str.m_length)];
for (int i { 0 }; i < str.m_length; ++i)
m_data[i] = str.m_data[i];
// return the existing object so we can chain this operator
return *this;
}
Now run the following program:
int main()
{
MyString alex { "Alex", 5 }; // Meet Alex
alex = alex; // Alex is himself
std::cout << alex; // Say your name, Alex
return 0;
}
You’ll probably get garbage output. What happened?
Fortunately, we can detect when self-assignment occurs. Here’s an updated implementation of our overloaded operator= for the MyString class:
MyString& MyString::operator= (const MyString& str)
{
// self-assignment check
if (this == &str)
return *this;
// if data exists in the current string, delete it
if (m_data) delete[] m_data;
m_length = str.m_length;
m_data = nullptr;
// copy the data from str to the implicit object
if (m_length)
m_data = new char[static_cast<std::size_t>(str.m_length)];
for (int i { 0 }; i < str.m_length; ++i)
m_data[i] = str.m_data[i];
// return the existing object so we can chain this operator
return *this;
}
By checking if the address of our implicit object is the same as the address of the object being passed in as a parameter, we can have our assignment operator just return immediately without doing any other work.
Because this is just a pointer comparison, it should be fast, and does not require operator== to be overloaded.
Typically the self-assignment check is skipped for copy constructors. Because the object being copy constructed is newly created, the only case where the newly created object can be equal to the object being copied is when you try to initialize a newly defined object with itself:
someClass c { c };
In such cases, your compiler should warn you that c is an uninitialized variable.
Second, the self-assignment check may be omitted in classes that can naturally handle self-assignment. Consider this Fraction class assignment operator that has a self-assignment guard:
// 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; // can handle self-assignment
m_denominator = fraction.m_denominator; // can handle self-assignment
// return the existing object so we can chain this operator
return *this;
}
If the self-assignment guard did not exist, this function would still operate correctly during a self-assignment (because all of the operations done by the function can handle self-assignment properly).
Because self-assignment is a rare event, some prominent C++ gurus recommend omitting the self-assignment guard even in classes that would benefit from it. We do not recommend this, as we believe it’s a better practice to code defensively and then selectively optimize later.
Unlike other operators, the compiler will provide a default public copy 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).
Just like other constructors and operators, you can prevent assignments from being made by making your copy assignment operator private or using the delete keyword:
#include
#include
class Fraction
{
private:
int m_numerator { 0 };
int m_denominator { 1 };
public:
// Default constructor
Fraction(int numerator = 0, int denominator = 1)
: m_numerator { numerator }, m_denominator { denominator }
{
assert(denominator != 0);
}
// Copy constructor
Fraction(const Fraction ©) = delete;
// Overloaded assignment
Fraction& operator= (const Fraction& fraction) = delete; // no copies through assignment!
friend std::ostream& operator<<(std::ostream& out, const Fraction& f1);
};
std::ostream& operator<<(std::ostream& out, const Fraction& f1)
{
out << f1.m_numerator << '/' << f1.m_denominator;
return out;
}
int main()
{
Fraction fiveThirds { 5, 3 };
Fraction f;
f = fiveThirds; // compile error, operator= has been deleted
std::cout << f;
return 0;
}
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!
Now, consider the following snippet of code:
#include
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;
}
While this code looks harmless enough, it contains an insidious problem that will cause the program to exhibit undefined behavior!
The root of this problem is the shallow copy done by the copy constructor – doing a shallow copy on pointer values in a copy constructor or overloaded assignment operator is almost always asking for trouble.
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.
Let’s go ahead and show how this is done for our MyString class:
// assumes m_data is initialized
void MyString::deepCopy(const MyString& source)
{
// 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 = nullptr;
}
// Copy constructor
MyString::MyString(const MyString& source)
{
deepCopy(source);
}
As you can see, this is quite a bit more involved than a simple shallow copy! First, we have to check to make sure source even has a string (line 11). If it does, then we allocate enough memory to hold a copy of that string (line 14). Finally, we have to manually copy the string (lines 17 and 18).
Now let’s do the overloaded assignment operator. The overloaded assignment operator is slightly trickier:
// Assignment operator
MyString& MyString::operator=(const MyString& source)
{
// check for self-assignment
if (this != &source)
{
// now do the deep copy
deepCopy(source);
}
return *this;
}
Note that our assignment operator is very similar to our copy constructor, but there are three major differences:
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!
#include
template <typename T>
T average(const T* myArray, int numValues)
{
T sum { 0 };
for (int count { 0 }; count < numValues; ++count)
sum += myArray[count];
sum /= numValues;
return sum;
}
class Cents
{
private:
int m_cents {};
public:
Cents(int cents)
: m_cents { cents }
{
}
friend std::ostream& operator<< (std::ostream& out, const Cents& cents)
{
out << cents.m_cents << " cents ";
return out;
}
Cents& operator+= (const Cents ¢s)
{
m_cents += cents.m_cents;
return *this;
}
Cents& operator/= (int x)
{
m_cents /= x;
return *this;
}
};
int main()
{
Cents centsArray[] { Cents { 5 }, Cents { 10 }, Cents { 15 }, Cents { 14 } };
std::cout << average(centsArray, 4) << '\n';
return 0;
}
Finally, our code will compile and run! Here is the result:
11 cents
Note that we didn’t have to modify average() at all to make it work with objects of type Cents. We simply had to define the operators used to implement average() for the Cents class, and the compiler took care of the rest!
If you do not supply a copy constructor, the compiler will create one for you. Compiler-provided copy constructors will use memberwise initialization, meaning each member of the copy is initialized from the original member. The copy constructor may be elided for optimization purposes, even if it has side-effects, so do not rely on your copy constructor actually executing.
New programmers often mix up when the assignment operator vs copy constructor are used, but it’s fairly straightforward:
By default, the copy constructor and assignment operators provided by the compiler do a memberwise initialization or assignment, which is a shallow copy. If your class dynamically allocates memory, this will likely lead to problems, as multiple objects will end up pointing to the same allocated memory. In this case, you’ll need to explicitly define these in order to do a deep copy. Even better, avoid doing your own memory management if you can and use classes from the standard library.
Similarly, programming is also full of recurring patterns, relationships and hierarchies. Particularly when it comes to programming objects, the same patterns that govern real-life objects are applicable to the programming objects we create ourselves. By examining these in more detail, we can better understand how to improve code reusability and write classes that are more extensible.
There are many different kinds of relationships two objects may have in real-life, and we use specific “relation type” words to describe these relationships. For example: a square “is-a” shape. A car “has-a” steering wheel. A computer programmer “uses-a” keyboard. A flower “depends-on” a bee for pollination. A student is a “member-of” a class. And your brain exists as “part-of” you (at least, we can reasonably assume so if you’ve gotten this far).
All of these relation types have useful analogies in C++.
In this chapter, we’ll explore the nuances of the relation types “part-of”, “has-a”, “uses-a”, “depends-on”, and “member-of”, and show how they can be useful in the context of C++ classes. We’ll also explore a couple of related topics that don’t fit nicely anywhere else.
Then we’ll devote the following two chapters to exploring “is-a” relationships, via C++’s inheritance model and virtual functions. Yup, it’s a biggie.
Broadly speaking, object composition models a “has-a” relationship between two objects. A car “has-a” transmission. Your computer “has-a” CPU. You “have-a” heart. The complex object is sometimes called the whole, or the parent. The simpler object is often called the part, child, or component.
There are two basic subtypes of object composition: composition and aggregation. We’ll examine composition in this lesson, and aggregation in the next.
A note on terminology: the term “composition” is often used to refer to both composition and aggregation, not just to the composition subtype. In this tutorial, we’ll use the term “object composition” when we’re referring to both, and “composition” when we’re referring specifically to the composition subtype.
To qualify as a composition, an object and a part must have the following relationship:
In a composition relationship, the object is responsible for the existence of the parts. Most often, this means the part is created when the object is created, and destroyed when the object is destroyed. But more broadly, it means the object manages the part’s lifetime in such a way that the user of the object does not need to get involved. For example, when a body is created, the heart is created too. When a person’s body is destroyed, their heart is destroyed too. Because of this, composition is sometimes called a “death relationship”.
And finally, the part doesn’t know about the existence of the whole. Your heart operates blissfully unaware that it is part of a larger structure. We call this a unidirectional relationship, because the body knows about the heart, but not the other way around.
While object composition models has-a type relationships (a body has-a heart, a fraction has-a denominator), we can be more precise and say that composition models “part-of” relationships (a heart is part-of a body, a numerator is part of a fraction). Composition is often used to model physical relationships, where one object is physically contained inside another.
The parts of an object composition can be singular or multiplicative – for example, a heart is a singular part of the body, but a body contains 10 fingers (which could be modeled as an array).
In general, if you can design a class using composition, you should design a class using composition. Classes designed using composition are straightforward, flexible, and robust (in that they clean up after themselves nicely).
The key point here is that the composition should manage its parts without the user of the composition needing to manage anything.
A good rule of thumb is that each class should be built to accomplish a single task. That task should either be the storage and manipulation of some kind of data (e.g. Point2D, std::string), OR the coordination of its members (e.g. Creature). Ideally not both.
To qualify as an aggregation, a whole object and its parts must have the following relationship:
In an aggregation, we also add parts as member variables. However, these member variables are typically either references or pointers that are used to point at objects that have been created outside the scope of the class. Consequently, an aggregation usually either takes the objects it is going to point to as constructor parameters, or it begins empty and the subobjects are added later via access functions or operators.
Because these parts exist outside of the scope of the class, when the class is destroyed, the pointer or reference member variable will be destroyed (but not deleted). Consequently, the parts themselves will still exist.
Let’s take a look at a Teacher and Department example in more detail. In this example, we’re going to make a couple of simplifications: First, the department will only hold one teacher. Second, the teacher will be unaware of what department they’re part of.
In this case, bob is created independently of department, and then passed into department‘s constructor. When department is destroyed, the m_teacher reference is destroyed, but the teacher itself is not destroyed, so it still exists until it is independently destroyed later in main().
Implement the simplest relationship type that meets the needs of your program, not what seems right in real-life.
Compositions:
Typically use normal member variables
Can use pointer members if the class handles object allocation/deallocation itself
Responsible for creation/destruction of parts
Aggregations:
Typically use pointer or reference members that point to or reference objects that live outside the scope of the aggregate class
Not responsible for creating/destroying parts
It is worth noting that the concepts of composition and aggregation can be mixed freely within the same class. It is entirely possible to write a class that is responsible for the creation/destruction of some parts but not others. For example, our Department class could have a name and a Teacher. The name would probably be added to the Department by composition, and would be created and destroyed with the Department. On the other hand, the Teacher would be added to the department by aggregation, and created/destroyed independently.
While aggregations can be extremely useful, they are also potentially more dangerous, because aggregations do not handle deallocation of their parts. Deallocations are left to an external party to do. If the external party no longer has a pointer or reference to the abandoned parts, or if it simply forgets to do the cleanup (assuming the class will handle that), then memory will be leaked.
For this reason, compositions should be favored over aggregations.
For a variety of historical and contextual reasons, unlike a composition, the definition of an aggregation is not precise – so you may see other reference material define it differently from the way we do. That’s fine, just be aware.
One final note: In the lesson 10.5 – Introduction to structs, members, and member selection, we defined aggregate data types (such as structs and classes) as data types that group multiple variables together. You may also run across the term aggregate class in your C++ journeys, which is defined as a struct or class that has no provided constructors, destructors, or overloaded assignment, has all public members, and does not use inheritance – essentially a plain-old-data struct. Despite the similarities in naming, aggregates and aggregation are different and should not be confused.
In the Department/Teacher example above, we used a reference in the Department to store the Teacher. This works fine if there is only one Teacher, but what if a Department has multiple Teachers? We’d like to store those Teachers in a list of some kind (e.g. a std::vector) but fixed arrays and the various standard library lists can’t hold references (because list elements must be assignable, and references can’t be reassigned).
std::vector<const Teacher&> m_teachers{}; // Illegal
Essentially, std::reference_wrapper is a class that acts like a reference, but also allows assignment and copying, so it’s compatible with lists like std::vector.
The good news is that you don’t really need to understand how it works to use it. All you need to know are three things:
1. std::reference_wrapper lives in the <functional> header.
2. When you create your std::reference_wrapper wrapped object, the object can’t be an anonymous object (since anonymous objects have expression scope, and this would leave the reference dangling).
3. When you want to get your object back out of std::reference_wrapper, you use the get() member function.
#include // std::reference_wrapper
#include
#include
#include
int main()
{
std::string tom{ "Tom" };
std::string berta{ "Berta" };
std::vector<std::reference_wrapper<std::string>> names{ tom, berta }; // these strings are stored by reference, not value
std::string jim{ "Jim" };
names.push_back(jim);
for (auto name : names)
{
// Use the get() member function to get the referenced string.
name.get() += " Beam";
}
std::cout << jim << '\n'; // Jim Beam
return 0;
}
To create a vector of const references, we’d have to add const before the std::string like so
// Vector of const references to std::string
std::vector<std::reference_wrapper<const std::string>> names{ tom, berta };
To qualify as an association, an object and another object must have the following relationship:
Unlike a composition or aggregation, where the part is a part of the whole object, in an association, the associated object is otherwise unrelated to the object. Just like an aggregation, the associated object can belong to multiple objects simultaneously, and isn’t managed by those objects. However, unlike an aggregation, where the relationship is always unidirectional, in an association, the relationship may be unidirectional or bidirectional (where the two objects are aware of each other).
The relationship between doctors and patients is a great example of an association. The doctor clearly has a relationship with his patients, but conceptually it’s not a part/whole (object composition) relationship. A doctor can see many patients in a day, and a patient can see many doctors (perhaps they want a second opinion, or they are visiting different types of doctors). Neither of the object’s lifespans are tied to the other.
We can say that association models as “uses-a” relationship. The doctor “uses” the patient (to earn income). The patient uses the doctor (for whatever health purposes they need).
Because associations are a broad type of relationship, they can be implemented in many different ways. However, most often, associations are implemented using pointers, where the object points at the associated object.
In this example, we’ll implement a bi-directional Doctor/Patient relationship, since it makes sense for the Doctors to know who their Patients are, and vice-versa.
In general, you should avoid bidirectional associations if a unidirectional one will do, as they add complexity and tend to be harder to write without making errors.
Sometimes objects may have a relationship with other objects of the same type. This is called a reflexive association. A good example of a reflexive association is the relationship between a university course and its prerequisites (which are also university courses).
In all of the previous cases, we’ve used either pointers or references to directly link objects together. However, in an association, this is not strictly required. Any kind of data that allows you to link two objects together suffices. In the following example, we show how a Driver class can have a unidirectional association with a Car without actually including a Car pointer or reference member:
Here’s a summary table to help you remember the difference between composition, aggregation, and association:
A dependency occurs when one object invokes another object’s functionality in order to accomplish some specific task. This is a weaker relationship than an association, but still, any change to object being depended upon may break functionality in the (dependent) caller. A dependency is always a unidirectional relationship.
There’s typically some confusion about what differentiates a dependency from an association.
In C++, associations are a relationship between two classes at the class level. That is, one class keeps a direct or indirect “link” to the associated class as a member. For example, a Doctor class has an array of pointers to its Patients as a member. You can always ask the Doctor who its patients are. The Driver class holds the id of the Car the driver object owns as an integer member. The Driver always knows what Car is associated with it.
Dependencies typically are not represented at the class level – that is, the object being depended on is not linked as a member. Rather, the object being depended on is typically instantiated as needed (like opening a file to write data to), or passed into a function as a parameter (like std::ostream in the overloaded operator<< above).
The value the container provides is largely in its ability to help organize and store items that are put inside it.
Similarly, a container class is a class designed to hold and organize multiple instances of another type (either another class, or a fundamental type).
Container classes typically implement a fairly standardized minimal set of functionality. Most well-defined containers will include functions that:
Sometimes certain container classes will omit some of this functionality. For example, arrays container classes often omit the insert and remove functions because they are slow and the class designer does not want to encourage their use.
Container classes implement a member-of relationship. For example, elements of an array are members-of (belong to) the array. Note that we’re using “member-of” in the conventional sense, not the C++ class member sense.
Container classes generally come in two different varieties. Value containers are compositions that store copies of the objects that they are holding (and thus are responsible for creating and destroying those copies). Reference containers are aggregations that store pointers or references to other objects (and thus are not responsible for creation or destruction of those objects).
In C++, containers typically only hold one type of data.
Despite the restrictions on their use, containers are immensely useful, and they make programming easier, safer, and faster.
We’ll also need some functions to help us clean up IntArrays. First, we’ll write a destructor, which simply deallocates any dynamically allocated data. Second, we’ll write a function called erase(), which will erase the array and set the length to 0.
~IntArray()
{
delete[] m_data;
// we don't need to set m_data to null or m_length to 0 here, since the object will be destroyed immediately after this function anyway
}
void erase()
{
delete[] m_data;
// We need to make sure we set m_data to nullptr here, otherwise it will
// be left pointing at deallocated memory!
m_data = nullptr;
m_length = 0;
}
Now let’s overload the [] operator so we can access the elements of the array. We should ensure the index parameter has a valid value, which we can do via by using the assert() function. We’ll also add an access function to return the length of the array. Here’s everything so far:
Although writing container classes can be pretty complex, the good news is that you only have to write them once. Once the container class is working, you can use and reuse it as often as you like without any additional programming effort required.
A few additional improvements that could/should be made: First, we could have made this a template class, so that it would work with any copyable type rather than just int. Second, we should add const overloads of various member functions to properly support const IntArrays.
One more thing: If a class in the standard library meets your needs, use that instead of creating your own. For example, instead of using IntArray, you’re better off using std::vector. It’s battle tested, efficient, and plays nicely with the other classes in the standard library. But sometimes you need a specialized container class that doesn’t exist in the standard library, so it’s good to know how to create your own when you need to. We’ll talk more about containers in the standard library once we’ve covered a few more fundamental topics.
In the previous lesson, we introduced the concept of container classes, and showed an example of an IntArray class that holds an array of integers:
int main()
{
// What happens if we try to use an initializer list with this container class?
IntArray array { 5, 4, 3, 2, 1 }; // this line doesn't compile
for (int count{ 0 }; count < 5; ++count)
std::cout << array[count] << ' ';
return 0;
}
This code won’t compile, because the IntArray class doesn’t have a constructor that knows what to do with an initializer list. As a result, we’re left initializing our array elements individually:
int main()
{
IntArray array(5);
array[0] = 5;
array[1] = 4;
array[2] = 3;
array[3] = 2;
array[4] = 1;
for (int count{ 0 }; count < 5; ++count)
std::cout << array[count] << ' ';
return 0;
}
That’s not so great.
When a compiler sees an initializer list, it automatically converts it into an object of type std::initializer_list. Therefore, if we create a constructor that takes a std::initializer_list parameter, we can create objects using the initializer list as an input.
std::initializer_list lives in the
Here’s our IntArray constructor that takes a std::initializer_list.
IntArray(std::initializer_list<int> list) // allow IntArray to be initialized via list initialization
: IntArray(static_cast<int>(list.size())) // use delegating constructor to set up initial array
{
// Now initialize our array from the list
int count{ 0 };
for (int element : list)
{
m_data[count] = element;
++count;
}
}
On line 1: As noted above, we have to use angled brackets to denote what type of element we expect inside the list. In this case, because this is an IntArray, we’d expect the list to be filled with int. Note that we don’t pass the list by const reference. Much like std::string_view, std::initializer_list is very lightweight and copies tend to be cheaper than an indirection.
On line 2: We delegate allocating memory for the IntArray to the other constructor via a delegating constructor (to reduce redundant code). This other constructor needs to know the length of the array, so we pass it list.size(), which contains the number of elements in the list. Note that list.size() returns a size_t (which is unsigned) so we need to cast to a signed int here. We use direct initialization, rather than brace initialization, because brace initialization prefers list constructors. Although the constructor would get resolved correctly, it’s safer to use direct initialization to initialize classes with list constructors if we don’t want to use the list constructor.
The body of the constructor is reserved for copying the elements from the list into our IntArray class. For some inexplicable reason, std::initializer_list does not provide access to the elements of the list via subscripting (operator[]). The omission has been noted many times to the standards committee and never addressed.
However, there are easy ways to work around the lack of subscripts. The easiest way is to use a for-each loop here. The ranged-based for loop steps through each element of the initialization list, and we can manually copy the elements into our internal array.
One caveat: Non-empty initializer lists will always favor a matching initializer_list constructor over other potentially matching constructors. Thus, this variable definition:
IntArray array { 5 };
would match to IntArray(std::initializer_list), not IntArray(int). If you want to match to IntArray(int) once a list constructor has been defined, you’ll need to use copy initialization or direct initialization. The same happens to std::vector and other container classes that have both a list constructor and a constructor with a similar type of parameter
std::vector<int> array(5); // Calls std::vector::vector(std::vector::size_type), 5 value-initialized elements: 0 0 0 0 0
std::vector<int> array{ 5 }; // Calls std::vector::vector(std::initializer_list), 1 element: 5
You can also use std::initializer_list to assign new values to a class by overloading the assignment operator to take a std::initializer_list parameter. This works analogously to the above. We’ll show an example of how to do this in the quiz solution below.
Note that if you implement a constructor that takes a std::initializer_list, you should ensure you do at least one of the following:
Here’s why: consider the following class (which doesn’t have any of these things), along with a list assignment statement:
If you provide list construction, it’s a good idea to provide list assignment as well.
Implementing a constructor that takes a std::initializer_list parameter allows us to use list initialization with our custom classes. We can also use std::initializer_list to implement other functions that need to use an initializer list, such as an assignment operator.
The process of building complex objects from simpler ones is called object composition. There are two types of object composition: composition, and aggregation.
In a dependency, one class uses another class to perform a task. The dependent class typically is not a member of the class using it, but rather is temporarily created, used, and then destroyed, or passed into a member function from an external source.
std::initializer_list can be used to implement constructors, assignment operators, and other functions that accept a list initialization parameter. std::initailizer_list lives in the
In the last chapter, we discussed object composition, where complex classes are constructed from simpler classes and types. Object composition is perfect for building new objects that have a “has-a” relationship with their parts. However, object composition is just one of the two major ways that C++ lets you construct complex classes. The second way is through inheritance, which models an “is-a” relationship between two objects.
Put into a diagram, the relationship between apples, bananas, and fruit might look something like this:
This diagram defines a hierarchy.
In this chapter, we’ll explore the basics of how inheritance works in C++.
Next chapter, we’ll explore how inheritance enables polymorphism (one of object-oriented programming’s big buzzwords) through virtual functions.
As we progress, we’ll also talk about inheritance’s key benefits, as well as some of the downsides.
class Supervisor: public Employee
{
public:
// This Supervisor can oversee a max of 5 employees
long m_overseesIDs[5]{};
};
All Supervisor objects inherit the functions and variables from both Employee and Person, and add their own m_overseesIDs member variable.
By constructing such inheritance chains, we can create a set of reusable classes that are very general (at the top) and become progressively more specific at each level of inheritance.
Inheriting from a base class means we don’t have to redefine the information from the base class in our derived classes. We automatically receive the member functions and member variables of the base class through inheritance, and then simply add the additional functions or member variables we want. This not only saves work, but also means that if we ever update or modify the base class (e.g. add new functions, or fix a bug), all of our derived classes will automatically inherit the changes!
For example, if we ever added a new function to Person, both Employee and Supervisor would automatically gain access to it. If we added a new variable to Employee, Supervisor would also gain access to it. This allows us to construct new classes in an easy, intuitive, and low-maintenance way!
Inheritance allows us to reuse classes by having other classes inherit their members. In future lessons, we’ll continue to explore how this works.
So when we instantiate an instance of Derived, first the Base portion of Derived is constructed (using the Base default constructor). Once the Base portion is finished, the Derived portion is constructed (using the Derived default constructor). At this point, there are no more derived classes, so we are done.
This process is actually easy to illustrate.
It is sometimes the case that classes are derived from other classes, which are themselves derived from other classes. For example:
#include
class A
{
public:
A()
{
std::cout << "A\n";
}
};
class B: public A
{
public:
B()
{
std::cout << "B\n";
}
};
class C: public B
{
public:
C()
{
std::cout << "C\n";
}
};
class D: public C
{
public:
D()
{
std::cout << "D\n";
}
};
Remember that C++ always constructs the “first” or “most base” class first. It then walks through the inheritance tree in order and constructs each successive derived class.
Here’s a short program that illustrates the order of creation all along the inheritance chain.
int main()
{
std::cout << "Constructing A: \n";
A a;
std::cout << "Constructing B: \n";
B b;
std::cout << "Constructing C: \n";
C c;
std::cout << "Constructing D: \n";
D d;
}
This code prints the following:
Constructing A:
A
Constructing B:
A
B
Constructing C:
A
B
C
Constructing D:
A
B
C
D
C++ constructs derived classes in phases, starting with the most-base class (at the top of the inheritance tree) and finishing with the most-child class (at the bottom of the inheritance tree). As each class is constructed, the appropriate constructor from that class is called to initialize that part of the class.
You will note that our example classes in this section have all used base class default constructors (for simplicity). In the next lesson, we will take a closer look at the role of constructors in the process of constructing derived classes (including how to explicitly choose which base class constructor you want your derived class to use).
Fortunately, C++ gives us the ability to explicitly choose which Base class constructor will be called! To do this, simply add a call to the Base class constructor in the member initializer list of the derived class:
class Derived: public Base
{
public:
double m_cost {};
Derived(double cost=0.0, int id=0)
: Base{ id } // Call Base(int) constructor with value id!
, m_cost{ cost }
{
}
double getCost() const { return m_cost; }
};
Now, when we execute this code:
#include
int main()
{
Derived derived{ 1.3, 5 }; // use Derived(double, int) constructor
std::cout << "Id: " << derived.getId() << '\n';
std::cout << "Cost: " << derived.getCost() << '\n';
return 0;
}
The base class constructor Base(int) will be used to initialize m_id to 5, and the derived class constructor will be used to initialize m_cost to 1.3!
Thus, the program will print:
Id: 5
Cost: 1.3
This may seem somewhat complex, but it’s actually very simple. All that’s happening is that the Derived constructor is calling a specific Base constructor to initialize the Base portion of the object. Because m_id lives in the Base portion of the object, the Base constructor is the only constructor that can initialize that value.
Note that it doesn’t matter where in the Derived constructor member initializer list the Base constructor is called – it will always execute first.
Classes in an inheritance chain work in exactly the same way.
#include
class A
{
public:
A(int a)
{
std::cout << "A: " << a << '\n';
}
};
class B: public A
{
public:
B(int a, double b)
: A{ a }
{
std::cout << "B: " << b << '\n';
}
};
class C: public B
{
public:
C(int a, double b, char c)
: B{ a, b }
{
std::cout << "C: " << c << '\n';
}
};
int main()
{
C c{ 5, 4.3, 'R' };
return 0;
}
Thus, this program prints:
A: 5
B: 4.3
C: R
It is worth mentioning that constructors can only call constructors from their immediate parent/base class. Consequently, the C constructor could not call or pass parameters to the A constructor directly. The C constructor can only call the B constructor (which has the responsibility of calling the A constructor).
When a derived class is destroyed, each destructor is called in the reverse order of construction. In the above example, when c is destroyed, the C destructor is called first, then the B destructor, then the A destructor.
When constructing a derived class, the derived class constructor is responsible for determining which base class constructor is called. If no base class constructor is specified, the default base class constructor will be used. In that case, if no default base class constructor can be found (or created by default), the compiler will display an error. The classes are then constructed in order from most base to most derived.
At this point, you now understand enough about C++ inheritance to create your own inherited classes!
To this point, you’ve seen the private and public access specifiers, which determine who can access the members of a class. As a quick refresher, public members can be accessed by anybody. Private members can only be accessed by member functions of the same class or friends. This means derived classes can not access private members of the base class directly!
class Base
{
private:
int m_private {}; // can only be accessed by Base members and friends (not derived classes)
public:
int m_public {}; // can be accessed by anybody
};
This is pretty straightforward, and you should be quite used to it by now.
C++ has a third access specifier that we have yet to talk about because it’s only useful in an inheritance context. The protected access specifier allows the class the member belongs to, friends, and derived classes to access the member. However, protected members are not accessible from outside the class.
class Base
{
public:
int m_public {}; // can be accessed by anybody
protected:
int m_protected {}; // can be accessed by Base members, friends, and derived classes
private:
int m_private {}; // can only be accessed by Base members and friends (but not derived classes)
};
class Derived: public Base
{
public:
Derived()
{
m_public = 1; // allowed: can access public base members from derived class
m_protected = 2; // allowed: can access protected base members from derived class
m_private = 3; // not allowed: can not access private base members from derived class
}
};
int main()
{
Base base;
base.m_public = 1; // allowed: can access public members from outside class
base.m_protected = 2; // not allowed: can not access protected members from outside class
base.m_private = 3; // not allowed: can not access private members from outside class
return 0;
}
In the above example, you can see that the protected base member m_protected is directly accessible by the derived class, but not by the public.
In general, it’s better to make your members private if you can, and only use protected when derived classes are planned and the cost to build and maintain an interface to those private members is too high.
Favor private members over protected members.
First, there are three different ways for classes to inherit from other classes: public, protected, and private.
To do so, simply specify which type of access you want when choosing the class to inherit from:
// Inherit from Base publicly
class Pub: public Base
{
};
// Inherit from Base protectedly
class Pro: protected Base
{
};
// Inherit from Base privately
class Pri: private Base
{
};
class Def: Base // Defaults to private inheritance
{
};
If you do not choose an inheritance type, C++ defaults to private inheritance (just like members default to private access if you do not specify otherwise).
Public inheritance is by far the most commonly used type of inheritance. In fact, very rarely will you see or use the other types of inheritance, so your primary focus should be on understanding this section. Fortunately, public inheritance is also the easiest to understand. When you inherit a base class publicly, inherited public members stay public, and inherited protected members stay protected. Inherited private members, which were inaccessible because they were private in the base class, stay inaccessible.
Public inheritance is what you should be using unless you have a specific reason not to.
Use public inheritance unless you have a specific reason to do otherwise.
Protected inheritance is the least common method of inheritance. It is almost never used, except in very particular cases. With protected inheritance, the public and protected members become protected, and private members stay inaccessible.
With private inheritance, all members from the base class are inherited as private. This means private members are inaccessible, and protected and public members become private.
Note that this does not affect the way that the derived class accesses members inherited from its parent! It only affects the code trying to access those members through the derived class.
Private inheritance can be useful when the derived class has no obvious relationship to the base class, but uses the base class for implementation internally. In such a case, we probably don’t want the public interface of the base class to be exposed through objects of the derived class (as it would be if we inherited publicly).
In practice, private inheritance is rarely used.
As a final note, although in the examples above, we’ve only shown examples using member variables, these access rules hold true for all members (e.g. member functions and types declared inside the class).
One obvious omission from the Base class is a way for the public to access m_value. We could remedy this by adding an access function in the Base class – but for the sake of example we’re going to add it to the derived class instead. Because m_value has been declared as protected in the Base class, Derived has direct access to it.
To add new functionality to a derived class, simply declare that functionality in the derived class like normal:
class Derived: public Base
{
public:
Derived(int value)
: Base { value }
{
}
int getValue() const { return m_value; }
};
Although it may be obvious, objects of type Base have no access to the getValue() function in Derived. The following does not work:
int main()
{
Base base { 5 };
std::cout << "base has value " << base.getValue() << '\n';
return 0;
}
This is because there is no getValue() function in Base. Function getValue() belongs to Derived. Because Derived is a Base, Derived has access to stuff in Base. However, Base does not have access to anything in Derived.
When derived.identify() is called, the compiler looks to see if function identify() has been defined in the Derived class. It hasn’t. Then it starts looking in the inherited classes (which in this case is Base). Base has defined an identify() function, so it uses that one. In other words, Base::identify() was used because Derived::identify() doesn’t exist.
This means that if the behavior provided by a base class is sufficient, we can simply use the base class behavior.
However, if we had defined Derived::identify() in the Derived class, it would have been used instead.
This means that we can make functions work differently with our derived classes by redefining them in the derived class!
Note that when you redefine a function in the derived class, the derived function does not inherit the access specifier of the function with the same name in the base class. It uses whatever access specifier it is defined under in the derived class. Therefore, a function that is defined as private in the base class can be redefined as public in the derived class, or vice-versa!
#include
class Base
{
private:
void print() const
{
std::cout << "Base";
}
};
class Derived : public Base
{
public:
void print() const
{
std::cout << "Derived ";
}
};
int main()
{
Derived derived;
derived.print(); // calls derived::print(), which is public
return 0;
}
Sometimes we don’t want to completely replace a base class function, but instead want to add additional functionality to it. In the above example, note that Derived::identify() completely hides Base::identify()! This may not be what we want. It is possible to have our derived function call the base version of the function of the same name (in order to reuse code) and then add additional functionality to it.
To have a derived function call a base function of the same name, simply do a normal function call, but prefix the function with the scope qualifier (the name of the base class and two colons). The following example redefines Derived::identify() so it first calls Base::identify() and then does its own additional stuff.
#include
class Derived: public Base
{
public:
Derived(int value)
: Base { value }
{
}
int getValue() const { return m_value; }
void identify() const
{
Base::identify(); // call Base::identify() first
std::cout << "I am a Derived\n"; // then identify ourselves
}
};
Calling function identify() without a scope resolution qualifier would default to the identify() in the current class, which would be Derived::identify(). This would cause Derived::identify() to call itself, which would lead to an infinite recursion!
There’s one bit of trickiness that we can run into when trying to call friend functions in base classes, such as operator<<. Because friend functions of the base class aren’t actually part of the base class, using the scope resolution qualifier won’t work. Instead, we need a way to make our Derived class temporarily look like the Base class so that the right version of the function can be called.
Fortunately, that’s easy to do, using static_cast. Here’s an example:
#include
class Base
{
private:
int m_value {};
public:
Base(int value)
: m_value{ value }
{
}
friend std::ostream& operator<< (std::ostream& out, const Base& b)
{
out << "In Base\n";
out << b.m_value << '\n';
return out;
}
};
class Derived : public Base
{
public:
Derived(int value)
: Base{ value }
{
}
friend std::ostream& operator<< (std::ostream& out, const Derived& d)
{
out << "In Derived\n";
// static_cast Derived to a Base object, so we call the right version of operator<<
out << static_cast<const Base&>(d);
return out;
}
};
int main()
{
Derived derived { 7 };
std::cout << derived << '\n';
return 0;
}
Because a Derived is-a Base, we can static_cast our Derived object into a Base, so that the appropriate version of operator<< that uses a Base is called.
This prints:
In Derived
In Base
7
Because Base::printValue() has been declared as protected, it can only be called by Base or its derived classes. The public can not access it.
Let’s define a Derived class that changes the access specifier of printValue() to public:
class Derived: public Base
{
public:
Derived(int value)
: Base { value }
{
}
// Base::printValue was inherited as protected, so the public has no access
// But we're changing it to public via a using declaration
using Base::printValue; // note: no parenthesis here
};
This means that this code will now work:
int main()
{
Derived derived { 7 };
// printValue is public in Derived, so this is okay
derived.printValue(); // prints 7
return 0;
}
You can only change the access specifiers of base members the derived class would normally be able to access. Therefore, you can never change the access specifier of a base member from private to protected or public, because derived classes do not have access to private members of the base class.
In C++, it is not possible to remove or restrict functionality from a base class other than by modifying the source code. However, in a derived class, it is possible to hide functionality that exists in the base class, so that it can not be accessed through the derived class. This can be done simply by changing the relevant access specifier.
For example, we can make a public member private:
#include
class Base
{
public:
int m_value{};
};
class Derived : public Base
{
private:
using Base::m_value;
public:
Derived(int value) : Base { value }
{
}
};
int main()
{
Derived derived{ 7 };
std::cout << derived.m_value; // error: m_value is private in Derived
Base& base{ static_cast<Base&>(derived) };
std::cout << base.m_value; // okay: m_value is public in Base
return 0;
}
This allowed us to take a poorly designed base class and encapsulate its data in our derived class. Alternatively, instead of inheriting Base’s members publicly and making m_value private by overriding its access specifier, we could have inherited Base privately, which would have caused all of Base’s member to be inherited privately in the first place.
However, it is worth noting that while m_value is private in the Derived class, it is still public in the Base class. Therefore the encapsulation of m_value in Derived can still be subverted by casting to Base& and directly accessing the member.
You can also mark member functions as deleted in the derived class, which ensures they can’t be called at all through a derived object:
#include
class Base
{
private:
int m_value {};
public:
Base(int value)
: m_value { value }
{
}
int getValue() const { return m_value; }
};
class Derived : public Base
{
public:
Derived(int value)
: Base { value }
{
}
int getValue() const = delete; // mark this function as inaccessible
};
int main()
{
Derived derived { 7 };
// The following won't work because getValue() has been deleted!
std::cout << derived.getValue();
return 0;
}
In the above example, we’ve marked the getValue() function as deleted. This means that the compiler will complain when we try to call the derived version of the function. Note that the Base version of getValue() is still accessible though. We can call Base::getValue() in one of two ways:
int main()
{
Derived derived { 7 };
// We can call the Base::getValue() function directly
std::cout << derived.Base::getValue();
// Or we can upcast Derived to a Base reference and getValue() will resolve to Base::getValue()
std::cout << static_cast<Base&>(derived).getValue();
return 0;
}
If using the casting method, we cast to a Base& rather than a Base to avoid making a copy of the Base portion of derived.
So far, all of the examples of inheritance we’ve presented have been single inheritance – that is, each inherited class has one and only one parent. However, C++ provides the ability to do multiple inheritance. Multiple inheritance enables a derived class to inherit members from more than one parent.
#include
#include
class Person
{
private:
std::string m_name;
int m_age{};
public:
Person(std::string_view name, int age)
: m_name{ name }, m_age{ age }
{
}
const std::string& getName() const { return m_name; }
int getAge() const { return m_age; }
};
class Employee
{
private:
std::string m_employer;
double m_wage{};
public:
Employee(std::string_view employer, double wage)
: m_employer{ employer }, m_wage{ wage }
{
}
const std::string& getEmployer() const { return m_employer; }
double getWage() const { return m_wage; }
};
// Teacher publicly inherits Person and Employee
class Teacher : public Person, public Employee
{
private:
int m_teachesGrade{};
public:
Teacher(std::string_view name, int age, std::string_view employer, double wage, int teachesGrade)
: Person{ name, age }, Employee{ employer, wage }, m_teachesGrade{ teachesGrade }
{
}
};
int main()
{
Teacher t{ "Mary", 45, "Boo", 14.3, 8 };
return 0;
}
A mixin (also spelled “mix-in”) is a small class that can be inherited from in order to add properties to a class. The name mixin indicates that the class is intended to be mixed into other classes, not instantiated on its own.
In the following example, the Box and Label classes are mixins that we inherit from in order to create a new Button class.
Because mixins are designed to add functionality to the derived class, not to provide an interface, mixins typically do not use virtual functions (covered in the next chapter). Instead, if a mixin class needs to be customized to work in a particular way, templates are typically used. For this reason, mixin classes are often templatized.
Perhaps surprisingly, a derived class can inherit from a mixin base class using the derived class as a template type parameter. Such inheritance is called Curiously Recurring Template Pattern (CRTP for short), which looks like this:
// The Curiously Recurring Template Pattern (CRTP)
template <class T>
class Mixin
{
// Mixin can use template type parameter T to access members of Derived
// via (static_cast(this))
};
class Derived : public Mixin<Derived>
{
};
You can find a simple example using CRTP here.
While multiple inheritance seems like a simple extension of single inheritance, multiple inheritance introduces a lot of issues that can markedly increase the complexity of programs and make them a maintenance nightmare. Let’s take a look at some of these situations.
First, ambiguity can result when multiple base classes contain a function with the same name. For example:
Second, and more serious is the diamond problem, which your author likes to call the “diamond of doom”. This occurs when a class multiply inherits from two classes which each inherit from a single base class. This leads to a diamond shaped inheritance pattern.
For example, consider the following set of classes:
class PoweredDevice
{
};
class Scanner: public PoweredDevice
{
};
class Printer: public PoweredDevice
{
};
class Copier: public Scanner, public Printer
{
};
We’ll talk more about ways to resolve the diamond problem in the next chapter (lesson 18.8 – Virtual base classes).
Many authors and experienced programmers believe multiple inheritance in C++ should be avoided at all costs due to the many potential problems it brings. Your author does not agree with this approach, because there are times and situations when multiple inheritance is the best way to proceed. However, multiple inheritance should be used extremely judiciously.
As an interesting aside, you have already been using classes written using multiple inheritance without knowing it: the iostream library objects std::cin and std::cout are both implemented using multiple inheritance!
Avoid multiple inheritance unless alternatives lead to more complexity.
Inheritance allows us to model an is-a relationship between two objects. The object being inherited from is called the parent class, base class, or superclass. The object doing the inheriting is called the child class, derived class, or subclass.
In the previous chapter, you learned all about how to use inheritance to derive new classes from existing classes. In this chapter, we are going to focus on one of the most important and powerful aspects of inheritance – virtual functions.
However, since Derived has a Base part, a more interesting question is whether C++ will let us set a Base pointer or reference to a Derived object. It turns out, we can!
#include
int main()
{
Derived derived{ 5 };
// These are both legal!
Base& rBase{ derived };
Base* pBase{ &derived };
std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
std::cout << "rBase is a " << rBase.getName() << " and has value " << rBase.getValue() << '\n';
std::cout << "pBase is a " << pBase->getName() << " and has value " << pBase->getValue() << '\n';
return 0;
}
This produces the result:
derived is a Derived and has value 5
rBase is a Base and has value 5
pBase is a Base and has value 5
This result may not be quite what you were expecting at first!
It turns out that because rBase and pBase are a Base reference and pointer, they can only see members of Base (or any classes that Base inherited). So even though Derived::getName() shadows (hides) Base::getName() for Derived objects, the Base pointer/reference can not see Derived::getName(). Consequently, they call Base::getName(), which is why rBase and pBase report that they are a Base rather than a Derived.
Note that this also means it is not possible to call Derived::getValueDoubled() using rBase or pBase. They are unable to see anything in Derived.
Now you might be saying, “The above examples seem kind of silly. Why would I set a pointer or reference to the base class of a derived object when I can just use the derived object?” It turns out that there are quite a few good reasons.
First, let’s say you wanted to write a function that printed an animal’s name and sound. Without using a pointer to a base class, you’d have to write it using overloaded functions, like this:
void report(const Cat& cat)
{
std::cout << cat.getName() << " says " << cat.speak() << '\n';
}
void report(const Dog& dog)
{
std::cout << dog.getName() << " says " << dog.speak() << '\n';
}
Not too difficult, but consider what would happen if we had 30 different animal types instead of 2. You’d have to write 30 almost identical functions! Plus, if you ever added a new type of animal, you’d have to write a new function for that one too. This is a huge waste of time considering the only real difference is the type of the parameter.
…
Second, let’s say you had 3 cats and 3 dogs that you wanted to keep in an array for easy access. Because arrays can only hold objects of one type, without a pointer or reference to a base class, you’d have to create a different array for each derived type, like this:
…
However, because both Cat and Dog are derived from Animal, it makes sense that we should be able to do something like this:
#include
#include
// Cat and Dog from the example above
int main()
{
const Cat fred{ "Fred" };
const Cat misty{ "Misty" };
const Cat zeke{ "Zeke" };
const Dog garbo{ "Garbo" };
const Dog pooky{ "Pooky" };
const Dog truffle{ "Truffle" };
// Set up an array of pointers to animals, and set those pointers to our Cat and Dog objects
// Note: to_array requires C++20 support (and at the time of writing, Visual Studio 2022 still doesn't support it correctly)
const auto animals{ std::to_array<const Animal*>({&fred, &garbo, &misty, &pooky, &truffle, &zeke }) };
// Before C++20, with the array size being explicitly specified
// const std::array animals{ &fred, &garbo, &misty, &pooky, &truffle, &zeke };
for (const auto animal : animals)
{
std::cout << animal->getName() << " says " << animal->speak() << '\n';
}
return 0;
}
While this compiles and executes, unfortunately the fact that each element of array “animals” is a pointer to an Animal means that animal->speak() will call Animal::speak() instead of the derived class version of speak() that we want. The output is
Fred says ???
Garbo says ???
Misty says ???
Pooky says ???
Truffle says ???
Zeke says ???
Although both of these techniques could save us a lot of time and energy, they have the same problem. The pointer or reference to the base class calls the base version of the function rather than the derived version. If only there was some way to make those base pointers call the derived version of a function instead of the base version…
Want to take a guess what virtual functions are for?
A virtual function is a special type of function that, when called, resolves to the most-derived version of the function that exists between the base and derived class. This capability is known as polymorphism. A derived function is considered a match if it has the same signature (name, parameter types, and whether it is const) and return type as the base version of the function. Such functions are called overrides.
To make a function virtual, simply place the “virtual” keyword before the function declaration.
Here’s the above example with a virtual function:
#include
#include
class Base
{
public:
virtual std::string_view getName() const { return "Base"; } // note addition of virtual keyword
};
class Derived: public Base
{
public:
virtual std::string_view getName() const { return "Derived"; }
};
int main()
{
Derived derived {};
Base& rBase{ derived };
std::cout << "rBase is a " << rBase.getName() << '\n';
return 0;
}
This example prints the result:
rBase is a Derived
Some modern compilers may give an error about having virtual functions and an accessible non-virtual destructor. If this is the case, add a virtual destructor to the base class. In the above program, add this to the definition of Base:
virtual ~Base() = default;
We discuss virtual destructors in lesson 18.4 – Virtual destructors, virtual assignment, and overriding virtualization.
Because rBase is a reference to the Base portion of a Derived object, when rBase.getName() is evaluated, it would normally resolve to Base::getName(). However, Base::getName() is virtual, which tells the program to go look and see if there are any more-derived versions of the function available between Base and Derived. In this case, it will resolve to Derived::getName()!
Note that virtual function resolution only works when a virtual member function is called through a pointer or reference to a class type object. This works because the compiler can differentiate the type of the pointer or reference from the type of the object being pointed to or referenced. We see this in example above.
Calling a virtual member function directly on an object (not through a pointer or reference) will always invoke the member function belonging to the same type of that object. For example:
C c{};
std::cout << c.getName(); // will always call C::getName
A a { c }; // copies the A portion of c into a (don't do this)
std::cout << a.getName(); // will always call A::getName
Virtual function resolution only works when a member function is called through a pointer or reference to a class type object.
Similarly, the following array example now works as expected:
Cat fred{ "Fred" };
Cat misty{ "Misty" };
Cat zeke{ "Zeke" };
Dog garbo{ "Garbo" };
Dog pooky{ "Pooky" };
Dog truffle{ "Truffle" };
// Set up an array of pointers to animals, and set those pointers to our Cat and Dog objects
Animal* animals[]{ &fred, &garbo, &misty, &pooky, &truffle, &zeke };
for (const auto* animal : animals)
std::cout << animal->getName() << " says " << animal->speak() << '\n';
Which produces the result:
Fred says Meow
Garbo says Woof
Misty says Meow
Pooky says Woof
Truffle says Woof
Zeke says Meow
A word of warning: the signature of the derived class function must exactly match the signature of the base class virtual function in order for the derived class function to be used. If the derived class function has different parameter types, the program will likely still compile fine, but the virtual function will not resolve as intended. In the next lesson, we’ll discuss how to guard against this.
Note that if a function is marked as virtual, all matching overrides in derived classes are also implicitly considered virtual, even if they are not explicitly marked as such.
If a function is virtual, all matching overrides in derived classes are implicitly virtual.
This does not work the other way around – a virtual override in a derived class does not implicitly make the base class function virtual.
Under normal circumstances, the return type of a virtual function and its override must match. Consider the following example:
class Base
{
public:
virtual int getValue() const { return 5; }
};
class Derived: public Base
{
public:
virtual double getValue() const { return 6.78; }
};
In this case, Derived::getValue() is not considered a matching override for Base::getValue() and compilation will fail.
Never call virtual functions from constructors or destructors.
Since most of the time you’ll want your functions to be virtual, why not just make all functions virtual? The answer is because it’s inefficient – resolving a virtual function call takes longer than resolving a regular one. Furthermore, the compiler also has to allocate an extra pointer for each class object that has one or more virtual functions. We’ll talk about this more in future lessons in this chapter.
1d)
#include
#include
class A
{
public:
virtual std::string_view getName() const { return "A"; }
};
class B: public A
{
public:
// note: no virtual keyword in B, C, and D
std::string_view getName() const { return "B"; }
};
class C: public B
{
public:
std::string_view getName() const { return "C"; }
};
class D: public C
{
public:
std::string_view getName() const { return "D"; }
};
int main()
{
C c {};
B& rBase{ c }; // note: rBase is a B this time
std::cout << rBase.getName() << '\n';
return 0;
}
C. Even though B and C aren’t marked as virtual functions, A::getName() is virtual and B::getName() and C::getName() are overrides. Therefore, B::getName() and C::getName() are considered implicitly virtual, and thus the call to rBase.getName() resolves to C::getName(), not B::getName().
1e)
#include
#include
class A
{
public:
virtual std::string_view getName() const { return "A"; }
};
class B: public A
{
public:
// Note: Functions in B, C, and D are non-const.
virtual std::string_view getName() { return "B"; }
};
class C: public B
{
public:
virtual std::string_view getName() { return "C"; }
};
class D: public C
{
public:
virtual std::string_view getName() { return "D"; }
};
int main()
{
C c {};
A& rBase{ c };
std::cout << rBase.getName() << '\n';
return 0;
}
A. This one is a little trickier. rBase is an A reference to a C object, so rBase.getName() would normally call A::getName(). But A::getName() is virtual, so it calls the most derived version of the function between A and C. And that is A::getName(). Because B::getName() and C::getName() are not const, they are not considered overrides! Consequently, this program prints A.
1f)
#include
#include
class A
{
public:
A() { std::cout << getName(); } // note addition of constructor
virtual std::string_view getName() const { return "A"; }
};
class B : public A
{
public:
virtual std::string_view getName() const { return "B"; }
};
class C : public B
{
public:
virtual std::string_view getName() const { return "C"; }
};
class D : public C
{
public:
virtual std::string_view getName() const { return "D"; }
};
int main()
{
C c {};
return 0;
}
A. Another tricky one. When we create a C object, the A part is constructed first. When the A constructor is called to do this, it calls virtual function getName(). Because the B and C parts of the class aren’t set up yet, this resolves to A::getName().
To address some common challenges with inheritance, C++ has two inheritance-related identifiers: override
and final
. Note that these identifiers are not keywords – they are normal words that have special meaning only when used in certain contexts. The C++ standard calls them “identifiers with special meaning”, but they are often referred to as “specifiers”.
Although final isn’t used very much, override is a fantastic addition that you should use regularly. In this lesson, we’ll take a look at both, as well as one exception to the rule that virtual function override return types must match.
If the function does not override a base class function (or is applied to a non-virtual function), the compiler will flag the function as an error.
#include
class A
{
public:
virtual std::string_view getName1(int x) { return "A"; }
virtual std::string_view getName2(int x) { return "A"; }
virtual std::string_view getName3(int x) { return "A"; }
};
class B : public A
{
public:
std::string_view getName1(short int x) override { return "B"; } // compile error, function is not an override
std::string_view getName2(int x) const override { return "B"; } // compile error, function is not an override
std::string_view getName3(int x) override { return "B"; } // okay, function is an override of A::getName3(int)
};
int main()
{
return 0;
}
Use the virtual keyword on virtual functions in a base class. Use the override specifier (but not the virtual keyword) on override functions in derived classes.
There may be cases where you don’t want someone to be able to override a virtual function, or inherit from a class. The final specifier can be used to tell the compiler to enforce this. If the user tries to override a function or inherit from a class that has been specified as final, the compiler will give a compile error.
In the case where we want to restrict the user from overriding a function, the final specifier is used in the same place the override specifier is, like so:
#include
class A
{
public:
virtual std::string_view getName() { return "A"; }
};
class B : public A
{
public:
// note use of final specifier on following line -- that makes this function no longer overridable
std::string_view getName() override final { return "B"; } // okay, overrides A::getName()
};
class C : public B
{
public:
std::string_view getName() override { return "C"; } // compile error: overrides B::getName(), which is final
};
In the above code, B::getName() overrides A::getName(), which is fine. But B::getName() has the final specifier, which means that any further overrides of that function should be considered an error. And indeed, C::getName() tries to override B::getName() (the override specifier here isn’t relevant, it’s just there for good practice), so the compiler will give a compile error.
In the case where we want to prevent inheriting from a class, the final specifier is applied after the class name:
#include
class A
{
public:
virtual std::string_view getName() { return "A"; }
};
class B final : public A // note use of final specifier here
{
public:
std::string_view getName() override { return "B"; }
};
class C : public B // compile error: cannot inherit from final class
{
public:
std::string_view getName() override { return "C"; }
};
In the above example, class B is declared final. Thus, when C tries to inherit from B, the compiler will give a compile error.
There is one special case in which a derived class virtual function override can have a different return type than the base class and still be considered a matching override. If the return type of a virtual function is a pointer or a reference to some class, override functions can return a pointer or a reference to a derived class. These are called covariant return types. Here is an example:
#include
#include
class Base
{
public:
// This version of getThis() returns a pointer to a Base class
virtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; }
void printType() { std::cout << "returned a Base\n"; }
};
class Derived : public Base
{
public:
// Normally override functions have to return objects of the same type as the base function
// However, because Derived is derived from Base, it's okay to return Derived* instead of Base*
Derived* getThis() override { std::cout << "called Derived::getThis()\n"; return this; }
void printType() { std::cout << "returned a Derived\n"; }
};
int main()
{
Derived d{};
Base* b{ &d };
d.getThis()->printType(); // calls Derived::getThis(), returns a Derived*, calls Derived::printType
b->getThis()->printType(); // calls Derived::getThis(), returns a Base*, calls Base::printType
return 0;
}
This prints:
called Derived::getThis()
returned a Derived
called Derived::getThis()
returned a Base
One interesting note about covariant return types: C++ can’t dynamically select types, so you’ll always get the type that matches the actual version of the function being called.
In the above example, we first call d.getThis(). Since d is a Derived, this calls Derived::getThis(), which returns a Derived*. This Derived* is then used to call non-virtual function Derived::printType().
Now the interesting case. We then call b->getThis(). Variable b is a Base pointer to a Derived object. Base::getThis() is a virtual function, so this calls Derived::getThis(). Although Derived::getThis() returns a Derived*, because Base version of the function returns a Base*, the returned Derived* is upcast to a Base*. Because Base::printType() is non-virtual, Base::printType() is called.
In other words, in the above example, you only get a Derived* if you call getThis() with an object that is typed as a Derived object in the first place.
Note that if printType() were virtual instead of non-virtual, the result of b->getThis() (an object of type Base*) would have undergone virtual function resolution, and Derived::printType() would have been called.
Covariant return types are often used in cases where a virtual member function returns a pointer or reference to the class containing the member function (e.g. Base::getThis() returns a Base*, and Derived::getThis() returns a Derived*). However, this isn’t strictly necessary. Covariant return types can be used in any case where the return type of the override member function is derived from the return type of the base virtual member function.
Although C++ provides a default destructor for your classes if you do not provide one yourself, it is sometimes the case that you will want to provide your own destructor (particularly if the class needs to deallocate memory). You should always make your destructors virtual if you’re dealing with inheritance. Consider the following example:
Whenever you are dealing with inheritance, you should make any explicit destructors virtual.
As with normal virtual member functions, if a base class function is virtual, all derived overrides will be considered virtual regardless of whether they are specified as such. It is not necessary to create an empty derived class destructor just to mark it as virtual.
Note that if you want your base class to have a virtual destructor that is otherwise empty, you can define your destructor this way:
virtual ~Base() = default; // generate a virtual default destructor
It is possible to make the assignment operator virtual. However, unlike the destructor case where virtualization is always a good idea, virtualizing the assignment operator really opens up a bag full of worms and gets into some advanced topics outside of the scope of this tutorial. Consequently, we are going to recommend you leave your assignments non-virtual for now, in the interest of simplicity.
Very rarely you may want to ignore the virtualization of a function. For example, consider the following code:
#include
class Base
{
public:
virtual ~Base() = default;
virtual std::string_view getName() const { return "Base"; }
};
class Derived: public Base
{
public:
virtual std::string_view getName() const { return "Derived"; }
};
There may be cases where you want a Base pointer to a Derived object to call Base::getName() instead of Derived::getName(). To do so, simply use the scope resolution operator:
#include
int main()
{
Derived derived {};
const Base& base { derived };
// Calls Base::getName() instead of the virtualized Derived::getName()
std::cout << base.Base::getName() << '\n';
return 0;
}
You probably won’t use this very often, but it’s good to know it’s at least possible.
Now that the final specifier has been introduced into the language, our recommendations are as follows:
Binding refers to the process that is used to convert identifiers (such as variable and function names) into addresses. Although binding is used for both variables and functions, in this lesson we’re going to focus on function binding.
Most of the function calls the compiler encounters will be direct function calls. A direct function call is a statement that directly calls a function. For example:
#include
void printValue(int value)
{
std::cout << value;
}
int main()
{
printValue(5); // This is a direct function call
return 0;
}
Direct function calls can be resolved using a process known as early binding. Early binding (also called static binding) means the compiler (or linker) is able to directly associate the identifier name (such as a function or variable name) with a machine address. Remember that all functions have a unique address. So when the compiler (or linker) encounters a function call, it replaces the function call with a machine language instruction that tells the CPU to jump to the address of the function.
In some programs, the function being called can’t be resolved until runtime. In C++, this is sometimes known as late binding (or in the case of virtual function resolution, dynamic binding).
In general programming terminology, the term “late binding” means the function being called is looked up by name at runtime. C++ does not support this. In C++, the term “late binding” is typically used in cases where the actual function being called is not known by the compiler or linker at the point where the function call is actually being made. Instead, the function to be called has been determined (at runtime) somewhere prior to that point.
In C++, one way to get late binding is to use function pointers. To review function pointers briefly, a function pointer is a type of pointer that points to a function instead of a variable. The function that a function pointer points to can be called by using the function call operator () on the pointer.
For example, the following code calls the add() function:
#include
int add(int x, int y)
{
return x + y;
}
int main()
{
// Create a function pointer and make it point to the add function
int (*pFcn)(int, int) { add };
std::cout << pFcn(5, 3) << '\n'; // add 5 + 3
return 0;
}
Late binding is slightly less efficient since it involves an extra level of indirection. With early binding, the CPU can jump directly to the function’s address. With late binding, the program has to read the address held in the pointer and then jump to that address. This involves one extra step, making it slightly slower. However, the advantage of late binding is that it is more flexible than early binding, because decisions about what function to call do not need to be made until run time.
In the next lesson, we’ll take a look at how late binding is used to implement virtual functions.
To implement virtual functions, C++ implementations typically use a form of late binding known as the virtual table. The virtual table is a lookup table of functions used to resolve function calls in a dynamic/late binding manner. The virtual table sometimes goes by other names, such as “vtable”, “virtual function table”, “virtual method table”, or “dispatch table”.
*__vptr
By now, you’re probably confused as to how these things all fit together, so let’s take a look at a simple example:
class Base
{
public:
virtual void function1() {};
virtual void function2() {};
};
class D1: public Base
{
public:
void function1() override {};
};
class D2: public Base
{
public:
void function2() override {};
};
Because there are 3 classes here, the compiler will set up 3 virtual tables: one for Base, one for D1, and one for D2.
Now, you might be saying, “But what if dPtr really pointed to a Base object instead of a D1 object. Would it still call D1::function1()?”. The answer is no.
int main()
{
Base b {};
Base* bPtr = &b;
bPtr->function1();
return 0;
}
In this case, when b is created, __vptr points to Base’s virtual table, not D1’s virtual table. Consequently, bPtr->__vptr will also be pointing to Base’s virtual table. Base’s virtual table entry for function1() points to Base::function1(). Thus, bPtr->function1() resolves to Base::function1(), which is the most-derived version of function1() that a Base object should be able to call.
So far, all of the virtual functions we have written have a body (a definition). However, C++ allows you to create a special kind of virtual function called a pure virtual function (or abstract function) that has no body at all! A pure virtual function simply acts as a placeholder that is meant to be redefined by derived classes.
To create a pure virtual function, rather than define a body for the function, we simply assign the function the value 0.
#include
class Base
{
public:
std::string_view sayHi() const { return "Hi"; } // a normal non-virtual function
virtual std::string_view getName() const { return "Base"; } // a normal virtual function
virtual int getValue() const = 0; // a pure virtual function
int doSomething() = 0; // Compile error: can not set non-virtual functions to 0
};
When we add a pure virtual function to our class, we are effectively saying, “it is up to the derived classes to implement this function”.
Using a pure virtual function has two main consequences: First, any class with one or more pure virtual functions becomes an abstract base class, which means that it can not be instantiated!
A pure virtual function is useful when we have a function that we want to put in the base class, but only the derived classes know what it should return. A pure virtual function makes it so the base class can not be instantiated, and the derived classes are forced to define these functions before they can be instantiated. This helps ensure the derived classes do not forget to redefine functions that the base class was expecting them to.
Just like with normal virtual functions, pure virtual functions can be called using a reference (or pointer) to a base class:
int main()
{
Cow cow{ "Betsy" };
Animal& a{ cow };
std::cout << a.speak(); // resolves to Cow::speak(), prints "Moo"
return 0;
}
In the above example, a.speak() resolves to Cow::speak() via virtual function resolution.
Since classes with pure virtual functions have a virtual function, don’t forget to make your destructor virtual too.
Visual Studio mistakenly allows pure virtual function declarations to be definitions, for example:
// wrong!
virtual std::string_view speak() const = 0
{
return "buzz";
}
This is wrong and cannot be disabled.
This capability isn’t used very commonly.
A destructor can be made pure virtual, but must be given a definition so that it can be called when a derived object is destructed.
An interface class is a class that has no member variables, and where all of the functions are pure virtual! In other words, the class is purely a definition, and has no actual implementation. Interfaces are useful when you want to define the functionality that derived classes must implement, but leave the details of how the derived class implements that functionality entirely up to the derived class.
Interface classes are often named beginning with an I. Here’s a sample interface class:
#include
class IErrorLog
{
public:
virtual bool openLog(std::string_view filename) = 0;
virtual bool closeLog() = 0;
virtual bool writeError(std::string_view errorMessage) = 0;
virtual ~IErrorLog() {} // make a virtual destructor in case we delete an IErrorLog pointer, so the proper derived destructor is called
};
Now, let’s say you need to write some code that uses an error log. If you write your code so it includes FileErrorLog or ScreenErrorLog directly, then you’re effectively stuck using that kind of error log (at least without recoding your program). For example, the following function effectively forces callers of mySqrt() to use a FileErrorLog, which may or may not be what they want.
#include // for sqrt()
double mySqrt(double value, FileErrorLog& log)
{
if (value < 0.0)
{
log.writeError("Tried to take square root of value less than 0");
return 0.0;
}
else
{
return std::sqrt(value);
}
}
A much better way to implement this function is to use IErrorLog instead:
#include // for sqrt()
double mySqrt(double value, IErrorLog& log)
{
if (value < 0.0)
{
log.writeError("Tried to take square root of value less than 0");
return 0.0;
}
else
{
return std::sqrt(value);
}
}
Now the caller can pass in any class that conforms to the IErrorLog interface.
Interface classes have become extremely popular because they are easy to use, easy to extend, and easy to maintain. In fact, some modern languages, such as Java and C#, have added an “interface” keyword that allows programmers to directly define an interface class without having to explicitly mark all of the member functions as abstract. Furthermore, although Java (prior to version 8) and C# will not let you use multiple inheritance on normal classes, they will let you multiple inherit as many interfaces as you like. Because interfaces have no data and no function bodies, they avoid a lot of the traditional problems with multiple inheritance while still providing much of the flexibility.
Abstract classes still have virtual tables, as these can still be used if you have a pointer or reference to the abstract class. The virtual table entry for a class with a pure virtual function will generally either contain a null pointer, or point to a generic function that prints an error (sometimes this function is named __purecall).
This produces the result:
PoweredDevice: 3
Scanner: 1
PoweredDevice: 3
Printer: 2
As you can see, PoweredDevice got constructed twice.
While this is often desired, other times you may want only one copy of PoweredDevice to be shared by both Scanner and Printer.
To share a base class, simply insert the “virtual” keyword in the inheritance list of the derived class. This creates what is called a virtual base class, which means there is only one base object. The base object is shared between all objects in the inheritance tree and it is only constructed once. Here is an example (without constructors for simplicity) showing how to use the virtual keyword to create a shared base class:
class PoweredDevice
{
};
class Scanner: virtual public PoweredDevice
{
};
class Printer: virtual public PoweredDevice
{
};
class Copier: public Scanner, public Printer
{
};
Now, when you create a Copier class object, you will get only one copy of PoweredDevice per Copier that will be shared by both Scanner and Printer.
However, this leads to one more problem: if Scanner and Printer share a PoweredDevice base class, who is responsible for creating it? The answer, as it turns out, is Copier. The Copier constructor is responsible for creating PoweredDevice. Consequently, this is one time when Copier is allowed to call a non-immediate-parent constructor directly:
class Copier: public Scanner, public Printer
{
public:
Copier(int scanner, int printer, int power)
: PoweredDevice{ power }, // PoweredDevice is constructed here
Scanner{ scanner, power }, Printer{ printer, power }
{
}
};
There are a few details that we would be remiss if we did not mention.
But what happens if instead of setting a Base reference or pointer to a Derived object, we simply assign a Derived object to a Base object?
int main()
{
Derived derived{ 5 };
Base base{ derived }; // what happens here?
std::cout << "base is a " << base.getName() << " and has value " << base.getValue() << '\n';
return 0;
}
Remember that derived has a Base part and a Derived part. When we assign a Derived object to a Base object, only the Base portion of the Derived object is copied. The Derived portion is not. In the example above, base receives a copy of the Base portion of derived, but not the Derived portion. That Derived portion has effectively been “sliced off”. Consequently, the assigning of a Derived class object to a Base class object is called object slicing (or slicing for short).
Because variable base does not have a Derived part, base.getName() resolves to Base::getName().
The above example prints:
base is a Base and has value 5
Used conscientiously, slicing can be benign. However, used improperly, slicing can cause unexpected results in quite a few different ways. Let’s examine some of those cases.
Now, you might think the above example is a bit silly. After all, why would you assign derived to base like that? You probably wouldn’t. However, slicing is much more likely to occur accidentally with functions.
Consider the following function:
void printName(const Base base) // note: base passed by value, not reference
{
std::cout << "I am a " << base.getName() << '\n';
}
This is a pretty simple function with a const base object parameter that is passed by value. If we call this function like such:
int main()
{
Derived d{ 5 };
printName(d); // oops, didn't realize this was pass by value on the calling end
return 0;
}
When you wrote this program, you may not have noticed that base is a value parameter, not a reference. Consequently, this program prints:
I am a Base
In this case, it’s pretty obvious what happened, but if your functions don’t actually print any identifying information like this, tracking down the error can be challenging.
Of course, slicing here can all be easily avoided by making the function parameter a reference instead of a pass by value (yet another reason why passing classes by reference instead of value is a good idea).
void printName(const Base& base) // note: base now passed by reference
{
std::cout << "I am a " << base.getName() << '\n';
}
int main()
{
Derived d{ 5 };
printName(d);
return 0;
}
This prints:
I am a Derived
Yet another area where new programmers run into trouble with slicing is trying to implement polymorphism with std::vector. Consider the following program:
Fixing this is a little more difficult. Many new programmers try creating a std::vector of references to an object, like this:
std::vector<Base&> v{};
Unfortunately, this won’t compile. The elements of std::vector must be assignable, whereas references can’t be reassigned (only initialized).
Another option is to use std::reference_wrapper, which is a class that mimics an reassignable reference:
#include // for std::reference_wrapper
#include
#include
#include
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{ value }
{
}
virtual std::string_view getName() const { return "Base"; }
int getValue() const { return m_value; }
};
class Derived : public Base
{
public:
Derived(int value)
: Base{ value }
{
}
std::string_view getName() const override { return "Derived"; }
};
int main()
{
std::vector<std::reference_wrapper<Base>> v{}; // a vector of reassignable references to Base
Base b{ 5 }; // b and d can't be anonymous objects
Derived d{ 6 };
v.push_back(b); // add a Base object to our vector
v.push_back(d); // add a Derived object to our vector
// Print out all of the elements in our vector
// we use .get() to get our element out of the std::reference_wrapper
for (const auto& element : v) // element has type const std::reference_wrapper &
std::cout << "I am a " << element.get().getName() << " with value " << element.get().getValue() << '\n';
return 0;
}
In the above examples, we’ve seen cases where slicing lead to the wrong result because the derived class had been sliced off. Now let’s take a look at another dangerous case where the derived object still exists!
Consider the following code:
int main()
{
Derived d1{ 5 };
Derived d2{ 6 };
Base& b{ d2 };
b = d1; // this line is problematic
return 0;
}
The first three lines in the function are pretty straightforward. Create two Derived objects, and set a Base reference to the second one.
The fourth line is where things go astray. Since b points at d2, and we’re assigning d1 to b, you might think that the result would be that d1 would get copied into d2 – and it would, if b were a Derived. But b is a Base, and the operator= that C++ provides for classes isn’t virtual by default. Consequently, only the Base portion of d1 is copied into d2.
As a result, you’ll discover that d2 now has the Base portion of d1 and the Derived portion of d2. In this particular example, that’s not a problem (because the Derived class has no data of its own), but in most cases, you’ll have just created a Frankenobject – composed of parts of multiple objects. Worse, there’s no easy way to prevent this from happening (other than avoiding assignments like this as much as possible).
Although C++ supports assigning derived objects to base objects via object slicing, in general, this is likely to cause nothing but headaches, and you should generally try to avoid slicing. Make sure your function parameters are references (or pointers) and try to avoid any kind of pass-by-value when it comes to derived classes.
One way would be to add a virtual function to Base called getName() (so we could call it with a Base pointer/reference, and have it dynamically resolve to Derived::getName()). But what would this function return if you called it with a Base pointer/reference that was actually pointing to a Base object? There isn’t really any value that makes sense. Furthermore, we would be polluting our Base class with things that really should only be the concern of the Derived class.
We know that C++ will implicitly let you convert a Derived pointer into a Base pointer (in fact, getObject() does just that). This process is sometimes called upcasting. However, what if there was a way to convert a Base pointer back into a Derived pointer? Then we could call Derived::getName() directly using that pointer, and not have to worry about virtual function resolution at all.
C++ provides a casting operator named dynamic_cast that can be used for just this purpose. Although dynamic casts have a few different capabilities, by far the most common use for dynamic casting is for converting base-class pointers into derived-class pointers. This process is called downcasting.
Using dynamic_cast works just like static_cast. Here’s our example main() from above, using a dynamic_cast to convert our Base pointer back into a Derived pointer:
int main()
{
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer
std::cout << "The name of the Derived is: " << d->getName() << '\n';
delete b;
return 0;
}
This prints:
The name of the Derived is: Apple
Because we haven’t checked for a null pointer result, we access d->getName(), which will try to dereference a null pointer, leading to undefined behavior (probably a crash).
In order to make this program safe, we need to ensure the result of the dynamic_cast actually succeeded:
int main()
{
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer
if (d) // make sure d is non-null
std::cout << "The name of the Derived is: " << d->getName() << '\n';
delete b;
return 0;
}
Always ensure your dynamic casts actually succeeded by checking for a null pointer result.
Also note that there are several cases where downcasting using dynamic_cast will not work:
It turns out that downcasting can also be done with static_cast. The main difference is that static_cast does no runtime type checking to ensure that what you’re doing makes sense. This makes using static_cast faster, but more dangerous. If you cast a Base* to a Derived*, it will “succeed” even if the Base pointer isn’t pointing to a Derived object. This will result in undefined behavior when you try to access the resulting Derived pointer (that is actually pointing to a Base object).
If you’re absolutely sure that the pointer you’re downcasting will succeed, then using static_cast is acceptable. One way to ensure that you know what type of object you’re pointing to is to use a virtual function. Here’s one (not great) way to do that:
Although all of the above examples show dynamic casting of pointers (which is more common), dynamic_cast can also be used with references. This works analogously to how dynamic_cast works with pointers.
Because C++ does not have a “null reference”, dynamic_cast can’t return a null reference upon failure. Instead, if the dynamic_cast of a reference fails, an exception of type std::bad_cast is thrown. We talk about exceptions later in this tutorial.
New programmers are sometimes confused about when to use static_cast vs dynamic_cast. The answer is quite simple: use static_cast unless you’re downcasting, in which case dynamic_cast is usually a better choice. However, you should also consider avoiding casting altogether and just use virtual functions.
There are some developers who believe dynamic_cast is evil and indicative of a bad class design. Instead, these programmers say you should use virtual functions.
In general, using a virtual function should be preferred over downcasting. However, there are times when downcasting is the better choice:
Run-time type information (RTTI) is a feature of C++ that exposes information about an object’s data type at runtime. This capability is leveraged by dynamic_cast. Because RTTI has a pretty significant space performance cost, some compilers allow you to turn RTTI off as an optimization. Needless to say, if you do this, dynamic_cast won’t function correctly.
Let’s start by overloading operator<< in the typical way:
#include
class Base
{
public:
virtual void print() const { std::cout << "Base"; }
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
out << "Base";
return out;
}
};
class Derived : public Base
{
public:
void print() const override { std::cout << "Derived"; }
friend std::ostream& operator<<(std::ostream& out, const Derived& d)
{
out << "Derived";
return out;
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{};
std::cout << d << '\n';
return 0;
}
Because there is no need for virtual function resolution here, this program works as we’d expect, and prints:
Base
Derived
Now, consider the following main() function instead:
int main()
{
Derived d{};
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}
This program prints:
Base
That’s probably not what we were expecting. This happens because our version of operator<< that handles Base objects isn’t virtual, so std::cout << bref calls the version of operator<< that handles Base objects rather than Derived objects.
Therein lies the challenge.
If this issue is that operator<< isn’t virtual, can’t we simply make it virtual?
The short answer is no. There are a number of reasons for this.
First, only member functions can be virtualized – this makes sense, since only classes can inherit from other classes, and there’s no way to override a function that lives outside of a class (you can overload non-member functions, but not override them). Because we typically implement operator<< as a friend, and friends aren’t considered member functions, a friend version of operator<< is ineligible to be virtualized. (For a review of why we implement operator<< this way, please revisit lesson 14.5 – Overloading operators using member functions).
Second, even if we could virtualize operator<< there’s the problem that the function parameters for Base::operator<< and Derived::operator<< differ (the Base version would take a Base parameter and the Derived version would take a Derived parameter). Consequently, the Derived version wouldn’t be considered an override of the Base version, and thus be ineligible for virtual function resolution.
So what’s a programmer to do?
The answer, as it turns out, is surprisingly simple.
First, we set up operator<< as a friend in our base class as usual. But rather than have operator<< determine what to print, we will instead have it call a normal member function that can be virtualized! This virtual function will do the work of determining what to print for each class.
In this first solution, our virtual member function (which we call identify()) returns a std::string, which is printed by Base::operator<<:
#include
class Base
{
public:
// Here's our overloaded operator<<
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
// Call virtual function identify() to get the string to be printed
out << b.identify();
return out;
}
// We'll rely on member function identify() to return the string to be printed
// Because identify() is a normal member function, it can be virtualized
virtual std::string identify() const
{
return "Base";
}
};
class Derived : public Base
{
public:
// Here's our override identify() function to handle the Derived case
std::string identify() const override
{
return "Derived";
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{};
std::cout << d << '\n'; // note that this works even with no operator<< that explicitly handles Derived objects
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}
This prints the expected result:
Base
Derived
Derived
The above solution works great, but has two potential shortcomings:
The latter is problematic in cases where we need a stream object, such as when we want to print the value of a member variable that has an overloaded operator<<.
Fortunately, it’s straightforward to modify the above example to resolve both of these issues. In the previous version, virtual function identify() returned a string to be printed by Base::operator<<. In this version, we’ll instead define virtual member function print() and delegate responsibility for printing directly to that function.
Here’s an example that illustrates the idea:
#include
class Base
{
public:
// Here's our overloaded operator<<
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
// Delegate printing responsibility for printing to virtual member function print()
return b.print(out);
}
// We'll rely on member function print() to do the actual printing
// Because print() is a normal member function, it can be virtualized
virtual std::ostream& print(std::ostream& out) const
{
out << "Base";
return out;
}
};
// Some class or struct with an overloaded operator<<
struct Employee
{
std::string name{};
int id{};
friend std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << "Employee(" << e.name << ", " << e.id << ")";
return out;
}
};
class Derived : public Base
{
private:
Employee m_e{}; // Derived now has an Employee member
public:
Derived(const Employee& e)
: m_e{ e }
{
}
// Here's our override print() function to handle the Derived case
std::ostream& print(std::ostream& out) const override
{
out << "Derived: ";
// Print the Employee member using the stream object
out << m_e;
return out;
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{ Employee{"Jim", 4}};
std::cout << d << '\n'; // note that this works even with no operator<< that explicitly handles Derived objects
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}
This outputs:
Base
Derived: Employee(Jim, 4)
Derived: Employee(Jim, 4)
In this version, Base::operator<< doesn’t do any printing itself. Instead, it just calls virtual member function print() and passes it the stream object. The print() function then uses this stream object to do its own printing. Base::print() uses the stream object to print “Base”. More interestingly, Derived::print() uses the stream object to print both “Derived: ” and to call Employee::operator<< to print the value of member m_e. The latter would have been more challenging to do in the prior example!
And so our journey through C++’s inheritance and virtual functions comes to an end. Fret not, dear reader, for there are plenty of other areas of C++ to explore as we move forward.
A virtual function is a special type of function that resolves to the most-derived version of the function (called an override) that exists between the base and derived class. To be considered an override, the derived class function must have the same signature and return type as the virtual base class function. The one exception is for covariant return types, which allow an override to return a pointer or reference to a derived class if the base class function returns a pointer or reference to the base class.
A function that is intended to be an override should use the override specifier to ensure that it is actually an override.
If you intend to use inheritance, you should make your destructor virtual, so the proper destructor is called if a pointer to the base class is deleted.
A virtual function can be made pure virtual/abstract by adding “= 0” to the end of the virtual function prototype. A class containing a pure virtual function is called an abstract class, and can not be instantiated. A class that inherits pure virtual functions must concretely define them or it will also be considered abstract. Pure virtual functions can have a body, but they are still considered abstract.
An interface class is one with no member variables and all pure virtual functions. These are often named starting with a capital I.
A virtual base class is a base class that is only included once, no matter how many times it is inherited by an object.
Dynamic casting can be used to convert a pointer to a base class object into a pointer to a derived class object. This is called downcasting. A failed conversion will return a null pointer.
The easiest way to overload operator<< for inherited classes is to write an overloaded operator<< for the most-base class, and then call a virtual member function to do the printing.