Defining new data types

C++ language is equipped with several basic data types such as

int, double, bool, char, long int, long long int, long double, etc.

The C++-standard library adds also other commonly used data types like std::string to represent text and std::vector to represent sequences (or lists) of values.

Programmers can also define new data types to represent entities such as a point in a $2$D space or a rectangle. Let’s first see why programmers may want to define their own data types.

Why to define new data types?

Assume that one is writing a program that deals with rectangles and operations with rectangles such as to draw rectangles. For each rectangle, there is the following information associated to it, as illustrated in the figure below.

Rectangle

To define a rectangle one needs a point and the rectangle’s color. A point in a $2$D space is represented by two integer coordinates. The color is represented using the RGB-color model which requires three integers (between $0$ and $255$) to represent red, green, and blue.

A function to draw a rectangle can be declared as follows (function’s implementation is less important now).

void draw_rectangle(int x, int y, int h, int b, int a, int r, int g, int bl);

And, a call to the function follows.

draw_rectangle(2, 2, 4, 8, 45, 255, 153, 102);

Functions with many arguments, like draw_rectangle, are difficult to understand. These functions also induce easily in error when calling them because one needs to recall exactly what each argument, from a rather long list, represents. Obviously, it’s easy to mix up the arguments. One way to tackle the problem is to define a new type to represent rectangles which contains all the data described above (i.e. a point, the height, the base, etc).

// A new type to represent rectangles
struct Rectangle {
  // A point
  // Height
  // Base
  // An angle
  // A color
};

Then, the function draw_rectangle can be declared as follows.

void draw_rectangle(const Rectangle& r);

One can then define variables of type Rectangle and call the function draw_rectangle.

Rectangle my_rectangle = // initialize the rectangle;

draw_reactangle(my_rectangle);

Note that function draw_rectangle has now one argument, instead of height “anonymous” arguments. This latter version requires a lower cognitive burden and recall effort from the programmers to be able to use the function correctly.

Good programming practice: keep the number of function arguments low.

Defining new data types also makes programs more readable and self-explanatory.

Next, we introduce how new data types can be defined with C++ structures (struct) and give examples. In the section Example, we then return to an example using the Rectangle data type.

The basics: struct

A C++-structure is defined using struct keyword and it represents a new data type consisting of a sequence of members. For instance, the struct below defines a point in a $2$D space as two integer members, one representing the $x$-coordinate and the other representing the $y$-coordinate.

struct Point {
  int x = 0;  // one member: the x-coordinate
  int y = 0;  // another member: the y-coordinate
};

It’s then possible to define variables of type Point and initialize them.

Point origin; // coordinates x and y are initialized to zero
Point p1 = {4, 8};

The first point, origin, has its x and y members initialized automatically with zero, while p1’s x and y members are initialized with $4$ and $8$, respectively.

It’s also possible to modify the x and y coordinates of point p1.

p1.x = -1;
p1.y = 10;

Note that variable p1 consists of two integers placed in the memory one after the other. Thus, if an integer occupies four bytes, then p1 consists of (at least) eight consecutive bytes, where the first four bytes are associated with p1.x and the next four bytes are associated with p1.y. The figure below illustrates this idea.

Struc memory

It’s possible that a struct occupies more memory bytes than the sum of its members’ memory due to memory alignment issues (also know as padding). For simplicity, we ignore this aspect in this tutorial.

It’s also possible to define a vector of points. The example below shows a vector initialized with three points: $(5,5)$, $(0,1)$, and $(-2,6)$.

std::vector<Point> points_list = {{5,5}}, {0,1}, {-2,6}};

We can also define

  • a function that reads two user given coordinates and returns a point; and
  • a function to display a point.
Point read_point() {
    std::cout << "Enter point coordinates: ";

    Point p;
    std::cin >> p.x >> p.y;

    return p;
}

void display_point(const Point& p) {
    std::cout << "(" << p.x << ", " << p.y << ")";
}

As you can see, functions can return a struct (e.g. a Point) and a struct can also be a function argument (like in function display_point). Note that call by const reference is used to pass a Point to function display_point. Firstly, we want to avoid copying a struct which has several members. Secondly, the function should not modify the variable (of type Point) passed to it.

It’s also possible to assign a Point variable to another Point variable.

Point origin;
Point p1 = {4, 8};

p1 = origin; // copy origin's coordinates to p1's coordinates
//p1.x = origin.x;
//p1.y = orogin.y;

In this way, all members of origin are copied into the corresponding members of p1.

Comparing variables of type Point with the operators <, <=, >, >=, ==, != is not possible.

if (p1 < origin) { // compilation error!!
    // do something
}

if (p1 == origin) { // compilation error!!
    // do something
}

What would the meaning of p1 < origin be? What are we comparing? The x-coordinates? The y-coordinates? The distance of the points to point $(0,0)$? If the programmer needs to compare points then she must define a boolean function that defines the meaning of the comparison. For instance, one can define a function less_than to compare two points based on their distance to point $(0,0)$. First, a function that returns the distance between two points is defined. This function is then called by function less_than.

