Data vs control (struct and enum)

This section will discuss data driven programming. Before that, you will learn two C++ data types you need in order to understand the explanation.

Record type (struct in C++)

Many programming languages allow the programmer to define their own data types which can be used to group different types of data elements. In a way, it is like a primitive version of class types: There is no function interface, but all the fields can be accessed directly. A data type like this is often called a record or, in C++, struct.

In C++, a record is defined using the reserved word struct:

struct Product {
    string product_name;
    double price;
};

After the definition above, the new type Product can be used pretty much the same way as the other data types:

Product item = {"soap", 1.23};

You can access the partial information of a Product type variable with the . operator:

item.price = 0.9 * item.price;  // Discount 10 %
cout << item.product_name << ": " << item.price << endl;
item = { "orange", 0.45 };

You can use the struct as a value that you store in the STL container, as a parameter of a function (a value or a reference parameter), and as a return value.

We can describe a person in a struct:

struct Person {
    string forename;
    string surname;
    string address;
    string email;
};

All fields of the above struct are of the same type. Therefore, the above data could have been stored in four elements’ vector. However, here struct is a better solution (and the correct one), since now we can use named fields instead of numeric indices. In a vector, you should not store data items, the meanings of which differ, although they have the same type.

Enumerated type

Sometimes you need a variable that can only have certain values. For example, let us say that the status of a book in the library software can only be either on the shelf, borrowed, reserved, or lost. Naturally, it is possible to use the types string or int, but they would allow many other values besides the aforementioned ones. That is why they are not the best options for storing the status of a book.

There is a data type called enumeration type (or actually a class), for which all the possible values (elements) are defined by the programmer.

In C++, the enumeration type (class) is defined as follows:

enum class Type_name { element_1, ... , element_n };

Above, we defined a new type called Type_name, and it contains the elements Type_name::element_1, …, Type_name::element_n. In a more concrete way:

enum class Book_status { ON_THE_SELF, BORROWED, RESERVED, LOST };

When using enumerated values, the name of the class is required as a specifier:

Book_status status;
status = Book_status::ON_THE_SELF;

Enumerated types are often an useful alternative when a program contains related constant values. For example, instead of writing:

const int RED = 0;
const int YELLOW = 1;
const int GREEN = 2;

One could instead define an enumerated type:

enum class TrafficLight { RED, YELLOW, GREEN };

Sometimes there is a need to write a loop construct to iterate over all the values of an enum type. The following example gives ideas how this could be achieved.

#include <iostream>

using namespace std;

// C++ automatically assigns an integer value for each
// enum class element. The numbering starts from zero:
//     MON = 0, TUE = 1, ... , SUN = 6
// However, there is no automatic conversion from
// enum values to int values or vice versa. This has
// to be done manually using static_cast.
enum class Weekday {
  // 0    1    2    3    4    5    6
    MON, TUE, WED, THU, FRI, SAT, SUN
};

// Since C++ numbers the enum values starting from zero,
// the number of days in a week will be SUN (6) + 1.
const int DAYS_IN_A_WEEK = static_cast<int>(Weekday::SUN) + 1;

Weekday next_weekday(Weekday day) {
    // Why do you think there is the remainder operator (%)
    // included as a part of the next day calculation?

    return static_cast<Weekday>((static_cast<int>(day) + 1) % DAYS_IN_A_WEEK);
}

