The C++ Programming Language

Part 6: Enums, Structs & Classes, user-defined types

Enums

Enum (enumeration) is a user-defined type that accepts a limited number of values. They are mainly used to assign names to integral values.

Declaration

Declaration starts with the enum keyword followed by the name we want for this type and finally with the list of names for the values enclosed in curly brackets:

enum months { jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec };

The compiler assigns o to the first name in the enum and increases by one until the end of the list. We can assign our own values to the names if we want:

enum days { mon = 1, tue = 2, wed = 3, thu = 4, fri = 5, sat = 6, sun = 7 };

Usage

After their declaration, enums are valid types and we use them to define variables:

months month = sep;
month = jan;

Example

Here is a code example for enums:

#include <iostream>

enum months { jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec };
enum days { mon = 1, tue = 2, wed = 3, thu = 4, fri = 5, sat = 6, sun = 7 };

int month_days(months mon) {
    int days[] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
    return days[mon];
}

const char* month_name(months mon) {
    const char* names[] = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"};
    return names[mon];
}

int main() {
    months month = sep;
    month = jan;
    std::cout << month_name(month) << " has " << month_days(month) << " days\n";
}

Structures

The C programming language introduced the concept of structures. With them we can pack together a group of variables and use them as single entity under one name. it allows us to create complex data types that are more manageable and logically organized. For all the reasons that led to the creation of C++, it has inherited this feature too.

Defining structures in C++

To define a structure, we use the struct keyword follower by the name we want to give to this new type and a block of member definitions enclosed in curly brackets:

struct structName {
    dataType var1;
    dataType var2;
    ...
};

Here is the definition of a point in 3D:

struct vec3d {
    float x;
    float y;
    float z;
};

After a structure is defined, we can use its name as a new type to declare variables.

vec3d p1;
vec3d p2 = { 2,3,5 }; // initialize the point

Member variables of a structure can also be structures allowing us to represent even more complex entities:

struct triangle {
    vec3d p1;
    vec3d p2;
    vec3d p3;
};

Using structures

We can access members of structures individually either to read or to modify their value. To access the members of a structure, we use the dot operator (.). If we have a pointer to a structure, we use the arrow operator (->) instead. Here's an example of initializing and accessing structure members:

// calculate Euclidean distance
double distance(vec3d& p1, vec3d& p2) {
    double dx = p2.x - p1.x;
    double dy = p2.y - p1.y;
    double dz = p2.z - p1.z;
    double l = sqrt(dx * dx + dy * dy + dz * dz);
    return l;
}

int main() {
    triangle t = { {1,1,1},{2,2,2},{3,3,1} };
    double side_length  = distance(t.p1, t.p2);
    std::cout << side_length << "\n";
}

When passing structs as arguments we prefer the by-reference call since structs are big objects and this way we save both space and time in the call.

Practical Uses of Structures

Structures are particularly useful in C and C++ for several reasons:

Limitations of Structures

Despite their obvious usefulness, structures in C have some limitations:

Memory Consumption: Due to member alignment for speed efficiency, structures may consume more memory than necessary.

No Data Hiding: Structure members are accessible from anywhere within the scope, which does not support encapsulation.

In summary, structures provide a way to handle complex data by grouping different types of variables together. They are versatile and form the basis for many advanced data handling techniques in programming. However, they also come with limitations that we need to be aware of when designing our applications.

Classes

Classes expand the concept of structures to allow for the creation of objects. They enabled encapsulation of data and functions to operate on that data. They have data hiding to keep the implementation secure and robust.

Classes are the blueprints of objects. On the other hand, object is an instantiation of a class. Speaking in terms of programming, an object is a variable and class is the definition of the form and behavior of that object.

A class is a user-defined data type which holds its own data members and member functions that operate on the data. Data members and member functions define the properties and behavior of the objects of a class.

Definition of a class

To define a class, we use the class keyword follower by the name we want to give to this new type and a block of member definitions enclosed in curly brackets. Classes introduce the concept of access specifier. The access specifier determines who has access to the members, data and functions that define the objects of the class.

class className {
access_specifier1:
    // variables
    // functions

access_specifier2:
    // variables
    // functions
    ... 
};

