Source: LearnCpp.com by Alex
Basic Object-Oriented Programming
In C++, classes and structs are essentially the same. In fact, the following struct and class are effectively identical:
1 | struct DateStruct { |
Just like a struct declaration, a class declaration does not declare any memory. It only defines what the class looks like.
Using the m_
prefix for member variables helps distinguish member variables from function parameters or local variables inside member functions. This is useful for several reasons:
- First, when we see an assignment to a variable with the “m_” prefix, we know that we are changing the state of the class.
- Second, unlike function parameters or local variables, which are declared within the function, member variables are declared in the class definition. Consequently, if we want to know how a variable with the “m_” prefix is declared, we know that we should look in the class definition instead of within the function.
A note about structs in C++
Intro
In C, structs can only hold data, and do not have associated member functions. In C++, after designing classes (using the class keyword), Bjarne Stroustrup spent some amount of time considering whether structs (which were inherited from C) should be granted the ability to have member functions. Upon consideration, he determined that they should, in part to have a unified ruleset for both. So although we wrote the above programs using the class keyword, we could have used the struct keyword instead.
Many developers (including myself) feel this was the incorrect decision
to be made, as it can lead to dangerous assumptions: For example, it’s fair to assume a class will clean up after itself (e.g. a class that allocates memory will deallocate it before being destroyed), but it’s not safe to assume a struct will. Consequently, we recommend using the struct keyword for data-only structures, and the class keyword for defining objects that require both data and functions to be bundled together.
Rule: Use the struct keyword for data-only structures. Use the class keyword for objects that have both data and functions.
Public vs private access specifiers
Members are public by default.
1 | struct Date { |
Classes can (and almost always do) use multiple access specifiers to set the access levels of each of its members. There is no limit
to the number of access specifiers you can use in a class.
In general, member variables are usually made private
, and member functions are usually made public
.
Some programmers prefer to list private members first, because the public members typically use the private ones, so it makes sense to define the private ones first. However, a good counterargument is that users of the class don’t care about the private members, so the public ones should come first. Either way is fine.
Access functions and encapsulation
Encapsulation
In object-oriented programming, Encapsulation
(also called information hiding
) is the process of keeping the details about how an object is implemented hidden away from users of the object. Instead, users of the object access the object through a public interface. In this way, users are able to use the object without having to understand how it is implemented.
In C++, we implement encapsulation via access specifiers. Typically, all member variables of the class are made private (hiding the implementation details), and most member functions are made public (exposing an interface for the user). Although requiring users of the class to use the public interface may seem more burdensome than providing public access to the member variables directly, doing so actually provides a large number of useful benefits that help encourage class re-usability
and maintainability
.
Note: The word encapsulation is also sometimes used to refer to the packaging of data and functions that work on that data together. We prefer to just call that object-oriented programming.
- Rule: Only provide access functions when it makes sense for the user to be able to get or set a value directly.
- Rule: Getters should usually return by
value
orconst reference
,not non-const reference
.
Constructors
Unlike normal member functions, constructors have specific rules for how they must be named:
- Constructors must have the
same name
as the class (with the same capitalization) - Constructors have
no return type
(not even void)
Remember: Fundamental variables aren’t initialized by default. they should be initialized in constructors.
1 | Fraction(int, numerator, int denominator=1) { |
Copy initialization using equals with classes
Much like with fundamental variables, it’s also possible to initialize classes using copy initialization:
1 | int x = 6; // Copy initialize an integer |
However, we recommend you avoid this form of initialization with classes, as it may be less efficient. Although direct initialization
, uniform initialization
, and copy initialization
all work identically with fundamental types, copy-initialization does not work the same with classes (though the end-result is often the same). We’ll explore the differences in more detail in a future chapter.
Rule: Do not use copy initialization for your classes.
Reducing your constructors
In the above two-constructor declaration of the Fraction class, the default constructor is actually somewhat redundant. We could simplify this class as follows:
1 | public: |
Although this constructor is still a default constructor, it has now been defined in a way that it can accept one or two user-provided values as well.
1 | Fraction zero; // will call Fraction(0, 1) |
This may produce unexpected results for classes that have multiple default parameters of different types. Consider:
1 | Something(int n = 0, double d = 1.2) { // allows us to construct a Something(int, double), Something(int), or Something() |
If we want to be able to construct a Something
with only a double, we’ll need to add a second (non-default) constructor:
1 | // Default constructor |
An implicitly generated default constructor
If your class has no other constructors, C++ will automatically generate a public default constructor
for you. This is sometimes called an implicit constructor (or implicitly generated constructor).
If your class has any other constructors, the implicitly generated constructor will not be provided. For example:
1 | public: |
Generally speaking, it’s a good idea to always provide at least one constructor in your class. This explicitly allows you to control how objects of your class are allowed to be created, and will prevent your class from potentially breaking later when you add other constructors.
Rule: Provide at least one constructor for your class, even if it’s an empty default constructor.
Many new programmers are confused about whether constructors create the objects or not. They do not (the code the compiler creates does that).
Constructors actually serve two purposes. The primary purpose is to initialize objects
that have just been created. The secondary purpose is to determine whether creation of an object is allowed
. That is, an object of a class can only be created if a matching constructor can be found. This means that a class without any public constructors can’t be created!
Constructors are only intended to be used for initialization when the object is created. You should not try to call a constructor to re-initialize an existing object. While it may compile, the results will not be what you intended (instead, the compiler will create a temporary object and then discard it).
Some types of data (e.g. const
and reference
variables) must be initialized on the line they are declared. Consider the following example:
1 | class Something { |
Member initializer lists
To solve this problem, C++ provides a method for initializing class member variables (rather than assigning values to them after they are created) via a member initializer list
(often called a “member initialization list”). Do not confuse these with the similarly named initializer list that we can use to assign values to arrays.
1 | class Something { |
Const problem solved:
1 | class Something { |
Rule: Use member initializer lists to initialize your class member variables instead of assignment.
Or even in C++11, we can use m_value {5}
.
We strongly encourage you to begin using this new syntax (even if you aren’t using const or reference member variables) as initialization lists are required when doing composition and inheritance (subjects we will be covering shortly).
Rule: Favor uniform initialization over direct initialization if your compiler is C++11 compatible
Prior to C++11, you can only zero an array member via a member initialization list:
1 | class Something { |
However, in C++11, you can fully initialize a member array using uniform initialization:
1 | class Something { |
A member initialization list can also be used to initialize members that are classes.
1 | class B { |
Formatting your initializer lists
C++ gives you a lot of flexibility in how to format your initializer lists, and it’s really up to you how you’d like to proceed. But here are some recommendations:
If the initializer list fits on the same line as the function name, then it’s fine to put everything on one line:
1 | Something() : m_value1(1), m_value2(2.2), m_value3('c') { |
If the initializer list doesn’t fit on the same line as the function name, then it should go indented on the next line.
1 | Something(int value1, double value2, char value3='c') { // this line already has a lot of stuff on it |
Initializer list order
Perhaps surprisingly, variables in the initializer list are not initialized in the order that they are specified in the initializer list. Instead, they are initialized in the order in which they are declared in the class.
For best results, the following recommendations should be observed:
- Don’t initialize member variables in such a way that they are dependent upon other member variables being initialized first. In other words, ensure your member variables will properly initialize even if the initialization ordering is different).
- Initialize variables in the initializer list in the same order in which they are declared in your class. This isn’t strictly required so long as the prior recommendation has been followed, but your compiler may give you a
warning
if you don’t do so and you have all warnings turned on.
Non-static member initialization
Starting with C++11, it’s possible to give normal class member variables (those that don’t use the static
keyword) a default initialization value directly:
1 | class Rectangle { |
Non-static member initialization (also called in-class member initializers) provides default values for your member variables that your constructors will use if the constructors do not provide initialization values for the members themselves (via the member initialization list).
Overlapping and delegating constructors
Overlapping functionality problem:
1 | class Foo { |
The obvious solution doesn’t work prior to C++11
The obvious solution would be to have the Foo(int)
constructor call the Foo()
constructor to do the A portion.
1 | class Foo { |
However, with a pre-C++11 compiler, if you try to have one constructor call another constructor, it will often compile, but it will not work as you expect, and you will likely spend a long time trying to figure out why, even with a debugger.
Note: Prior to C++11, calling a constructor explicitly from another constructor creates a temporary object, initializes the temporary object using the constructor, and then discards it, leaving your original object unchanged.
Using a separate function
1 | Foo() { |
One small caveat: be careful when using Init() functions and dynamically allocated memory. Because Init() functions can be called by anyone at any time, dynamically allocated memory may or may not have already been allocated when Init() is called. Be careful to handle this situation appropriately – it can be slightly confusing, since a non-null pointer could be either dynamically allocated memory or an uninitialized pointer!
Delegating constructors in C++11
Starting with C++11, constructors are now allowed to call other constructors. This process is called delegating constructors
(or constructor chaining
).
1 | class Foo { |
This works exactly as you’d expect. Make sure you’re calling the constructor from the member initializer list, not in the body of the constructor.
Here’s another example of using delegating constructor to reduce redundant code:
1 |
|
Destructors
If your class object is holding any resources (e.g. dynamic memory, or a file or database handle), or if you need to do any kind of maintenance before the object is destroyed, the destructor is the perfect place to do so, as it is typically the last thing to happen before the object is destroyed.
Like constructors, destructors have specific naming rules:
- The destructor must have the same name as the class, preceded by a tilde (~).
- The destructor cannot take arguments.
- The destructor has no return type.
Generally you should not call a destructor explicitly (as it will be called automatically when the object is destroyed), since there are rarely cases where you’d want to clean up an object more than once. However, destructors may safely call other member functions since the object isn’t destroyed until after the destructor executes.
RAII (Resource Acquisition Is Initialization)
is a programming technique whereby resource use is tied to the lifetime of objects with automatic duration (e.g. non-dynamically allocated objects). In C++, RAII is implemented via classes with constructors and destructors. A resource (such as memory, a file or database handle, etc…) is typically acquired in the object’s constructor (though it can be acquired after the object is created if that makes sense). That resource can then be used while the object is alive. The resource is released in the destructor, when the object is destroyed. The primary advantage of RAII is that it helps prevent resource leaks (e.g. memory not being deallocated) as all resource-holding objects are cleaned up automatically.
Under the RAII paradigm, objects holding resources should not be dynamically allocated. This is because destructors are only called when an object is destroyed. For objects allocated on the stack, this happens automatically when the object goes out of scope, so there’s no need to worry about a resource eventually getting cleaned up. However, for dynamically allocated objects, the user is responsible for deletion – if the user forgets to do that, then the destructor will not be called, and the memory for both the class object and the resource being managed will be leaked!
The IntArray class at the top of this lesson is an example of a class that implements RAII – allocation in the constructor, deallocation in the destructor. std::string and std::vector are examples of classes in the standard library that follow RAII – dynamic memory is acquired on initialization, and cleaned up automatically on destruction.
Rule: If your class dynamically allocates memory, use the RAII paradigm, and don’t allocate objects of your class dynamically.
A warning about the exit() function
Note that if you use the exit()
function, your program will terminate and no destructors will be called. Be wary if you’re relying on your destructors to do necessary cleanup work (e.g. write something to a log file or database before exiting).
The hidden “this” pointer
1 | void setID(int id) { m_id = id; } |
Putting it all together:
- When we call
simple.setID(2)
, the compiler actually callssetID(&simple, 2)
. - Inside
setID()
, the “this” pointer holds the address of object simple. - Any member variables inside
setID()
are prefixed with “this->”. So when we saym_id = id
, the compiler is actually executingthis->m_id = id
, which in this case updatessimple.m_id
toid
.
Although using this->xxx
is acceptable coding practice, we find using the “m_” prefix on all member variable names provides a better solution by preventing duplicate names altogether!
Some developers prefer to explicitly add this-> to all class members. We recommend that you avoid doing so, as it tends to make your code less readable for little benefit. Using the m_
prefix is a more readable way to differentiate member variables from non-member (local) variables.
Chaining member functions
1 | std::cout << "Hello, " << userName; |
In this case, std::cout is an object, and operator <<
is a member function that operates on that object. The compiler evaluates the above snippet like this:
1 | (std::cout << "Hello, ") << userName; |
Here is a version of Calc with “chainable” functions:
1 | class Calc { |
Class code and header files
Defining member functions outside the class definition
C++ provides a way to separate the “declaration” portion of the class from the “implementation” portion. This is done by defining the class member functions outside of the class definition. To do so, simply define the member functions of the class as if they were normal functions, but prefix the class name to the function using the scope resolution operator (::) (same as for a namespace).
Here is our Date class with the Date constructor and setDate() function defined outside of the class definition. Note that the prototypes for these functions still exist inside the class definition, but the actual implementation has been moved outside:
1 | class Calc { |
Putting class definitions in a header file
Date.h:
1 |
|
Date.cpp:
1 |
|
Doesn’t defining member functions in the header violate the one-definition rule?
It depends. Member functions defined inside the class definition
are considered implicitly inline
. Inline functions are exempt from the one definition per program part of the one-definition rule. This means there is no problem defining trivial member functions (such as access functions) inside the class definition itself
.
Member functions defined outside the class definition are treated like normal functions
, and are subject to the one definition per program part of the one-definition rule. Therefore, those functions should be defined in a code file, not inside the header. The one exception for this is for template functions, which we’ll cover in a future chapter.
So what should I define in the .h file vs the .cpp file, and what inside the class definition vs outside?
You might be tempted to put all of your member function definitions into the header file, inside the class. While this will compile, there are a couple of downsides to doing so.
- First, as mentioned above, this
clutters up
your class definition. - Second, functions defined inside the class are
implicitly inline
. For larger functions that are called from many places, this can bloat your code. - Third, if you change anything about the code in the header, then you’ll need to
recompile
every file that includes that header. This can have a ripple effect, where one minor change causes the entire program to need to recompile (which can be slow). If you change the code in a .cpp file, only that .cpp file needs to be recompiled!
Therefore, we recommend the following:
- For classes that are used in only one file and aren’t generally reusable, define them directly in the single
.cpp
file they’re used in. - For classes used in multiple files, or intended for general reuse, define them in a
.h
file that has the same name as the class. - Trivial member functions (trivial
constructors
ordestructors
, access functions, etc…) can be defined inside the class. - Non-trivial member functions should be defined in a
.cpp
file that has the same name as the class.
Default parameters
Default parameters for member functions should be declared in the class definition (in the header file), where they can be seen by whomever #includes the header.
Libraries
Separating the class definition and class implementation is very common for libraries that you can use to extend your program. Throughout your programs, you’ve #included headers that belong to the standard library, such as iostream, string, vector, array, and other. Notice that you haven’t needed to add iostream.cpp, string.cpp, vector.cpp, or array.cpp into your projects. Your program needs the declarations from the header files in order for the compiler to validate you’re writing programs that are syntactically correct. However, the implementations for the classes that belong to the C++ standard library is contained in a precompiled file that is linked in at the link stage. You never see the code.
Outside of some open source software (where both .h and .cpp files are provided), most 3rd party libraries provide only header files, along with a precompiled library file. There are several reasons for this:
- It’s faster to link a precompiled library than to recompile it every time you need it.
- A single copy of a precompiled library can be shared by many applications, whereas compiled code gets compiled into executable copies that uses it (inflating file sizes).
- Intellectual property reasons (you don’t want people stealing your code).
Having your own files separated into declaration (header) and implementation (code file) is not only good form, it also makes creating your own custom libraries easier. Creating your own libraries is beyond the scope of these tutorials, but separating your declaration and implementation is a prerequisite to doing so.
Const class objects and member functions
1 | const Date date1; // initialize using default constructor |
Once a const class object has been initialized via constructor, any attempt to modify the member variables of the object is disallowed, as it would violate the const-ness of the object. This includes both changing member variables directly (if they are public), or calling member functions that set the value of member variables. Consider the following class:
1 | const Something something; |
Now, consider the following line of code:
1 | std::cout << something.getValue(); |
Perhaps surprisingly, this will also cause a compile error, even though getValue() doesn’t do anything to change a member variable! It turns out that const class objects can only explicitly call const member functions, and getValue() has not been marked as a const member function.
A const member function
is a member function that guarantees it will not modify the object or call any non-const member functions (as they may modify the object).
To make getValue() a const member function, we simply append the const keyword to the function prototype, after the parameter list, but before the function body:
1 | public: |
Futhermore, any const member function that attempts to change a member variable or call a non-const member function will cause a compiler error to occur.
Note that constructors cannot be marked as const. This is because constructors need to be able to initialize their member variables, and a const constructor would not be able to do so. Consequently, the language disallows const constructors.
Const references
Although instantiating const class objects is one way to create const objects, a more common way is by passing an object to a function by const reference.
We covered the merits of passing class arguments by const reference instead of by value. To recap, passing a class argument by value causes a copy of the class to be made (which is slow) – most of the time, we don’t need a copy, a reference to the original argument works just fine, and is more performant because it avoids the needless copy. We typically make the reference const in order to ensure the function does not inadvertently change the argument, and to allow the function to work with R-values (e.g. literals), which can be passed as const references, but not non-const references.
1 | void printDate(const Date &date) { |
Overloading const and non-const function
Finally, although it is not done very often, it is possible to overload a function in such a way to have a const and non-const version of the same function:
1 | class Something { |
The non-const version of getValue() will only work with non-const objects, but is more flexible in that we can use it to both read and write m_value (which we do by assigning the string “Hi”).
The const version of getValue() will work with either const or non-const objects, but returns a const reference, to ensure we can’t modify the const object’s data.
Because passing objects by const reference is common, your classes should be const-friendly. That means making any member function that does not modify the state of the class object const!
Static member variables
C++ introduces two more uses for the static keyword when applied to classes: static member variables, and static member functions. Fortunately, these uses are fairly straightforward. We’ll talk about static member variables in this lesson, and static member functions in the next.
Member variables of a class can be made static by using the static keyword. Unlike normal member variables, static member variables are shared by all objects of the class. Consider the following program, similar to the above:
1 | class Something { |
If the class is defined in a .h file, the static member definition is usually placed in the associated code file for the class (e.g. Something.cpp). If the class is defined in a .cpp file, the static member definition is usually placed directly underneath the class. Do not put the static member definition in a header file (much like a global variable, if that header file gets included more than once, you’ll end up with multiple definitions, which will cause a compile error).
Static member functions
What if static member variables are declared private:
1 | class Something { |
Normally we access private members through public member functions. While we could create a normal public member function to access s_value, we’d then need to instantiate an object of the class type to use the function! We can do better. It turns out that we can also make functions static.
1 | public: |
*Static member functions have no this pointer
C++ does not support static constructors
If initializing your static member variable requires executing code
(e.g. a loop), there are many different, somewhat obtuse ways of doing this. The following code presents one of the better methods. However, it is a little tricky, and you’ll probably never need it, so feel free to skip the remainder of this section if you desire.
1 | class MyClass { |
Friend functions and classes
A friend
function is a function that can access the private members of a class as though it were a member of that class. In all other regards, the friend function is just like a normal function
. A friend function may be either a normal function, or a member function of another class. To declare a friend function, simply use the friend keyword in front of the prototype of the function you wish to be a friend of the class. It does not matter whether you declare the friend function in the private or public section of the class.
1 | class Accumulator { |
Note that we have to pass an Accumulator object to reset(). This is because reset() is not a member function. It does not have a *this pointer, nor does it have an Accumulator object to work with, unless given one.
Note: A function can be a friend of more than one class at the same time.
It is also possible to make an entire class a friend of another class. This gives all of the members of the friend class access to the private members of the other class. Here is an example:
1 | class Storage { |
Friend member functions
Instead of making an entire class a friend, you can make a single member function a friend. This is done similarly to making a normal function a friend, except using the name of the member function with the className:: prefix included (e.g. Display::displayItem).
However, in actuality, this can be a little trickier than expected. Let’s convert the previous example to make Display::displayItem a friend member function. You might try something like this:
Note: But it is not a member function! (member func -> a friend)
1 | class Storage { |
However, it turns out this won’t work. In order to make a class’s member function a friend, the compiler has to have seen the full definition for the class of the friend member function
(not just a forward declaration). Since class Storage hasn’t seen the full definition for class Display yet, the compiler will error at the point where we try to make the member function a friend.
Fortunately, this is easily resolved simply by moving the definition of class Display before the definition of class Storage.
1 | // forward declaration |
If this seems like a pain – it is. Fortunately, this dance is only necessary because we’re trying to do everything in a single file. A better solution is to put each class definition in a separate header file, with the member function definitions in corresponding .cpp files. That way, all of the class definitions would have been visible immediately in the .cpp files, and no rearranging of classes or functions is necessary!
Summary
A friend function or class is a function or class that can access the private members of another class as though it were a member of that class. This allows the friend or class to work intimately with the other class, without making the other class expose its private members (e.g. via access functions).
Friending is uncommonly
used when two or more classes need to work together in an intimate way, or much more commonly, when defining overloading operators (which we’ll cover in chapter 9).
Note that making a specific member function a friend requires the full definition for the class of the member function to have been seen first.
Anonymous objects
Although our prior examples have been with built-in data types, it is possible to construct anonymous objects of our own class types as well. This is done by creating objects like normal, but omitting the variable name.
1 | Cents cents(5); |
More specifically,
1 | class Cents { |
It is worth noting that anonymous objects are treated as rvalues (not lvalues, which have an address). This means anonymous objects can only be passed or returned by value or const reference. Otherwise, a named variable must be used instead.
Nested types in classes
Unlike functions, which can’t be nested inside each other, in C++, types can be defined (nested) inside of a class. To do this, you simply define the type inside the class, under the appropriate access specifier.
1 | class Fruit { |
Classes essentially act as a namespace for any nested types. In the prior example, we were able to access enumerator APPLE directly, because the APPLE enumerator was placed into the global scope (we could have prevented this by using an enum class instead of an enum, in which case we’d have accessed APPLE via Fruit::FruitType::APPLE
instead). Now, because FruitType is considered to be part of the class, we access the APPLE enumerator by prefixing it with the class name: Fruit::APPLE.
Note that because enum classes also act like namespaces, if we’d nested FruitType inside Fruit as an enum class instead of an enum, we’d access the APPLE enumerator via Fruit::FruitType::APPLE
.
Although enumerations are probably the most common type that is nested inside a class, C++ will let you define other types within a class, such as typedefs, type aliases, and even other classes
!
Like any normal member of a class, nested classes have the same access to members of the enclosing class that the enclosing class does. However, the nested class does not have any special access to the “this” pointer of the enclosing class.
Defining nested classes isn’t very common, but the C++ standard library does do so in some cases, such as with iterator classes.
Timing your code (Timing API)
One easy way is to time your code to see how long it takes to run. C++11 comes with some functionality in the chrono library
to do just that. However, using the chrono library is a bit arcane. The good news is that we can easily encapsulate all the timing functionality we need into a class that we can then use in our own programs.
1 |
|
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.
- Make sure you’re using a release build target, not a debug build target. Debug build targets typically
turn optimization off
, and that optimization can have a significant impact on the results. - For best results, make sure your system isn’t doing anything CPU or memory intensive.
- When doing comparisons between two sets of code, be wary of what may change between runs that could impact timing. Your system may have kicked off an antivirus scan in the background, or maybe you’re streaming music now when you weren’t previously. Randomization can also impact timing.
Operator Overloading
In C++, operators are implemented as functions. By using function overloading on the operator functions, you can define your own versions of the operators that work with different data types (including classes that you’ve written). Using function overloading to overload operators is called operator overloading.
1 | x + y // translated as follow |
Resolving overloaded operators
When evaluating an expression containing an operator, the compiler uses the following rules:
- If all of the operands are
fundamental data types
, the compiler will call a built-in routine if one exists. If one does not exist, the compiler will produce a compiler error. - If any of the operands are
user data types
(e.g. one of your classes, or an enum type), the compiler looks to see whether the type has a matching overloaded operator function that it can call. If it can’t find one, it will try to convert one or more of the user-defined type operands into fundamental data types so it can use a matching built-in operator (via an overloaded typecast, which we’ll cover later in this chapter). If that fails, then it will produce a compile error.
Note:
First, almost any existing operator in C++ can be overloaded. The exceptions are: conditional (?:), sizeof, scope (::), member selector (.), and member pointer selector (.*).
Second, you can only overload the operators that exist. You cannot create new operators or rename existing operators. For example, you could not create an operator ** to do exponents.
Third, at least one of the operands in an overloaded operator must be a user-defined type. This means you can not overload the plus operator (+) to work with one integer and one double. However, you could overload the plus operator to work with an integer and a MyString.
Fourth, it is not possible to change the number of operands an operator supports.
Finally, all operators keep their default precedence and associativity (regardless of what they’re used for) and this cannot be changed.
- One example: Some new programmers attempt to overload the bitwise XOR operator (^) to do exponentiation. However, in C++, operator^ has a lower precedence level than the basic arithmetic operators, which causes expressions to evaluate incorrectly.
Rule: When overloading operators, it’s best to keep the function of the operators as close to the original intent
of the operators as possible.
Rule: If the meaning of an operator when applied to a custom class is not clear and intuitive, use a named function instead.
Overload the arithmetic operators using friend functions
1 | // addCents example |
Friend functions can be defined inside the class:
Even though friend functions are not members of the class, they can still be defined inside the class if desired:
1 | public: |
We generally don’t recommend this, as non-trivial (important) function definitions are better kept in a separate .cpp file, outside of the class definition. However, we will use this pattern in future tutorials to keep the examples concise.
Overloading operators for operands of different types
When C++ evaluates the expression x + y, x becomes the first parameter, and y becomes the second parameter. When x and y have the same type, it does not matter if you add x + y or y + x – either way, the same version of operator+ gets called. However, when the operands have different types, x + y does not call the same function as y + x.
For example, Cents(4) + 6
would call operator+(Cents, int)
, and 6 + Cents(4)
would call operator+(int, Cents)
.
Therefore, we need to write two functions separately:
1 | // add Cents + int using a friend function |
It is often possible to define overloaded operators by calling other overloaded operators. You should do so if and when doing so produces simpler code.
Overload operators using normal functions
Using a friend function to overload an operator is convenient because it gives you direct access to the internal members of the classes you’re operating on. In the initial Cents example above, our friend function version of operator+ accessed member variable m_cents
directly.
However, if you don’t need that access, you can write your overloaded operators as normal non-member functions. Note that the Cents class above contains an access function (getCents()
) that allows us to get at m_cents
without having to have direct access to private members. Because of this, we can write our overloaded operator+ as a non-friend:
One-file version:
1 | // note: this function is not a member function nor a friend function! |
Two-file version:
Cents.h:
1
2
3
4
5
6
7
8
9
10class Cents {
private:
int m_cents;
public:
Cents(int cents) { m_cents = cents; }
int getCents() const { return m_cents; }
};
// Need to explicitly provide prototype for operator+ so uses of operator+ in other files know this overload exists
Cents operator+(const Cents &c1, const Cents &c2);Cents.cpp
1
2
3
4
5
6
7
// note: this function is not a member function nor a friend function!
Cents operator+(const Cents &c1, const Cents &c2) {
// use the Cents constructor and operator+(int, int)
// we don't need direct access to private members here
return Cents(c1.getCents() + c2.getCents());
}
In general, a normal function should be preferred over a friend function if it’s possible to do so with the existing member functions available (the less functions touching your classes’ internals, the better). However, don’t add additional access functions just to overload an operator as a normal function instead of a friend function!
Rule: Prefer overloading operators as normal functions instead of friends if it’s possible to do so without adding additional functions.
Overload the I/O operators
1 | // std::ostream is the type for object std::cout |
The trickiest part here is the return type. With the arithmetic operators, we calculated and returned a single answer by value (because we were creating and returning a new result). However, if you try to return std::ostream by value, you’ll get a compiler error. This happens because std::ostream specifically disallows being copied.
In this case, we return the left-hand parameter as a reference. This not only prevents a copy of std::ostream from being made, it also allows us to “chain” output commands together, such as std::cout << point << std::endl;
.
Overloading operator>>
It is also possible to overload the input operator. This is done in a manner analogous to overloading the output operator. The key thing you need to know is that std::cin is an object of type std::istream. Here’s our Point class with an overloaded operator>>:
1 | std::istream& operator>> (std::istream &in, Point &point) { |
In conclusion, overloading operator<< and operator>> make it extremely easy to output your class to screen and accept user input from the console.
Overload operators using member functions
Overloading operators using a member function is very similar to overloading operators using a friend function. When overloading an operator using a member function:
- The overloaded operator must be added as a member function of the left operand.
- The left operand becomes the implicit *this object
- All other operands become function parameters.
Converting a friend overloaded operator to a member overloaded operator is easy:
- The left parameter is removed, because that parameter now becomes the implicit *this object.
- Inside the function body, all references to the left parameter can be removed (e.g. cents.m_cents becomes m_cents, which implicitly references the *this object).
1 | // friend version |
So if we can overload an operator as a friend or a member, which should we use? In order to answer that question, there’s a few more things you’ll need to know.
Not everything can be overloaded as a friend function:
The assignment (=), subscript ([]), function call (()), and member selection (->) operators must be overloaded as member functions, because the language requires them to be.
Not everything can be overloaded as a member function:
In lesson 9.3 – Overloading the I/O operators, we overloaded operator<< for our Point class using the friend function method.
However, we are not able to overload operator<< as a member function. Why not? Because the overloaded operator must be added as a member of the left operand. In this case, the left operand is an object of type std::ostream. std::ostream is fixed as part of the standard library. We can’t modify the class declaration to add the overload as a member function of std::ostream. This necessitates that operator<< be overloaded as a friend.
Similarly, although we can overload operator+(Cents, int) as a member function (as we did above), we can’t overload operator+(int, Cents) as a member function, because int isn’t a class we can add members to.
Typically, we won’t be able to use a member overload if the left operand is either not a class (e.g. int), or it is a class that we can’t modify (e.g. std::ostream).
When do we use a normal, friend, or member function overload?
One of the two is usually a better choice than the other.
When dealing with
binary operators
thatdon't modify the left operand
(e.g. operator+), the normal or friend function version is typically preferred, because it works for all parameter types (even when the left operand isn’t a class object, or is a class that is not modifiable). The normal or friend function version has the added benefit of “symmetry”, as all operands become explicit parameters (instead of the left operand becoming *this and the right operand becoming an explicit parameter).When dealing with
binary operators
thatdo modify the left operand
(e.g. operator+=), the member function version is typically preferred. In these cases, the leftmost operand will always be a class type, and having the object being modified become the one pointed to by *this is natural. Because the rightmost operand becomes an explicit parameter, there’s no confusion over who is getting modified and who is getting evaluated.Unary operators
are usually overloaded as member functions as well, since the member version has no parameters.
The following rules of thumb can help you determine which form is best for a given situation:
- If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a
member function
. - If you’re overloading a
unary operator
, do so as amember function
. - If you’re overloading a
binary operator
that modifies its left operand (e.g. operator+=), do so as amember function
if you can. - If you’re overloading a
binary operator
that does not modify its left operand (e.g. operator+), do so as anormal function or friend function
. (if left operands of basic types change, they are new variables, not the original ones)
Also, friend or normal?
- Depending on whether you’re going to access the private members of the class.
- But in the normal version, you can still call public getter and setter to access private members.
Overload unary operators +, -, and !
1 | public: |
Note that there’s no confusion between the negative operator- and the
minus operator- since they have a different number of parameters.
1 | public: |
Overload the comparison operators
1 | // normal or friend version (don't need const) |
What about operator< and operator>? What would it mean for a Car to be greater or less than another Car? We typically don’t think about cars this way. Since the results of operator< and operator> would not be immediately intuitive, it may be better to leave these operators undefined.
Recommendation: Don’t define overloaded operators that don’t make sense for your class.
However, there is one common exception to the above recommendation. What if we wanted to sort a list of Cars
? In such a case, we might want to overload the comparison operators to return the member (or members) you’re most likely to want to sort on. For example, an overloaded operator< for Cars might sort based on make and model alphabetically.
Overload the increment and decrement operators
Because the increment and decrement operators are both unary
operators and they modify
their operands, they’re best overloaded as member functions. We’ll tackle the prefix versions first because they’re the most straightforward.
1 | public: |
Overloading postfix increment and decrement
Normally, functions can be overloaded when they have the same name but a different number and/or different types of parameters. However, consider the case of the prefix and postfix increment and decrement operators. Both have the same name (eg. operator++), are unary, and take one parameter of the same type. So how it is possible to differentiate the two when overloading?
dummy = fake, false
The answer is that C++ uses a dummy variable
or dummy argument
for the postfix operators. This argument is a fake integer parameter that only serves to distinguish the postfix version of increment/decrement from the prefix version.
++i
: i is the (this), so the 1st parameter is not needed.i++
: i corresponds the 2nd parameter “int”, but we don’t use it; instead we’ll use the implicit, hidden 1st parameter (this).
I don’t know if it is correct. Too confusing. Maybe it just belongs to syntax stuff.
Here is the above Digit class with both prefix and postfix overloads:
1 | public: |
There are a few interesting things going on here.
First, note that we’ve distinguished the prefix from the postfix operators by providing an integer dummy parameter on the postfix version.
Second, because the dummy parameter is not used in the function implementation, we have not even given it a name. This tells the compiler to treat this variable as a
placeholder
, which means it won’t warn us that we declared a variable but never used it.Third, note that the prefix and postfix operators do the same job – they both increment or decrement the object. The difference between the two is in the value they return. The overloaded prefix operators return the object after it has been incremented or decremented. Consequently, overloading these is fairly straightforward. We simply increment or decrement our member variables, and then return
*this
.The postfix operators, on the other hand, need to return the state of the object before it is incremented or decremented. This leads to a bit of a conundrum – if we increment or decrement the object, we won’t be able to return the state of the object before it was incremented or decremented. On the other hand, if we return the state of the object before we increment or decrement it, the increment or decrement will never be called.
The typical way this problem is solved is to use a temporary variable that holds the value of the object before it is incremented or decremented. Then the object itself can be incremented or decremented. And finally, the temporary variable is returned to the caller. In this way, the caller receives a copy of the object before it was incremented or decremented, but the object itself is incremented or decremented. Note that this means the return value of the overloaded operator must be a non-reference, because we can’t return a reference to a local variable that will be destroyed when the function exits. Also note that this means the postfix operators are typically less efficient
than the prefix operators because of the added overhead of instantiating a temporary variable and returning by value instead of reference.
1 | // Note that there are two different objects! |
Overload the subscript operator
The subscript operator is one of the operators that must be overloaded as a member function
. An overloaded operator[] function will always take one parameter
(unary): the subscript that the user places between the hard braces. In our IntList case, we expect the user to pass in an integer index, and we’ll return an integer value back as a result.
1 | class IntList { |
This is both easy syntactically and from a comprehension standpoint. When list[2] evaluates, the compiler first checks to see if there’s an overloaded operator[] function. If so, it passes the value inside the hard braces (in this case, 2) as an argument to the function.
Note that although you can provide a default value for the function parameter, actually using operator[] without a subscript inside is not considered a valid syntax, so there’s no point.
Why operator[] returns a reference
Let’s take a closer look at how list[2] = 3 evaluates. Because the subscript operator has a higher precedence than the assignment operator, list[2] evaluates first. list[2] calls operator[], which we’ve defined to return a reference to list.m_list[2]. Because operator[] is returning a reference, it returns the actual list.m_list[2] array element. Our partially evaluated expression becomes list.m_list[2] = 3, which is a straightforward integer assignment.
Note: Because the result of operator[] can be used on the left-hand side of an assignment, the return value of operator[] must be l-value, because you can only take a reference of variables that have memory addresses.
Dealing with const objects
In the above IntList example, operator[] is non-const
, and we can use it as an l-value to change the state of non-const objects. However, what if our IntList object was const
? In this case, we wouldn’t be able to call the non-const version of operator[] because that would allow us to potentially change the state of a const object.
The good news is that we can define a non-const and a const version of operator[] separately. The non-const version will be used with non-const objects, and the const version with const-objects.
1 | public: |
Pointers to objects and overloaded operator[] don’t mix
If you try to call operator[] on a pointer to an object, C++ will assume you’re trying to index an array of objects of that type.
1 | IntList *list = new IntList; |
Because we can’t assign an integer to an IntList, this won’t compile. However, if assigning an integer was valid, this would compile and run, with undefined results.
Rule: Make sure you’re not trying to call an overloaded operator[] on a pointer to an object.
The function parameter does not need to be an integer
As mentioned above, C++ passes what the user types between the hard braces as an argument to the overloaded function. In most cases, this will be an integer value. However, this is not required – and in fact, you can define that your overloaded operator[] take a value of any type you desire. You could define your overloaded operator[] to take a double, a std::string, or whatever else you like.
1 | class Stupid { |
Overload the parenthesis operator
Why this one is special
All of the overloaded operators you have seen so far let you define the type of the operator’s parameters, but not the number of parameters (which is fixed based on the type of the operator). For example, operator== always takes two parameters, whereas operator! always takes one. The parenthesis operator() is a particularly interesting operator in that it allows you to vary both the type AND number
of parameters it takes.
There are two things to keep in mind:
- First, the parenthesis operator must be implemented as a
member function
. - Second, in non-object-oriented C++, the () operator is used to call functions. In the case of classes, operator() is just a normal operator that calls a function (named operator()) like any other overloaded operator.
The following content is about Matrix.
Operator[] is limited to a single parameter, it is not sufficient to let us index a two-dimensional array.
However, because the () operator can take as many parameters as we want it to have, we can declare a version of operator() that takes two integer index parameters, and use it to access our two-dimensional array. Here is an example of this:
1 | private: |
Because the () operator is so flexible, it can be tempting to use it for many different purposes. However, this is strongly discouraged, since the () symbol does not really give any indication of what the operator is doing. In our example above, it would be better to have written the erase functionality as a function called clear() or erase(), as matrix.erase() is easier to understand than matrix() (which could do anything!).
Having fun with functors
Operator() is also commonly overloaded to implement functors
(or function object), which are classes that operate like functions. The advantage of a functor over a normal function is that functors can store data in member variables (since they are classes).
1 | class Accumulator { |
Note that using our Accumulator looks just like making a normal function call, but our Accumulator object is storing an accumulated value.
You may wonder why we couldn’t do the same thing with a normal function and a static local variable to preserve data between function calls. We could, but because functions only have one
global instance, we’d be limited to using it for one thing at a time. With functors, we can instantiate as many
separate functor objects as we need and use them all simultaneously.
Overload typecasts
C++ already knows how to convert between the built-in data types. However, it does not know how to convert any of our user-defined classes. That’s where overloading the typecast operators comes into play.
1 | void printInt(int value) { |
If we have already written a lot of functions that take integers as parameters, our code will be littered with calls to getCents(), which makes it more messy than it needs to be.
To make things easier, we can overload the int typecast, which will allow us to cast our Cents class object directly into an int. The following example shows how this is done:
1 | class Cents { |
There are two things to note:
- To overload the function that casts our class to an int, we write a new function in our class called
operator int()
. Note that there is a space between the word operator and the type we are casting to. - Casting operators do not have a return type. C++ assumes you will be returning the correct type.
We can now also explicitly cast our Cents variable to an int by static_cast<int>()
:
1 | Cents cents(7); |
You can overload cast operators for any data type you wish, including your own user-defined data types
!
Here’s a new class called Dollars that provides an overloaded Cents cast operator:
1 | class Dollars { |
The copy constructor
Recap the types of initialization that C++ supports:
- direct initialization
- uniform initialization
- copy initialization
With direct and uniform initialization, the object being created is directly
initialized. However, copy initialization is a little more complicated. We’ll explore copy initialization in more detail in the next lesson. But in order to do that effectively, we need to take a short detour.
1 | int main() { |
The answer is that this line is calling Fraction’s copy constructor
. A copy constructor is a special type of constructor used to create a new object as a copy of an existing object. And much like a default constructor, if you do not provide a copy constructor for your classes, C++ will create a public copy constructor for you. Because the compiler does not know much about your class, by default, the created copy constructor utilizes a method of initialization called memberwise initialization
. Memberwise initialization simply means that each member of the copy is initialized directly from the member of the class being copied. In the above example, fCopy.m_numerator
would be initialized from fiveThirds.m_numerator
, etc.
Just like we can explicitly define a default constructor, we can also explicitly define a copy constructor. The copy constructor looks just like you’d expect it to:
1 | // Copy constructor |
One interesting note: You’ve already seen a few examples of overloaded operator<<, where we’re able to access the private members of parameter f1 because the function is a friend of the Fraction class. Similarly, member functions of a class can access the private members of parameters of the same class type. Since our Fraction copy constructor takes a parameter of the class type (to make a copy of), we’re able to access the members of parameter fraction directly, even though it’s not the implicit object.
Preventing copies
We can prevent copies of our classes from being made by making the copy constructor private:
1 | private: |
The copy constructor may be elided
1 | public: |
Why didn’t our copy constructor get called?
Note that initializing an anonymous
object and then using that object to direct initialize our defined object takes two steps (one to create the anonymous object using the default constructor, one to call the copy constructor). However, the end result is essentially identical to just doing a direct initialization, which only takes one step.
For this reason, in such case, the compiler is allowed to opt out of calling the copy constructor and just do a direct initialization instead. This process is called elision
.
So although you wrote:
1 | Fraction fiveThirds(Fraction(5, 3)); |
The compiler may change this to:
1 | Fraction fiveThirds(5, 3); |
which only requires one constructor call (to Fraction(int, int)). Note that in cases where elision is used, any statements in the body of the copy constructor are not executed, even if they would have produced side effects (like printing to the screen)!
Prior to C++17, copy elision is an optimization the compiler can make. As of C++17, some cases of copy elision (including the example above) have been made mandatory.
Finally, note that if you make the copy constructor private, any initialization that would use the copy constructor will cause a compile error, even if the copy constructor is elided
! Jesus!
1 | // copy constructor is private |
Copy initialization
Consider the following line of code:
1 | int x = 5; |
This statement uses copy initialization
to initialize newly created integer variable x to the value of 5. However, classes are a little more complicated, since they use constructors for initialization.
1 | Fraction six = oldSix; // evaluated as Fraction six(oldSix); |
And as you learned in the previous lesson, this can potentially make calls to both Fraction(int, int)
and the Fraction copy constructor
(which may be elided for performance reasons). However, because eliding isn’t guaranteed (prior to C++17, where elision in this particular case is now mandatory), it’s better to avoid copy initialization for classes, and use uniform initialization instead.
Other places copy initialization is used
There are a few other places copy initialization is used, but two of them are worth mentioning explicitly. When you pass or return
a class by value
, that process uses copy initialization.
1 | Fraction makeNegative(Fraction f) { |
In the above case, both the argument passed by value and the return value cannot be elided. However, in other cases, if the argument or return value meet specific criteria, the compiler may opt to elide the copy constructor. For example:
1 | class Something { |
In this case, the compiler will probably elide the copy constructor, even though variable s is returned by value.
Convert constructors, explicit, and delete
By default, C++ will treat any constructor as an implicit conversion operator
. Consider the following case:
1 | public: |
Although function makeNegative()
is expecting a Fraction, we’ve given it the integer literal 6 instead. Because Fraction has a constructor willing to take a single integer, the compiler will implicitly convert the literal 6 into a Fraction object. It does this by copy-initializing
makeNegative() parameter f using the Fraction(int, int)
constructor. And in this case, copy initialization is elided.
Note: This implicit conversion works for all kinds of initialization (direct, uniform, and copy).
Constructors eligible to be used for implicit conversions are called converting constructors
(or conversion constructors). Prior to C++11, only constructors taking one parameter could be converting constructors. However, with the new uniform initialization syntax in C++11, this restriction was lifted, and constructors taking multiple parameters can now be converting constructors.
The explicit keyword
While doing implicit conversions makes sense in the Fraction case. In other cases, this may be undesirable, or lead to unexpected behaviors:
1 | public: |
In the above example, the user is trying to initialize a string with a char. Because chars are part of the integer family, the compiler will use the converting constructor MyString(int) constructor to implicitly convert the char to a MyString. The program will then print this MyString, to unexpected results.
One way to address this issue is to make constructors (and conversion functions) explicit via the explicit
keyword, which is placed in front of the constructor’s name. Constructors and conversion functions made explicit will not be used for implicit conversions or copy initialization:
1 | public: |
However, note that making a constructor explicit
only prevents implicit conversions. Explicit conversions (via casting) are still allowed:
1 | std::cout << static_cast<MyString>(5); |
Direct or uniform initialization will also still convert parameters to match (uniform initialization will not do narrowing conversions, but it will happily do other types of conversions).
I am losing myself! Complicated!
1 | MyString str('x'); |
Rule: Consider making your constructors and user-defined conversion member functions explicit
to prevent implicit conversion errors.
In C++11, the explicit keyword can also be used with conversion operators.
The delete keyword
In our MyString case, we really want to completely disallow ‘x’ from being converted to a MyString (whether implicit or explicit, since the results aren’t going to be intuitive). One way to partially do this is to add a MyString(char) constructor, and make it private:
1 | private: |
However, this constructor can still be used from inside the class (private access only prevents non-members from calling this function).
A better way to resolve the issue is to use the delete
keyword (introduced in C++11) to delete the function:
1 | public: |
When a function has been deleted, any use of that function is considered a compile error.
Note that the copy constructor
and overloaded operators
may also be deleted in order to prevent those functions from being used.
Overload the assignment operator
Assignment vs Copy constructor
The purpose of the copy constructor and the assignment operator are almost equivalent – both copy one object to another. However, the copy constructor initializes
new objects, whereas the assignment operator replaces
the contents of existing objects.
The difference between the copy constructor and the assignment operator causes a lot of confusion for new programmers, but it’s really not all that difficult. Summarizing:
It depends on whether the new object has been created or not.
- If a new object has to be created before the copying can occur, the copy constructor is used (note: this includes passing or returning objects by value).
- If a new object does not have to be created before the copying can occur, the assignment operator is used.
Overloading the assignment operator (operator=
) is fairly straightforward, with one specific caveat that we’ll get to. The assignment operator must be overloaded as a member function.
If operator= is not overloaded, copy constructor is always used.
1 | public: |
Issues due to self-assignment
1 | int main() { |
This will call f1.operator=(f1)
, and under the simplistic implementation above, all of the members will be assigned to themselves. In this particular example, the self-assignment causes each member to be assigned to itself, which has no overall impact, other than wasting time. In most cases, a self-assignment doesn’t need to do anything at all!
Solution: detecting and handling self-assignment
1 | // A better implementation of operator= |
By checking if our implicit object is the same as the one being passed in as a parameter, we can have our assignment operator just return immediately without doing any other work.
Note that there is no need to check for self-assignment in a copy-constructor. This is because the copy constructor is only called when new objects are being constructed, and there is no way to assign a newly created object to itself in a way that calls to copy constructor.
Default assignment operator
Unlike other operators, the compiler will provide a default public assignment operator
for your class if you do not provide one. This assignment operator does memberwise assignment
(which is essentially the same as the memberwise initialization that default copy constructors do).
Shallow vs. deep copying
Because C++ does not know much about your class, the default copy constructor and the default assignment operator it provides use a copying method known as a memberwise copy (also known as a shallow copy
). This means that C++ copies each member of the class individually (using the assignment operator for overloaded operator=, and direct initialization for the copy constructor). When classes are simple (e.g. do not contain any dynamically allocated memory), this works very well.
However, when designing classes that handle dynamically allocated memory, memberwise (shallow) copying can get us in a lot of trouble! This is because shallow copies of a pointer just copy the address of the pointer – it does not allocate any memory or copy the contents being pointed to.
1 | class MyString { |
If we do not provide specific constructors, C++ will provide a default copy constructor and default assignment operator that do a shallow copy. The copy constructor will look something like this:
1 | MyString::MyString(const MyString &source) |
Note that m_data is just a shallow pointer copy of source.m_data
, meaning they now both point to the same thing.
1 | int main() { |
Deep copying
One answer to this problem is to do a deep copy
on any non-null pointers being copied. A deep copy allocates memory for the copy and then copies the actual value, so that the copy lives in distinct memory from the source. This way, the copy and source are distinct and will not affect each other in any way. Doing deep copies requires that we write our own copy constructors and overloaded assignment operators.
1 | // Copy constructor |
A better solution
Classes in the standard library that deal with dynamic memory, such as std::string
and std::vector
, handle all of their memory management, and have overloaded copy constructors and assignment operators that do proper deep copying. So instead of doing your own memory management, you can just initialize or assign them like normal fundamental variables! That makes these classes simpler to use, less error-prone, and you don’t have to spend time writing your own overloaded functions!