int main() {
    cout << "A week has " << DAYS_IN_A_WEEK << " days and they are:" << endl;

    // Please note, that the end condition of this for-loop
    // is pointless. This is because day is always less or equal than SUN
    // as Sunday is the "largest" day. The loop is ended with
    // break-command when the day is SUN. A matter of taste perhaps,
    // but maybe for-loop is not the clearest choice in a situation
    // like this where the looped values are cyclic (SUN is followed
    // by MON forever and ever).

    for ( Weekday day = Weekday::MON; day <= Weekday::SUN; day = next_weekday(day) ) {
        cout << "Day number " << static_cast<int>(day) << ": ";

        if ( day == Weekday::MON ) {
            cout << "Monday :( :(" << endl;

        } else if ( day == Weekday::TUE ) {
            cout << "Tuesday :(" << endl;

        } else if ( day == Weekday::WED ) {
            cout << "Wednesday :(" << endl;

        } else if ( day == Weekday::THU ) {
            cout << "Thursday :|" << endl;

        } else if ( day == Weekday::FRI ) {
            cout << "Friday :)" << endl;

        } else if ( day == Weekday::SAT ) {
            cout << "Saturday :) :)" << endl;

        } else if ( day == Weekday::SUN ) {
            cout << "Sunday :|" << endl;

            // Loop can be finished since the whole week
            // has been printed.
            break;
        }
    }

    // Of course a while-loop would also work.

    Weekday day = Weekday::MON;

    while ( true ) {
        cout << "Day number " << static_cast<int>(day) << ": ";

        if ( day == Weekday::MON ) {
            cout << "Monday :( :(" << endl;

        } else if ( day == Weekday::TUE ) {
            cout << "Tuesday :(" << endl;

        } else if ( day == Weekday::WED ) {
            cout << "Wednesday :(" << endl;

        } else if ( day == Weekday::THU ) {
            cout << "Thursday :|" << endl;

        } else if ( day == Weekday::FRI ) {
            cout << "Friday :)" << endl;

        } else if ( day == Weekday::SAT ) {
            cout << "Saturday :) :)" << endl;

        } else if ( day == Weekday::SUN ) {
            cout << "Sunday :|" << endl;
        }

        day = next_weekday(day);

        // day can only be MON here, if the previous day was SUN
        // Or in other words, all the weekdays have been
        // printed once already: time to end the loop.

        if ( day == Weekday::MON ) {
            break;
        }
    }
}

There is one more useful thing to know when it comes to loops and enumerated types. Range-based for loop can be useful, when a partial set of enum values is to be looped over:

for ( Weekday day: { Weekday::WED, Weekday::MON, Weekday::FRI } ) {
    cout << "Weekday number is " << static_cast<int>(day) << endl;
}

Data driven programming

A computer program consists of data and the various commands targeting it. In reality, the matter is not divided that clearly, because in some situations, data can replace some of the commands and vice versa. The so-called data driven or data directed designing and programming utilize that characteristic.

The idea of data driven programming is to design and implement data structures within a program that reduce the amount of data processing commands, that is, to replace certain commands by data. You can see this more clearly from the example using the before mentioned types struct and enum. (The complete program code of the example is located in Git: examples/03/datadrivenprogramming).

Let us first think about a snippet of code in which we will not use the philosophy of data driven programming:

#include <string>

enum class PostalAbbr {AL, AK, AZ, AR, CA, CO, ERROR_CODE};  // Excluded the rest 44 elements

// Version 1: First idea
PostalAbbr name_to_abbreviation(const string& name)
{
    if(name == "Alabama"){
        return PostalAbbr::AL;
    } else if (name == "Alaska"){
        return PostalAbbr::AK;
    } else if (name == "Arizona"){
        return PostalAbbr::AZ;
    } else if (name == "Arkansas"){
        return PostalAbbr::AR;
    } else if (name == "California"){
        return PostalAbbr::CA;
    } else if (name == "Colorado"){
        return PostalAbbr::CO;
    } else {   // Excluded 44 "else if" blocks
        return PostalAbbr::ERROR_CODE;
    }
}

The if structure above would be a lot longer in reality if the rest of the code with the remaining 44 states was written out. If the same piece of code will be written according to the principles of data driven programming, we could get rid of the long if sentence like this:

#include <string>
#include <vector>

enum class PostalAbbr {AL, AK, AZ, AR, CA, CO, ERROR_CODE};  // Excluded the rest 44 elements

struct StateInfo {
    string name;
    PostalAbbr abbreviation;
};

const vector<StateInfo> STATES = {
    { "Alabama", PostalAbbr::AL },
    { "Alaska", PostalAbbr::AK },
    { "Arizona", PostalAbbr::AZ },
    { "Arkansas", PostalAbbr::AR },
    { "California", PostalAbbr::CA },
    { "Colorado", PostalAbbr::CO }  // Excluded 44 lines
};

// Version 2: Better solution
PostalAbbr name_to_abbreviation(const string& name)
{
    for(auto s : STATES) {
        if(name == s.name){
            return s.abbreviation;
        }
    }
    return PostalAbbr::ERROR_CODE;
}

The difference between these two examples is clear: After going through a little bit of trouble to design and initialize a suitable data structure, the actual function name_to_abbreviation was decreased significantly. The technique of data driven programming is always - in one way or another – the use of pre-processed information comprised of initial values and the results from them.

The benefits of data driven programming include:

  1. The programs created are usually shorter, but still clear.

  2. The possibility of making errors is smaller.

  3. The programs are easier to expand and maintain.

A clear sign of a need for data driven programming within a program is an if structure that grows out of proportion (or a switch structure, which we have not studied so far.)