The access specifier can be one of the keywords: public, protected and private. Here is our point example as a class:

class vec3d {
public:
    double x;
    double y;
    double z;

    void move_by(double dx, double dy, double dz) {
        x += dx;
        y += dy;
        z += dz;
    }
};

We have added a member function which moves (translates) the point by a specified amount in each axis. So, we do not need to care about the internal representation of a point anymore.

The access specifier we used is public. This makes all the members visible and accessible to everyone. This attribute remains until we change it with another specifier. By default, everything in a class is considered private. Private members are only accessible to member functions.

From a class to an object

As we said a class is just a description, a variable of its type is the actual object:

int main() {
    // declare and initialize a point
    vec3d pt = { 1,1,1 };
    // move it
    pt.move_by(2, 3, 4);
    // directly set a coordinate
    pt.x = 23;
}

We can access the members of the class using the dot operator (.) when we have a class or a reference to it, and the arrow operator (->) when we have a pointer to the class.

In this example we created a vec3d object, and then we called a member function to modify it, and we also modified a member variable, or attribute, directly.

Class Constructors

The constructor is a special member function of the class. As the name implies, it constructs or initializes an object of that class when it is instantiated. The constructor is automatically called by the compiler. The constructor has the same name as the class, has no return type and may have arguments.

class ClassName {
access_specifier:
    // constructor
    ClassName() {
        // some code
    }
    // more declarations
};

Revisiting our vec3d class, we can modify it like this:

class vec3d {
public:
    double x;
    double y;
    double z;

public:
    vec3d() {
        x = y = z = 0;
    }
    ...
};

Whenever we create a vec3d object it is initialized at (0,0,0). The constructor that has no arguments is also called default constructor and can be automatically generated by the compiler if we do not write one. The default constructor has no code and performs no initializations to member variables as we did in the example. The above constructor can also be written like this:

vec3d() :x(0), y(0), z(0) { }

This also initializes the variables to 0. Constructors can be overloaded like any function, and they can also have default arguments, and these arguments can be used to initialize member variables:

vec3d(double _x=0, double _y = 0, double _z = 0) :x(_x), y(_y), z(_z) { }

This constructor that can be invoked without arguments, because it has default values for all of them, replaces the default constructor.

Usually, constructors are declared as public. There are some cases where we declare them protected or private. That is done when we want to restrict the instantiation of a class and force the use of special mechanisms to do so. We will discuss some application design methods in the last part of this tutorial where this concept will become clear.

The copy constructor

There is special constructor that takes as argument a reference to an object of the same class. The purpose of this compiler is to make the new object a copy of the object we pass:

vec3d(const vec3d & org) : x(org.x), y(org.y), z(org.z) { }

The keyword const in the declaration means that we will not modify the object passed, and if we try to do so the compiler will raise an error.

How to invoke a constructor

Here is how we can instantiate point objects and control the constructor the compiler will call.

// default constructor
vec3d pt1;
// initialization constructor
vec3d pt3(1, 2, 3);
// copy constructor
vec3d pt4(pt3);

The constructor called is actually determined by the same rules as any overloaded function.

Data hiding due to constructors

Having a constructor allows us to move the attributes of the class under the private modifier. This will make them inaccessible to code outside the class giving us more freedom to make any changes we like.

Here is how our vec3d class could become:

class vec3d {
private:
    double x;
    double y;
    double z;

public:
    vec3d(double _x=0, double _y = 0, double _z = 0) :x(_x), y(_y), z(_z) {
        std::cout << "default/initializing constructor\n";
    }
    vec3d(const vec3d & org) : x(org.x), y(org.y), z(org.z) {
        std::cout << "copy constructor\n";
    }
    void move_to(double _x, double _y, double _z) {
        x = _x;
        y = _y;
        z = _z;
    }

    void move_by(double dx, double dy, double dz) {
        x += dx;
        y += dy;
        z += dz;
    }
    double X() {
        return x;
    }
    double Y() {
        return y;
    }
    double Z() {
        return z;
    }
};

We can set initial coordinates for our points when we instantiate them, move them around with the move_to and move_by functions, and read their positions with the X, Y and Z functions.

