Classes - C++ Recipes: A Problem-Solution Approach (2015)

C++ Recipes: A Problem-Solution Approach (2015)

CHAPTER 5

image

Classes

Classes are the language feature that sets C++ apart from the C programming language. The addition of classes to C++ allows it to be used for programs designed using the object-oriented programming (OOP) paradigm. OOP quickly became the main software engineering practice used worldwide to build complex applications. You can find class support in most leading languages today, including Java, C#, and Objective-C.

Recipe 5-1. Defining a Class

Problem

Your program design calls for objects, and you need to be able to define classes in your programs.

Solution

C++ provides the class keyword and syntax for creating class definitions.

How It Works

The class keyword is used in C++ to create class definitions. This keyword is followed by the class name and then the body of the class. Listing 5-1 shows a class definition.

Listing 5-1. A Class Definition

class Vehicle
{

};

The Vehicle class definition in Listing 5-1 tells the compiler that it should recognize the word Vehicle as a type. This means code can now create variables of type Vehicle. Listing 5-2 shows this in action.

Listing 5-2. Creating a Vehicle Variable

class Vehicle
{

};

int main(int argc, char* argv[])
{
Vehicle myVehicle;
return 0;
}

Creating a variable like this results in your program creating an object. In the common terminology used when working with classes, the class definition itself is referred to as the class. Variables of the class are referred to as objects, so you can have multiple objects of the same class. The process of creating an object from a class is referred to as instantiating a class.

Recipe 5-2. Adding Data to a Class

Problem

You would like to be able to store data in your classes.

Solution

C++ allows classes to contain variables. Each object gets its own unique variable and can store its own values.

How It Works

C++ has the concept of a member variable: a variable that exists in the class definition. Each instantiated object from the class definition gets its own copy of the variable. Listing 5-3 shows a class that contains a single member variable.

Listing 5-3. The Vehicle Class with a Member Variable

#include <cinttypes>

class Vehicle
{
public:
uint32_t m_NumberOfWheels;
};

The Vehicle class contains a single uint32_t variable to store the number of wheels the vehicle has. Listing 5-4 shows how you can set this value and print it.

Listing 5-4. Accessing Member Variables

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
uint32_t m_NumberOfWheels;
};

int main(int argc, char* argv[])
{
Vehicle myCar;
myCar.m_NumberOfWheels = 4;

cout << "Number of wheels: " << myCar.m_NumberOfWheels << endl;

return 0;
}

Listing 5-4 shows that you can use the dot (.) operator to access member variables on an object. This operator is used twice in the code: once to set the value of m_NumberOfWheels to 4 and once to retrieve the value to print it. Listing 5-5 adds another instance of the class to show that different objects can store different values in their members.

Listing 5-5. Adding a Second Object

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
uint32_t m_NumberOfWheels;
};

int main(int argc, char* argv[])
{
Vehicle myCar;
myCar.m_NumberOfWheels = 4;

cout << "Number of wheels: " << myCar.m_NumberOfWheels << endl;

Vehicle myMotorcycle;
myMotorcycle.m_NumberOfWheels = 2;

cout << "Number of wheels: " << myMotorcycle.m_NumberOfWheels << endl;

return 0;
}

Listing 5-5 adds a second object and names it myMotorcycle. This instance of the class has its m_NumberOfWheels variable set to 2. You can see the different output values in Figure 5-1.

9781484201589_Fig05-01.jpg

Figure 5-1. The output generated by Listing 5-5

Recipe 5-3. Adding Methods

Problem

You need to be able to carry out repeatable tasks on a class.

Solution

C++ allows programmers to add functions to classes. These functions are known as member methods and have access to class member variables.

How It Works

You can add a member method to a class simply by adding a function to that class. Any function you add can then use the member variables that belong to the class. Listing 5-6 shows two member methods in action.

Listing 5-6. Adding Member Methods to a Class