#include <cmath>  // std::sqrt, std::pow

double distance(const Point& p1, const Point& p2) {
    return (std::sqrt(std::pow(p1.x - p2.x, 2) + std::pow(p1.y - p2.y, 2)));
}

// Return true if distance of p1 to point(0,0) is less than distance of p2 to (0,0)
bool less_than(const Point& p1, const Point& p2) {
    Point origin = {0,0};

    return distance(p1, origin) < distance(p2, origin);
}

Recall that the math functions std::pow and std::sqrt are defined in the C++-standard library cmath.

We can use function less_than to sort increasingly a list of points stored in a vector by their distance to point $(0,0)$.

void swap(Point& p1, Point& p2){
    Point aux = p1;	  
    p1 = p2;
    p2 = aux;
}

// Bubble sort algorithm
void sort(std::vector<Point>& V) {
    for (int pass = 1; pass < std::ssize(V); ++pass) {
        for (int i = 0; i < std::ssize(V) - 1; ++i) {
            if ( less_than(V[i + 1], V[i]) ) {  // wrong order?
                swap(V[i], V[i + 1]);
            }
        }
    }
}

Example

We return now to our initial problem about rectangles. You can find below a program that reads a list of user given rectangles and stores it in a vector. It then loops through the list and displays the each of the rectangles.

First, two new types are defined, Point and RGB, to represent a point in $2$D-space and a RGB color, respectively. These types are then used to define the type Rectangle.

struct Point {
    int x = 0;
    int y = 0;
};

struct RGB {
    int red = 0;
    int green = 0;
    int blue = 0;
};

struct Rectangle {
    Point lower_left_corner;
    int height = 0;
    int base = 0;
    int angle = 0;
    RGB color;
};

Rectangle r = {{2, 3}, 3, 4, 45, {209, 102, 131}};

The figure below depicts the rectangle r members in the computer’s memory. Assume that each slot in the figure stores an int (i.e. $4$ bytes).

Rectangle memory

The program below reads a list of rectangles given by the user and stores the list in a vector. The user marks the end of the input list by giving a rectangle with a height or base less than or equal to zero. The program then draws all rectangles in the list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <iostream>
#include <vector>

struct Point {
    int x = 0;
    int y = 0;
};

struct RGB {
    int red = 0;
    int green = 0;
    int blue = 0;
};

struct Rectangle {
    Point lower_left_corner;
    int height = 0;
    int base = 0;
    int angle = 0;
    RGB color;
};

Point read_point();

RGB read_color();

Rectangle read_rectangle();

// Read a list of user given rectangles
// Stop reading when height <= 0 or base <= 0
// Return a vector with the rectangles read
std::vector<Rectangle> read_reactangle_list();

void draw_rectangle(const Rectangle& r);


/* ****************** */

int main() {
    // Create a list of rectangles
    std::vector<Rectangle> rectangles_list = read_reactangle_list();
	
    std::cout << "\n";
    std::cout << "\n";

    for (Rectangle r : rectangles_list) {
        draw_rectangle(r);
    }
}

/* ****************** */

Point read_point() {
    std::cout << "Enter point coordinates:\n";

    Point p;
    std::cin >> p.x >> p.y;

    return p;
}

RGB read_color() {
    RGB c;

    std::cout << "Enter a color (RGB values):\n";
    std::cin >> c.red >> c.green >> c.blue;

    return c;
}

Rectangle read_rectangle() {
    Rectangle r;

    r.lower_left_corner = read_point();

    std::cout << "Enter height and base:\n";
    std::cin >> r.height >> r.base;

    std::cout << "Enter angle:\n";
    std::cin >> r.angle;

    r.color = read_color();

    return r;
}

// Read a list of user given rectangles
// Stop reading when height <= 0 or base <= 0
// Return a vector with the rectangles read
std::vector<Rectangle> read_reactangle_list() {
    std::vector<Rectangle> V;
	
    Rectangle r = read_rectangle(); // read first rectangle
    while(r.height > 0 && r.base > 0) { // end of the list?
        V.push_back(r);
        r = read_rectangle(); // read next rectangle
    }

    /*
    // Another way to write the reading loop
    for (Rectangle r = read_rectangle(); r.height > 0 && r.base > 0; r = read_rectangle()) {
        V.push_back(r);
    }
    */

    return V;
}

void draw_rectangle(const Rectangle& r) {
    // Code to draw rectangle r goes here
	
   std::cout << "Drawing a rectangle with area " << r.height * r.base << "\n";  // just for testing
}

Though the code above compiles and executes, we skip here the implementation of function draw_rectangle because it would require the use of graphic libraries which are outside the scope of the course. Some libraries that could be used are, for instance

You can use Coliru and modify the program above to display the RGB color codes and area of each rectangle entered by the user. Write first a function that displays the RGB color and aread of a given rectangle. This function should then be called in the main, for each rectangle stored in the vector rectangles_list.