Any direct modification to the x,y and z values are now prohibited and only available through functions. This ensures that the values are always valid and within the limits we specify when designing our application.

Class Destructors

The destructor is another special member function of a class. It is called automatically when an object goes out of scope or deleted with the delete operator. Their primary role is to enable us perform cleanup when an object goes out of existence.

This is a good time to recall the std::vector class. Any vector object can grow the memory it occupies to store the data we want dynamically. We do not need to clean that memory ourselves because it is done when the object terminates its lifecycle and its destructor is called.

These are the key points of the destructor:

Here is the destructor for our point class. Here we just display the message that the object goes out of scope:

~vec3d() {
    std::cout << "vec3d goes out of scope\n";
}

Constructors and Destructors are essential in C++ because they allow us to effectively manage computer resources such as memory we consume during our program execution. This simplifies the design of our software and minimizes the number of errors we may make when programming.

The this pointer

Every object is located somewhere in the computer memory and there are the data members. Its member functions though are in another place and their code is shared among all objects. The this pointer is a special pointer that allows the functions to operate on the correct data every time. This pointer is implicitly passed to the function by the compiler and each member variable is actually accessed like this->variableName, although we write variableName.

These are some key uses of the this pointer:

Accessing members: although it is used implicitly by the compiler we can also use it explicitly this->variableName.

Resolving Name Conflicts: when a function argument has the same name as a member function we can use the this pointer to resolve the conflict:

void move_to(double x, double y, double z) {
    this->x = x;
    this->y = y;
    this->z = z;
}

Returning the object itself: The this pointer can be used to return the object itself from a member function, which is useful for chaining function calls:

// a function returning a reference to the object
vec3d& move_by(double dx, double dy, double dz) {
    x += dx;
    y += dy;
    z += dz;
    return *this;
}

// moving the point and then getting the coordinate
std::cout << "pt4.x=" << pt4.move_by(5,6,7).X() << "\n";

Member functions

C++ introduced the concept of member functions. These functions are declared as members of the class like the member variables. They can access the and modify the member variables and define the behavior of the objects.

Keeping together the data and the behavior is what we call encapsulation.

Declaration and Definition of member functions

Member functions can be declared inside the class definition. We can also define member functions outside the class using the scope resolution operator (::). Here is the definition of the vec3d class in the header:

class vec3d {
private:
    double x;
    double y;
    double z;

public:
    vec3d(double _x = 0, double _y = 0, double _z = 0);
    vec3d(const point& org);
    ~vec3d();
    point& move_to(double x, double y, double z);
    point& move_by(double dx, double dy, double dz);
    double X();
    double Y();
    double Z();
};

In the header we only declare the member functions. Their implementation is in the respective source file:

vec3d::point(double _x /*= 0*/, double _y /*= 0*/, double _z /*= 0*/) :x(_x), y(_y), z(_z) {
    std::cout << "default/initializing constructor\n";
}
vec3d::point(const vec3d& org) : x(org.x), y(org.y), z(org.z) {
    std::cout << "copy constructor\n";
}
vec3d::~vec3d() {
    std::cout << "point goes out of scope\n";
}
vec3d & vec3d::move_to(double x, double y, double z) {
    this->x = x;
    this->y = y;
    this->z = z;
    return *this;
}
vec3d & vec3d::move_by(double dx, double dy, double dz) {
    x += dx;
    y += dy;
    z += dz;
    return *this;
}
double vec3d::X() {
    return x;
}
double vec3d::Y() {
    return y;
}
double vec3d::Z() {
    return z;
}

Using member functions

To use a member function, we create an object and then call the function using the name of the object with the dot operator (.). the member function will be invoked with the right object data.

vec3d my_point(2, 3, 1);
my_point.move_to(5, 6, 2);

If we have a pointer to the object then we use the arrow operator (->).

void someFunction(vec3d * pt) {
    pt->move_by(2, 2, 2);
}

Member functions & the const qualifier

A const member function is a functions that does not modify the object or call any non-const member function. This situation arises when we declare a const object variable. In this example:

const vec3d c_point(4, 4, 4);
double x = c_point.X();