#include <cinttypes>
class Vehicle
{
public:
uint32_t m_NumberOfWheels;

void SetNumberOfWheels(uint32_t numberOfWheels)
{
m_NumberOfWheels = numberOfWheels;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

The Vehicle class shown in Listing 5-6 contains two member methods: SetNumberOfWheels takes a parameter that is used to set the member m_NumberOfWheels, and GetNumberOfWheels retrieves the value of m_NumberOfWheels. Listing 5-7 uses these methods.

Listing 5-7. Using the Member Methods from the Vehicle Class

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
uint32_t m_NumberOfWheels;

public:
void SetNumberOfWheels(uint32_t numberOfWheels)
{
m_NumberOfWheels = numberOfWheels;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myCar;
myCar.SetNumberOfWheels(4);

cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle;
myMotorcycle.SetNumberOfWheels(2);

cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

return 0;
}

The member methods are used to alter and retrieve the value of the m_NumberOfWheels member variable in Listing 5-7. The output generated by this code is shown in Figure 5-2.

9781484201589_Fig05-02.jpg

Figure 5-2. The output generated by the code in Listing 5-7

Recipe 5-4. Using Access Modifiers

Problem

Exposing all member variables to calling code can lead to several problems including high coupling and higher maintenance costs.

Solution

Use the C++ access modifiers to utilize encapsulation and hide class implementations from calling code.

How It Works

C++ provides access modifiers that allow you to control whether code can access internal member variables and methods. Listing 5-8 shows how you can use the private access modifier to restrict access to a variable and the public access specifier to provide methods that access the member indirectly.

Listing 5-8. Using the public and private Access Modifiers

#include <cinttypes>

class Vehicle
{
private:
uint32_t m_NumberOfWheels;

public:
void SetNumberOfWheels(uint32_t numberOfWheels)
{
m_NumberOfWheels = numberOfWheels;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

To use an access modifier, insert the keyword into your class, followed by a colon. Once invoked, the access modifier is applied to all member variables and methods that follow until another access modifier is specified. In Listing 5-8, this means the m_NumberOfWheels variable is private and the SetNumberOfWheels and GetNumberOfWheels member methods are public.

If you tried to access m_NumberOfWheels directly in calling code, your compiler would give you an access error. Instead, you have to access the variable through the member methods. Listing 5-9 shows a working sample with a private member variable.

Listing 5-9. Using Access Modifiers

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
uint32_t m_NumberOfWheels;

public:
void SetNumberOfWheels(uint32_t numberOfWheels)
{
m_NumberOfWheels = numberOfWheels;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myCar;
// myCar.m_NumberOfWheels = 4; -Access error
myCar.SetNumberOfWheels(4);

cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle;
myMotorcycle.SetNumberOfWheels(2);

cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

return 0;
}

You can see the error that the compiler generates by uncommenting the bold line in Listing 5-9. Encapsulating data in this manner allows you to alter the implementation at a later time without affecting the rest of your code. Listing 5-10 updates the code from Listing 5-9 to use a completely different method of working out the number of wheels on a vehicle.

Listing 5-10. Altering the Vehicle Class Implementation

#include <vector>
#include <cinttypes>
#include <iostream>

using namespace std;

class Wheel
{

};

class Vehicle
{
private:
using Wheels = vector<Wheel>;
Wheels m_Wheels;

public:
void SetNumberOfWheels(uint32_t numberOfWheels)
{
m_Wheels.clear();
for (uint32_t i = 0; i < numberOfWheels; ++i)
{
m_Wheels.push_back({});
}
}

uint32_t GetNumberOfWheels()
{
return m_Wheels.size();
}
};

int main(int argc, char* argv[])
{
Vehicle myCar;
myCar.SetNumberOfWheels(4);

cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle;
myMotorcycle.SetNumberOfWheels(2);

cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

return 0;
}

Comparing the Vehicle class from Listing 5-9 and that in Listing 5-10 reveals that the implementations of SetNumberOfWheels and GetNumberOfWheels are completely different. The class in Listing 5-10 doesn’t store the value in a uint32_t member; instead, it stores avector of Wheel objects. The SetNumberOfWheels method adds a new instance of Wheel to the vector for the number supplied as its numberOfWheels parameter. The GetNumberOfWheels method returns the size of the vector. The main function in both listings is identical, as is the output generated by executing the code.

Recipe 5-5. Initializing Class Member Variables

Problem

Uninitialized variables can cause undefined program behavior.

Solution

C++ classes can initialize their member variables at instantiation and provide constructor methods for user-supplied values.

How It Works

Uniform Initialization

Classes in C++ can use uniform initialization to provide default values to class members when they’re instantiated. Uniform initialization allows you to use a common syntax when initializing built-in types or objects created from your classes. C++ uses the curly-braces syntax to support this form of initialization. Listing 5-11 shows a class with a member variable initialized in this way.

Listing 5-11. Initializing a Class Member Variable

#include <cinttypes>
class Vehicle
{
private:
uint32_t m_NumberOfWheels{};

public:
uint32 GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

In Listing 5-11, the class’s m_NumberOfWheels member is initialized using uniform initialization. This is achieved using the curly braces after the name. No value is supplied to the initializer, which causes the compiler to initialize the value to 0. Listing 5-12 shows this class used in context.

Listing 5-12. Using the Vehicle Class

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
uint32_t m_NumberOfWheels{};

public:
uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myCar;
cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle;
cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

return 0;
}

Figure 5-3 shows the output generated by this code.

9781484201589_Fig05-03.jpg

Figure 5-3. The output generated by the code in Listing 5-12.

Figure 5-3 shows output with a 0 for each class. This is an improvement on code that doesn’t initialize the data, as shown in Figure 5-4.

9781484201589_Fig05-04.jpg

Figure 5-4. The output generated by a program that doesn’t initialize its member variables

Using Constructors

Figure 5-3 represents a better situation than Figure 5-4, but neither is ideal. You’d really like the myCar and myMotorcycle objects in Listing 5-12 to print different values. Listing 5-13 adds a constructor so that you can specify the number of wheels when instantiating classes.

Listing 5-13. Adding a Constructor to a Class

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
uint32_t m_NumberOfWheels{};

public:
Vehicle(uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{

}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myCar{ 4 };
cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle{ 2 };
cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

return 0;
}

Listing 5-13 adds the ability to initialize the number of wheels on a Vehicle at the time of instantiation. It does this by adding a constructor to the Vehicle class that takes the number of wheels as a parameter. The use of a constructor lets you rely on a function call to occur at the time of object creation. This function is used to ensure that all the member variables your class contains have been properly initialized. Uninitialized data is a very common cause of unexpected program behavior such as crashes.

The myCar and myMotorcycle objects are instantiated with different values for their number of wheels. Unfortunately, adding a constructor to the class means you can no longer construct default versions of this class; you must always supply a value for the number of wheels inListing 5-13. Listing 5-14 overcomes this limitation by adding an explicit default operator to the class.

Listing 5-14. Default Constructors

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{

}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myCar{ 4 };
cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle{ 2 };
cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

Vehicle noWheels;
cout << "Number of wheels: " << noWheels.GetNumberOfWheels() << endl;

return 0;
}

The Vehicle class in Listing 5-14 contains an explicit default constructor. The default keyword is used along with an equals operator to inform the compiler that you want to add a default constructor to this class. Thanks to the uniform initialization of them_NumberOfWheels variable, you can create an instance of the class noWheels that contains 0 in the m_NumberOfWheels variable. Figure 5-5 shows the output generated by this code.

9781484201589_Fig05-05.jpg

Figure 5-5. The output generated by Listing 5-14, showing the 0 in the noWheels class

Recipe 5-6. Cleaning Up Classes

Problem

Some classes require their members to be cleaned up when an object is being destroyed.

Solution

C++ provides for destructors to be added to classes that allow code to be executed when a class is being destroyed.

How It Works

You can add a special destructor method to your classes in C++ using the ~ syntax. Listing 5-15 shows how to achieve this.

Listing 5-15. Adding a Destructor to a Class

#include <cinttypes>
#include <string>

using namespace std;

class Vehicle
{
private:
string m_Name;
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(string name, uint32_t numberOfWheels)
: m_Name{ name }
, m_NumberOfWheels{ numberOfWheels }
{

}

~Vehicle()
{
cout << m_Name << " is being destroyed!" << endl;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

The Vehicle class in Listing 5-15 contains a destructor. This destructor simply prints out the name of the object being destroyed. The constructor can be initialized with the name of an object, and the default constructor of Vehicle calls the default constructor of the string class automatically. Listing 5-16 shows how this class can be used in practice.

Listing 5-16. Using Classes with Destructors

#include <cinttypes>
#include <iostream>
#include <string>

using namespace std;

class Vehicle
{
private:
string m_Name;
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(string name, uint32_t numberOfWheels)
: m_Name{ name }
, m_NumberOfWheels{ numberOfWheels }
{

}

~Vehicle()
{
cout << m_Name << " is being destroyed!" << endl;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myCar{ "myCar", 4 };
cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle{ "myMotorcycle", 2 };
cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

Vehicle noWheels;
cout << "Number of wheels: " << noWheels.GetNumberOfWheels() << endl;

return 0;
}

As you can see from the main function in Listing 5-16, you don’t have to add any special code to call a class destructor. Destructors are called automatically when objects go out of scope. In this case, the calls to the destructors of the Vehicle objects occur after the return. Figure 5-6shows the output from this program to prove the destructor code is executed.

9781484201589_Fig05-06.jpg

Figure 5-6. The output generated by Listing 5-16, showing that destructors have been executed

It’s important to pay attention to the order in which these destructors are called. The Vehicle objects are destroyed in an order that’s the reverse of that in which they were created. This is important if you have resources that rely on being created and destroyed in the correct order.

The compiler implicitly creates a default destructor if you don’t define your own. You can also explicitly define a destructor using the code shown in Listing 5-17.

Listing 5-17. Explicitly Defining a Destructor

#include <cinttypes>

class Vehicle
{
private:
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{

}

~Vehicle() = default;

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

It’s considered good practice to always be explicit with your default constructor and destructors. Doing so removes any ambiguity from the code and lets other programmers know that you were happy with the default behavior. The omission of this code could lead others to believe that you overlooked its inclusion.

Recipe 5-7. Copying Classes

Problem

You would like to ensure that you’re copying data from one object to another in a proper manner.

Solution

C++ provides the copy constructor and assignment operator that you can use to add code to your class that is executed when a copy takes place.

How It Works

You can copy objects in C++ in a number of scenarios. An object is copied when you pass it into the constructor of another object of the same type. An object is also copied when you assign one object to another. Passing an object into a function or method by value also results in a copy operation taking place.

Implicit and Default Copy Constructors and Assignment Operators

C++ classes support these operations through the copy constructor and assignment operator. Listing 5-18 shows the default versions of these methods being invoked in the main method.

Listing 5-18. Using the Copy Constructor and Assignment Operator

#include <cinttypes>
#include <iostream>
#include <string>

using namespace std;

class Vehicle
{
private:
string m_Name;
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(string name, uint32_t numberOfWheels)
: m_Name{ name }
, m_NumberOfWheels{ numberOfWheels }
{

}

~Vehicle()
{
cout << m_Name << " at " << this << " is being destroyed!" << endl;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myCar{ "myCar", 4 };
cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle{ "myMotorcycle", 2 };
cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

Vehicle myCopiedCar{ myCar };
cout << "Number of wheels: " << myCopiedCar.GetNumberOfWheels() << endl;

Vehicle mySecondCopy;
mySecondCopy = myCopiedCar;
cout << "Number of wheels: " << mySecondCopy.GetNumberOfWheels() << endl;

return 0;
}

The myCopiedCar variable is constructed using a copy constructor. This is achieved by passing another object of the same type into myCopiedCar’s brace initializer. The mySecondCopy variable is constructed using the default constructor. Thus the object is initialized with an empty name and 0 as the number of wheels. The code then assigns to mySecondCopy using myCopiedCar. You can see the results of these operations in Figure 5-7.

9781484201589_Fig05-07.jpg

Figure 5-7. The output generated by Listing 5-18

As expected, you have three objects named myCar, each of which has four wheels. You can see the distinct objects when the destructor prints the address in memory where each object resides.

Explicit Copy Constructors and Assignment Operators

The code in Listing 5-18 takes advantage of the implicit copy constructor and assignment operator. The C++ compiler automatically adds these functions to your classes when it encounters code that will use them. Listing 5-19 shows how you can create these functions explicitly.

Listing 5-19. Explicitly Creating the Copy Constructor and Assignment Operator

#include <cinttypes>
#include <iostream>
#include <string>

using namespace std;

class Vehicle
{
private:
string m_Name;
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(string name, uint32_t numberOfWheels)
: m_Name{ name }
, m_NumberOfWheels{ numberOfWheels }
{

}

~Vehicle()
{
cout << m_Name << " at " << this << " is being destroyed!" << endl;
}

Vehicle(const Vehicle& other) = default;
Vehicle& operator=(const Vehicle& other) = default;

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

The signature for a copy constructor resembles that of a normal constructor. It’s a method with no return type; however, the copy constructor takes a constant reference to an object of the same type as a parameter. The assignment operator uses operator overloading to overload the =arithmetic operator for the class when the right side of the statement is another object of the same type, as in someVehicle = someOtherVehicle. The default keyword comes in useful again to allow you to communicate with other programmers that you’re happy with the default operations.

Disallowing Copy and Assignment

Sometimes you’ll create classes in which you absolutely don’t want copy constructors and assignment operators to be used. C++ provides the delete keyword for these cases. Listing 5-20 shows how this is implemented.

Listing 5-20. Disallowing Copy and Assignment

#include <cinttypes>
#include <iostream>
#include <string>

using namespace std;

class Vehicle
{
private:
string m_Name;
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(string name, uint32_t numberOfWheels)
: m_Name{ name }
, m_NumberOfWheels{ numberOfWheels }
{

}

~Vehicle()
{
cout << m_Name << " at " << this << " is being destroyed!" << endl;
}

Vehicle(const Vehicle& other) = delete;
Vehicle& operator=(const Vehicle& other) = delete;

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myCar{ "myCar", 4 };
cout << "Number of wheels: " << myCar.GetNumberOfWheels() << endl;

Vehicle myMotorcycle{ "myMotorcycle", 2 };
cout << "Number of wheels: " << myMotorcycle.GetNumberOfWheels() << endl;

Vehicle myCopiedCar{ myCar };
cout << "Number of wheels: " << myCopiedCar.GetNumberOfWheels() << endl;

Vehicle mySecondCopy;
mySecondCopy = myCopiedCar;
cout << "Number of wheels: " << mySecondCopy.GetNumberOfWheels() << endl;

return 0;
}

The delete keyword is used in place of default to inform the compiler that you don’t wish the copy and assignment operations to be available to a class. The code in the main function will no longer compile and operate.

Custom Copy Constructors and Assignment Operators

In addition to using the default versions of these operations, it’s possible to supply your own versions. This is done by using the same signatures for the methods in your class definition but providing a method body in place of the default assignment.

More often than not in modern C++, the places you’ll overload these operators are limited; but it’s important to be aware of the one place where you absolutely want to do so. The default copy and assignment operations carry out a shallow copy. They call the assignment operator on each member of an object and assign the value from the class passed in. There are occasions when you have a class that manually manages a resource, such as memory, and a shallow copy ends up with a pointer in both classes pointing to the same address in memory. If that memory is freed in the class’s destructor, you’re left in a situation where one object is pointing to memory that has been freed by another. In this case, your program is likely to crash or exhibit other strange behavior. Listing 5-21 shows an example in which this could occur.

Listing 5-21. Shallow-Copying a C-Style String Member

#include <cinttypes>
#include <cstring>
#include <iostream>

using namespace std;

class Vehicle
{
private:
char* m_Name{};
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(const char* name, uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{
const uint32_t length = strlen(name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, name);
}

~Vehicle()
{
delete m_Name;
m_Name = nullptr;
}

Vehicle(const Vehicle& other) = default;
Vehicle& operator=(const Vehicle& other) = default;

char* GetName()
{
return m_Name;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myAssignedCar;

{
Vehicle myCar{ "myCar", 4 };
cout << "Vehicle name: " << myCar.GetName() << endl;

myAssignedCar = myCar;
cout << "Vehicle name: " << myAssignedCar.GetName() << endl;
}

cout << "Vehicle name: " << myAssignedCar.GetName() << endl;

return 0;
}

Image Note The code in Listing 5-21 is purposefully constructed to create a situation that would be better solved by using a STL string class. This code is simply intended to be an easy-to-understand example of how things can go wrong.

The main function in Listing 5-21 creates two instances of the Vehicle class. The second is created in a block. This block causes the myCar object to be destructed when the block ends and the object goes out of scope. This is a problem because the last line of the block invokes the assignment operator and does a shallow copy of the class members. After this takes place, the myCar and myAssignedCar objects point to the same memory address in their m_Name variables. This memory is released in the destructor for myCar before the code tries to print the name ofmyAssignedCar. You can see the result of this error in Figure 5-8.

9781484201589_Fig05-08.jpg

Figure 5-8. Output showing the error from shallow-copying an object before it’s destroyed

Figure 5-8 proves that the shallow copy results in a dangerous situation for the code. The memory pointed to by the m_Name variable in myAssignedCar is no longer valid as soon as the myCar variable has been destroyed. Listing 5-22 solves this problem by providing a copy constructor and an assignment operator that carry out a deep copy of the class.

Listing 5-22. Carrying Out a Deep Copy

#include <cinttypes>
#include <cstring>
#include <iostream>

using namespace std;

class Vehicle
{
private:
char* m_Name{};
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(const char* name, uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{
const uint32_t length = strlen(name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, name);
}

~Vehicle()
{
delete m_Name;
m_Name = nullptr;
}

Vehicle(const Vehicle& other)
{
const uint32_t length = strlen(other.m_Name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, other.m_Name);

m_NumberOfWheels = other.m_NumberOfWheels;
}

Vehicle& operator=(const Vehicle& other)
{
if (m_Name != nullptr)
{
delete m_Name;
}

const uint32_t length = strlen(other.m_Name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, other.m_Name);

m_NumberOfWheels = other.m_NumberOfWheels;

return *this;
}

char* GetName()
{
return m_Name;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myAssignedCar;

{
Vehicle myCar{ "myCar", 4 };
cout << "Vehicle name: " << myCar.GetName() << endl;

myAssignedCar = myCar;
cout << "Vehicle name: " << myAssignedCar.GetName() << endl;
}

cout << "Vehicle name: " << myAssignedCar.GetName() << endl;

return 0;
}

This time, the code provides methods to be carried out when a copy or assignment takes place. The copy constructor is invoked when a new object is created by copying an old object, so you never need to worry about deleting the old data. The assignment operator, on the other hand, can’t guarantee that the existing class didn’t already exist. You can see the implications of this in the assignment operator when it’s responsibly deleting the memory allocated for the existing m_Name variable. The result of these deep copies can be seen in Figure 5-9.

9781484201589_Fig05-09.jpg

Figure 5-9. The result of using a deep copy

The output is now correct, thanks to the use of a deep copy. This gives the myAssignedCar variable its own copy of the name string rather than simply having its pointer assigned the same address as the myCar class. The proper solution to solving the problem in this case is to use an STL string in place of the C-style string, but the example will be valid if you ever have to write classes that may end up pointed to the same dynamically allocated memory or stack memory in the future.

Recipe 5-8. Optimizing Code with Move Semantics

Problem

Your code is running slowly, and you think the problem is caused by copying temporary objects.

Solution

C++ provides support for move semantics in the form of a move constructor and a move assignment operator.

How It Works

The code shown in Listing 5-23 performs a deep copy of an object to avoid the scenario where a different object is left pointing at an invalid memory address.

Listing 5-23. Using Deep Copy to Avoid Invalid Pointers

#include <cinttypes>
#include <cstring>
#include <iostream>

using namespace std;

class Vehicle
{
private:
char* m_Name{};
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(const char* name, uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{
const uint32_t length = strlen(name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, name);
}

~Vehicle()
{
delete m_Name;
m_Name = nullptr;
}

Vehicle(const Vehicle& other)
{
const uint32_t length = strlen(other.m_Name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, other.m_Name);

m_NumberOfWheels = other.m_NumberOfWheels;
}

Vehicle& operator=(const Vehicle& other)
{
if (m_Name != nullptr)
{
delete m_Name;
}

const uint32_t length = strlen(other.m_Name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, other.m_Name);

m_NumberOfWheels = other.m_NumberOfWheels;

return *this;
}

char* GetName()
{
return m_Name;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myAssignedCar;

{
Vehicle myCar{ "myCar", 4 };
cout << "Vehicle name: " << myCar.GetName() << endl;

myAssignedCar = myCar;
cout << "Vehicle name: " << myAssignedCar.GetName() << endl;
}

cout << "Vehicle name: " << myAssignedCar.GetName() << endl;

return 0;
}

This is the correct solution when you know that two objects may live a considerable time but one may be destroyed before the other, which would likely result in a crash. Sometimes, however, you know that the object you’re copying from is about to destroyed. C++ allows you to optimize such situations using move semantics. Listing 5-24 adds a move constructor and a move assignment operator to the class and uses the move function to invoke them.

Listing 5-24. The Move Constructor and Move Assignment Operator

#include <cinttypes>
#include <cstring>
#include <iostream>

using namespace std;

class Vehicle
{
private:
char* m_Name{};
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

Vehicle(const char* name, uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{
const uint32_t length = strlen(name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, name);
}

~Vehicle()
{
if (m_Name != nullptr)
{
delete m_Name;
m_Name = nullptr;
}
}

Vehicle(const Vehicle& other)
{
const uint32_t length = strlen(other.m_Name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, other.m_Name);

m_NumberOfWheels = other.m_NumberOfWheels;
}

Vehicle& operator=(const Vehicle& other)
{
if (m_Name != nullptr)
{
delete m_Name;
}

const uint32_t length = strlen(other.m_Name) + 1; // Add space for null terminator
m_Name = new char[length]{};
strcpy(m_Name, other.m_Name);

m_NumberOfWheels = other.m_NumberOfWheels;

return *this;
}

Vehicle(Vehicle&& other)
{
m_Name = other.m_Name;
other.m_Name = nullptr;

m_NumberOfWheels = other.m_NumberOfWheels;
}

Vehicle& operator=(Vehicle&& other)
{
if (m_Name != nullptr)
{
delete m_Name;
}

m_Name = other.m_Name;
other.m_Name = nullptr;

m_NumberOfWheels = other.m_NumberOfWheels;

return *this;
}

char* GetName()
{
return m_Name;
}

uint32_t GetNumberOfWheels()
{
return m_NumberOfWheels;
}
};

int main(int argc, char* argv[])
{
Vehicle myAssignedCar;

{
Vehicle myCar{ "myCar", 4 };
cout << "Vehicle name: " << myCar.GetName() << endl;

myAssignedCar = move(myCar);
//cout << "Vehicle name: " << myCar.GetName() << endl;
cout << "Vehicle name: " << myAssignedCar.GetName() << endl;
}

cout << "Vehicle name: " << myAssignedCar.GetName() << endl;

return 0;
}

Move semantics work by providing class methods that take rvalue references as parameters. These rvalue references are denoted by using the double ampersand operator on the parameter type. You can invoke the move operations using the move function; you can see this in action in the main function. The move function can be used here because you know that myCar is about to be destroyed. The move assignment operator is invoked, and the pointer address is shallow-copied to myAssignedCar. The move assignment operator releases the memory that the object may already have been using for m_Name. Importantly, it then copies the address from other before setting other.m_Name to nullptr. Setting the other object’s pointer to nullptr prevents that object from deleting the memory in its destructor. In this case, the code is able to move the value of m_Name from other to this without having to allocate more memory and deep-copy the values from one to the other. The end result is that you can no longer use the value of m_Name stored by myCar—the commented-out line in Listing 5-24’s main function would result in a crash.