Advanced Classes and Objects Part I


Friends

Classes can declare other classes, other classes' member functions etc as friends and can access protected and private data members and methods. Example, given two classes A and B,
you can say B is a friend of A as follows:
class A
{
    friend class B;
}
Now all the methods of B can access the private and protected data members and methods of A.

Dynamic memory allocation in objects

If the amount of memory needed is unknown ahead of time, objects can dynamically allocate memory. Assume the CheckingAccount class from the previous article is updated as follows to include account number:
checkingaccount.h:
#ifndef CHECKING_H
#define CHECKING_H
#include <string>
using std::string;
class CheckingAccount
{
    public:
        CheckingAccount(std::string accountNumber, double balance);
        ~CheckingAccount();
        void deposit(double amount);
        bool withdraw(double amount);
        double getBalance() const;
        string getAccountNumber();
    private:
        std::string mAccountNumber;
        double mBalance;
} 
#endif
checkingaccount.cpp: Note that the :: operator is the scope resolution operator.
string CheckingAccount::getAccountNumber()
{
    return mAccountNumber;
}
CheckingAccount::CheckingAccount(string accountNumber, double balance) //constructor
{
    mBalance = balance;
    mAccountNumber = accountNumber;
}

void CheckingAccount::deposit(double amount)
{
    mBalance += amount;
}

bool CheckingAccount::withdraw(double amount)
{
    if((mBalance - amount) > 0)
    {
        mBalance -= amount;
        return true;
    }
    return false;
}

double getBalance() const
{
    return mBalance;
}
//Destructor not implemented yet as there is no dynamic memory allocated.
Then a simple Bank class can be seen as a collection of CheckingAccounts. Here is the first revision of the Bank class:
bank.h:
#ifndef BANK_H
#define BANK_H
#include <string>
#include "checkingaccount.h"
using std::string;
class Bank
{
    public:
        Bank(string name, int numAccounts);
        void depositIntoCheckingAccount(string accountNumber, double depositAmount);
        void withdrawFromCheckingAccount(string accountNumber, double withdrawAmount);
        CheckingAccount& getCheckingAccount(string accountNumber);
        string getName();
        int getNumAccounts();
    private:
        string mName;
        int mNumAccounts;
        CheckingAccount* mCheckingAccounts = nullptr; //in modern C++ will use vector<CheckingAccount>
}
#endif
bank.cpp:
Bank::Bank(string name, int numAccounts) : mName(name), mNumAccounts(numAccounts)
{ 
    mCheckingAccounts = new CheckingAccount[numAccounts];
}
The memory can be visualized as follows:


The implementations of getCheckingAccountdepositIntoCheckingAccount, and withdrawFromCheckingAccount are as follows:
bank.cpp:
CheckingAccount& Bank::getCheckingAccount(string accountNumber)
{
    CheckingAccount* checkingAccount = new CheckingAccount("",0.0);
    for(int i = 0; i < numAccounts; i++)
    {
        if(mCheckingAccounts[i].getAccountNumber == accountNumber)
        {
            return mCheckingAccounts[i];
        }
    }
    return *checkingAccount;
}

void Bank::depositIntoCheckingAccount(string accountNumber, double depositAmount)
{
    CheckingAccount& account = getCheckingAccount(accountNumber);
    if(account.accountNumber != "")
    { 
        account.deposit(depositAmount);
    }
    else
    {
        cout << "Couldn't retrieve account with number "<< accountNumber;
        //handle error condition here.
    }
}

void Bank::withdrawFromCheckingAccount(string accountNumber, double depositAmount)
{
    CheckingAccount& account = getCheckingAccount(accountNumber);
    if(account.accountNumber != "")
    { 
        account.withdraw(depositAmount);
    }
    else
    {
        cout << "Couldn't retrieve account with number "<< accountNumber;
        //handle error condition here.
    }
} 

Using destructors to free memory

The place to free memory dynamically allocated by the object is the destructor. Here is the destructor for the Bank class:
bank.cpp:
Bank::~Bank()
{
    delete[] mCheckingAccounts;
}

Copy Constructor and Assignment Operator

The compiler-generated copy constructor and assignment operators only provide shallow copies. Here is the problem with shallow copies, when using the default copy constructor:
Bank b1("123",5);
printBankInfo(b1);