We get a compilation error because we declared c_point as const, and there is no guarantee that calling X() to read the x-coordinate does not modify the internal state of the object. To overcome this, we must make the function X() as const by appending the const qualifier after the argument list both in the declaration and the definition:

// the declaration of the function
double X() const;

// and its definition
double vec3d::X() const{
    return x;
}

Overloading member functions

Member functions can be overloaded just like any other function in C++. Apart from the argument list, member functions can also differ in the const qualifier:

// the declarations
double X() const;
double X();

// and the definitions
double vec3d::X() const{
    std::cout << "const X()\n";
    return x;
}
double vec3d::X() {
    std::cout << "non const X()\n";
    return x;
}

These are validly overloaded functions although they have the same argument list. The first is called when we have a const object and the second is called when we have an ordinary object.

Member variables

Member variables are variables declared inside a class. They are the attributes of the objects created from the class. Member variables can be under any access specifier.

Provided that they are public, we can access them using the dot operator (.), or in case we have a pointer using the arrow operator(->). If they are not public and we try to access them we will get a compilation error. Using the vec3d class, we created before we could make the following mistake:

void someFunction(vec3d * pt) {
    pt->x = 10;  // generates compilation error
}

Static members

We can declare class members as static. This applies both to variables and functions.

Static member variables

Static member variables belong to the class itself and not to the objects. This means they are no copied for every object but are shared among all objects of the class.

Static member variables are declared in the class definition. They are assigned storage space in program memory when they are defined in a source file, usually the file we define the code of the class. Here is the definition of our vec3d class with a new static variable.

class vec3d {
private:
    // other declarations
    static int counter;
};
// and in the source file we add this declaration
int vec3d::counter = 0;

It is a good practice to initialize our variables when we declare them.

Our member functions have access to this variable:

vec3d::vec3d(double _x /*= 0*/, double _y /*= 0*/, double _z /*= 0*/) :x(_x), y(_y), z(_z) {
    std::cout << "default/initializing constructor\n";
    ++counter;
}
vec3d::vec3d(const vec3d & org) : x(org.x), y(org.y), z(org.z) {
    std::cout << "copy constructor\n";
    ++counter;
}
vec3d::~vec3d() {
    std::cout << "vec3d goes out of scope\n";
    --counter;
}

The constructors increase the value of the variable and the destructors decrease it, thus making it a counter of how many objects of the class exist.

Static member functions

Static member functions also belong to the class itself and not to the objects. They do not have access to this pointer and cannot call any ordinary member function or access any data member.

However, they can access the private members of any object of the class we pass them as arguments.

They are declared with the keyword static preceding the function declaration. Their definition can be done in the class definition or out of it like ordinary functions.

We can call them using the name of the class followed by the scope operator (::) and the function name. Here is an example:

class vec3d {
    // other definitions
public:
    // defining the static function
    static int get_counter() {
        return counter;
    }
};

// and calling it
std::cout << "active objects:" << vec3d::get_counter() << "\n";

Operator overloading

Operator overloading allows us to redefine how operators work. They are useful when we want to define operations involving user-defined types like classes.

Assume we have to add two vec3d objects. Our only option is to write a function that takes two vec3d objects and return their sum.

inline vec3d add_vectors(const vec3d& v1, const vec3d& v2) {
    return vec3d(v1.X() + v2.X(), v1.Y() + v2.Y(), v1.Z() + v2.Z());
}

The code could be like:

vec3d a1(1, 2, 3);
vec3d a2(4, 5, 6);
vec3d a3 = add_vectors(a1, a2);

This solution is acceptable but not very elegant. We can redefine the addition operator (+) to call a function we create and implement the vector addition as we know it. So, when the compiler encounters an addition of two vectors it will call that function automatically.

Here is the syntax of operator overloading:

class ClassName {
public:
    returnType operator operator_symbol (argument_list) {
        // code
        return returnType;
    }
};

In our case we added the operator in the vec3d class:

vec3d operator+(const vec3d& v2) {
    return vec3d(x + v2.x, y + v2.y, z + v2.z);
}

And our code will become:

vec3d a1(1, 2, 3);
vec3d a2(4, 5, 6);
vec3d a3 = a1 + a2;

Which is a lot more intuitive and easier to understand.

Structures revisited