void printBankInfo(Bank b)
{
    //code to print bank information
}
When b1 is passed to printBankInfo, a shallow copy of the object is made, so the memory looks like the following:


When printBankInfo exists, the copy of b1 is destroyed, so b1 is now pointing to freed memory. This is known as the dangling pointer problem. With the assignment operator, there is an additional problem that the original memory held by the overwritten object is orphaned and causes a memory leak.
Therefore, it is best to provide one's own copy constructor and overloaded assignment operator as follows:
Bank::Bank(const Bank& src) : Bank(src.mName, src.mNumAccounts)
{ 
    for(int k = 0 ; k < mNumAccounts; k++;)
    { 
        mCheckingAccounts[k] = src.mCheckingAccounts[k];
    }
}
The copy-and-swap idiom is used to implement the assignment operator in an exception safe manner. A non-member swap() function is implemented as a friend of the Bank class:
class Bank
{
    public:
        Bank& operator=(const Bank& rhs);
        friend void swap(Bank& first, Bank& second) noexcept;
}
swap swaps each data member using the std::swap function in the <utility> header file:
void swap(Bank& first, Bank& second) noexcept
{
    using std::swap;
    swap(first.mAccountNumber, second.mAcccountNumber);
    swap(first.mNumAccounts, second.mNumAccounts);
    swap(first.mCheckingAccounts, second.mCheckingAccounts);
}
Then the assignment operator is as follows:
Bank& Bank::operator=(const Bank& rhs)
{
    if(this == &rhs)
    {
        return *this;
    }
    Bank temp(rhs);
    swap(*this,temp);
    return *this;

}

Not allowing assignment and pass-by-value

One can disallow assignment and pass by value with the by marking the copy constructor and assignment operator with delete, as follows:
class Bank
{
    public: 
        //...
        Bank(const Bank& src) = delete;
        Bank& operator=(const Bank& rhs) = delete;
}

Introduction to move semantics

With move semantics, the programmer provides a move constructor and move assignment operator. These can be used when the source object is a temp object that can be destroyed after the operation is done. The move constructor and assignment operator "move" the data members from the source object to the destination object, leaving the source object in an indeterminate state. This process moves ownership to the destination object. Essentially, a shallow copy is done and ownership of allocated memory and other resources is switched to the destination, to prevent dangling pointers and memory leaks.

rvalues and rvalue references

An lvalue is something for which you can take the address, for example a named variable. lvalues appear on the left hand side of the assignment. An rvalue is anything that is not an lvalue, such as a literal or a temporary object or value. Typically rvalues are on the right hand side of the assignment.
An rvalue reference is a reference to an rvalue, especially when the rvalue is a temporary object. rvalue reference allows for a particular function to be invoked when a temporary object is involved. The upshot is that instead of copying large values, the pointers to those values can be copied, and the temporary object is then destroyed.
A function can specify an rvalue reference parameter by using && as part of the parameter list. Normally, a temporary object is a const type&, but when the function is overloaded with an rvalue reference, a temporary object can be resolved to that overload. Example:
void foo(string& message)
{
    cout << "Message with lvalue reference" << endl;
}

void foo(string&& message)
{
    cout << "Message with rvalue reference" << endl;
}
In this example, foo() accepting lvalue is called:
string a = "hello";
string t = "world";
foo(a);
If an expression is given as an argument the rvalue version is called
foo(a + t);
A literal also triggers the invocation of the rvalue version. If the lvalue version is deleted, doing something like foo(a) will cause an error because the rvalue reference cannot be bound to an lvalue. To cast an lvalue into an rvalue use std::move:
foo(std::move(a))
Note that once an rvalue is inside a function it becomes an lvalue because it is named. To pass it to a function expecting an rvalue, the std::move() is needed.

Implementing move semantics

Move semantics are implemented by using rvalue references. One needs to implement a move constructor and move assignment operator. They should be marked with the noexcept qualifier to state that they will not throw exceptions. Example using the Bank class:
class Bank
{
    public: 
        Bank(Bank&& src) noexcept; //move constructor
        Bank& operator=(Bank&& rhs) noexcept; //move assignment operator

    private:
        void cleanup() noexcept;
        void moveFrom(Bank &src) noexcept;
}
To be continued

References:

Gregoire, M. (2018). Professional C++. Indiana, IN: John Wiley & Sons.

Comments

Popular posts from this blog

QTreeView and QTableView dynamic changes

C++ strings and string_view