In C++ structures are identical to classes. Whatever we have said about classes is also meant for structures as well. There is one difference only. The default access level for structures is public unless we modify it, while for classes it is private. The main reason is to assist porting of C code to C++ and to assist C developers move to C++.

It is a good practice not to modify access for structs and use class if you want to take advantage of the object-oriented features C++ has. This will make your code easier to read and understand.

Function objects

Function Objects or functors are class or structure objects that can be called like a function. They actually extend the function pointers we saw in Part3. This is done by overloading the function call operator (). Being classes or structures makes them far more flexible than simple function pointers.

Create a function object

All we have to do is define the function-call operator:

class is_odd {
public:
    is_odd() {}
    bool operator()(int value) {
        if (value % 2) return true;
        return false;
    }
};

Here we create a function object that checks if a number is odd. We can create an instance of this class and use it to evaluate some numbers:

int main() {
    is_odd iso;

    for (int i = 0; i < 10; i++) {
        if (iso(i))
            std::cout << i << " is odd\n";
    }
}

This functor can be extended to handle both odds and evens:

class odd_or_even {
    int chk;
public:
    odd_or_even(int c) :chk(c % 2) {}
    bool operator()(int value) {
        if (value % 2 == chk) return true;
        return false;
    }
};

The behavior depends on the value we pass to the constructor at instantiation. Odd numbers make it identify odds and even numbers the evens:

odd_or_even odds(1);  // with odd numbers it checks for odds
odd_or_even evens(0); // with even numbers it checks for evens

We can even pass it to another function or class in order to dynamically modify its behavior:

// the template enables the function to accept both function pointers and functors
template<typename F>
void check_values(std::vector<int>& vals, std::vector<int>& results, F& check_fun) {
    for (auto i : vals) {
        if (check_fun(i)){
            results.push_back(i);
        }
    }
}

The function is defined as template so that it can adapt to whatever argument we use, as long as it implements the function call operator. This allows us to use function pointers as well:

std::vector<int> results;
check_values(values, results, odds);

results.clear();
check_values(values, results, is_odd);

Lambdas

Lambda expressions in C++ are a feature introduced in C++11 that allow us to define anonymous function objects directly in our code. They are particularly useful for short snippets of code that you want to pass to algorithms or asynchronous functions.

He syntax of lambda expressions is:

[capture_clause] (parameters) -> return_type
{
    statement(s)
}

The most powerful characteristic of lambda functions over normal functions is that they can have access to the local variables of the enclosing scope. That is declared by the capture_clause. It can be by value, by reference or mixed capture, other variables by value and other by reference.

[=]  : capture all variables by value
[&]  : capture all variables by reference
[x, &y] : capture x by value and y by reference

Here is an example:

// this function iterates the vector and
// counts how many times the 'func' returns true
template<class T, class func>
int count_objects(std::vector<T>& v, func f)
{
    int count = 0;
    for (auto it = v.begin(); it != v.end(); ++it)
    {
        if (f(*it))
            ++count;
    }
    return count;
}

void lambda_s()
{
    // basic lambda, we create an inline function
    auto greet = []() {std::cout << "lambda sample\n"; };
    greet();

    std::vector<int> v= { 1, 8, 3, 4, 0, 9, 7, 2, 1, 3, 5, 6, 
                          3, 4, 7, 2, 1, 8, 5, 6, 3, 9, 7, 2 };
    // count how many '3' s are in the vector
    auto se = [](int i) { return i == 3; };
    std::cout << "found:" << count_objects(v, se) << "\n";

    // count how many numbers in the vector are between 3 and 7
    // we can write the code inline, but it is not so readable
    std::cout << "found:" << count_objects(v, [](int i) { return i >= 3 && i <= 7; }) << "\n";

    int x = 2;
    // here we define a lambda function that returns 'bool'
    // the return type in this example is optional
    // it is also accessing the local variable by value
    auto l = [=](int y) ->bool { return y * x; };
    std::cout << "result:" << l(3) << "\n";

    // here we are accessing x by reference
    auto lr = [&](int y) {++x; return y * x; };
    std::cout << "result:" << x << ", " << lr(4) << "\n";
}

Summary

In this part we covered:

The C++ Programming Language