Very Basic Elements of C++

1 Hello world

Write and compile the following C++ program:

#include <iostream>

int main() {
    std::cout << "Hello world!" << std::endl;
}

Since it's good to start with examples, let's try a non-trivial program:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    int i;
    std::vector<int> A;
    while (std::cin >> i)
	A.push_back(i);

    std::sort(A.begin(), A.end());

    for (std::vector<int>::iterator itr = A.begin(); itr != A.end(); ++itr)
	std::cout << *itr << std::endl;
}

The above code examplifies many features of C++.

2 Small differences

2.1 Headers

Standard headers do not have a ".h" name suffix.

#include <iostream>

All the C standard headers are available with a "c" prefix, and without the ".h" name suffix.

#include <cstdio>
#include <cassert>

2.2 Null pointers

Since C++11, C++ has a null pointer value

int * p = nullptr;

There is also a definition of a null pointer type in the standard library, defined in the cstddef header:

typedef decltype(nullptr) nullptr_t; // std::nullptr_t

2.3 Boolean type and values

C++ has a specific Boolean type

bool hungry = false;
bool happy = true;

3 References

A reference is an alias to another object.

int i = 1;
int & ref = i;                  // reference initialization

The code above declares ref as an alias of object i. Notice that the reference is not itself an object. In fact, in this case the compiler might implement the reference without allocating any memory, conceptually replacing every use of ref with i. In some cases, the compiler might need to store a reference, in which case the underlying implementation will consist of a pointer object.

Notice that there is a fundamental difference between the initialization and an assignment of a reference.

int i = 1;
int j = 2;
int & ref = i;                  // reference initialization
ref = j;                        // assignment.  Of what object?
j = j + 1;
std::cout << ref << std::endl;

Since a reference is an alias and not an object, every reference must be immediately initialized.

int & ref;                      // error: makes no sense

Also, again since a reference is not an object, it makes no sense to declare arrays of references, pointers to references, and references to references.

int i;                          // i is the only object here
int & ref = i;                  // ref is now an alias of i
int & * p;                      // error: makes no sense
int & A[10];                    // error: makes no sense
int & & double_ref;             // error: makes no sense

References can and are often used as formal parameters for methods and functions. What is the output of the following program?

void swap(int & a, int & b) {
    int tmp = a;
    a = b;
    b = tmp;
}

int main() {
    int i = 7;
    int j = 2;
    swap(i, j);
    std::cout << "i = " << i << std::endl << "j = " << j << std::endl;
}

Notice that the initialization of a reference must evaluate to an l-value. Do you see why? Consider the following code as an example.

int i;
int & r = i;                    // okay
int & s = i + 1;                // Error!

However, the initialization of a const reference—meaning, a reference to a const object—does not have to be an r-value. Do you see why? Consider the following code as an example.

int sum(const int & a, const int & b) {
    return a + b;
}

int main() {
    int i = 7;
    int j = 2;
    std::cout << sum(i, j + 1) << std::endl;
}

4 Namespaces

Namespaces support basic modularization

namespace usi {

namespace math_lib {

int sum(int a, int b) {
    return a + b;
}

} // namespace math_lib

} // namespace usi

int vector_sum(int * begin, int * end) {
    int s;
    for(s = 0; begin != end; ++begin) 
	s = usi::math_lib::sum(s, *begin); // qualified name for function sum()
    return s;
}

Namespaces are what they say they are: confined spaces for names to modularize and avoid conflicts.

namespace usi {

namespace math_lib {

int sum(int a, int b) {
    return a + b;
}

} // namespace math_lib

} // namespace usi

namespace mod_7 {

double sum(int a, int b) {
    return (a + b) % 7;
}

}

using mod_7::sum;

int main() {
    int i;
    int j;
    i = sum(1,2);
    j = usi::math_lib::sum(1,2);
}

The C programming language does not have namespaces, and therefore a similar modularization is achieved through name prefixes:

/* namespace "usi" { print(); set_name(); get_name() */
void usi_print();
void usi_set_name(const char * name);
const char * usi_get_name();

5 "using" declarations

using namespace std;

using InputStream = std::istream;
// equivalent to the following C declaration:
typedef std::istream InputStream;

int main() {
    cout << "ciao!" << endl;
};

6 Exceptions

C++ provides exceptions as a powerful error-handling mechanism. You may throw any value.

#include <string>
#include <iostream>

const char * small_error = "okay";
const char * big_error = "not okay";
const char * nasty_error = "AAAHHhhhh!";

int f(int x) {
    if (x > 7)
	return x - 2;

    switch(x) {
    case 0: throw 10;
    case 1: throw 20;
    case 2: throw small_error;
    case 3: throw big_error;
    default: throw nasty_error;
    }
}

int main() {
    int i;
    std::cout << "Input: ";
    std::cin >> i;
    try {
	std::cout << "Output: " << f(i) << std::endl;
    } catch (int ex) {
	std::cout << "error number " << ex << std::endl;
    } catch (const char * error_msg) {
	std::cout << error_msg << std::endl;
    }
}

6.1 Example with "throw" specification

#include <string>
#include <iostream>
#include <exception>

const char * small_error = "okay";
const char * big_error = "not okay";
const char * nasty_error = "AAAHHhhhh!";

void abandon_ship() {
    std::cout << "Mayday mayday mayday!" << std::endl;
}

int f(int x) throw(int) {
    if (x > 7)
	return x - 2;

    switch(x) {
    case 0: throw 10;
    case 1: throw 20;
    case 2: throw small_error;
    case 3: throw big_error;
    default: throw nasty_error;
    }
}

int main() {
    std::set_unexpected(abandon_ship);

    int i;
    std::cout << "Input: ";
    std::cin >> i;
    try {
	std::cout << "Output: " << f(i) << std::endl;
    } catch (int ex) {
	std::cout << "error number " << ex << std::endl;
    } catch (const char * error_msg) {
	std::cout << error_msg << std::endl;
    }
}

6.2 Example with "noexcept" specification

#include <string>
#include <iostream>
#include <exception>

const char * small_error = "okay";
const char * big_error = "not okay";
const char * nasty_error = "AAAHHhhhh!";

void abandon_ship() {
    std::cout << "Mayday mayday mayday!" << std::endl;
}

void ciao() noexcept {
    std::cout << "ciao!" << std::endl;
}

void goodbye() {
    std::cout << "goodbye!" << std::endl;
}

int f(int x) throw(int) {
    if (x > 7)
	return x - 2;

    switch(x) {
    case 0: throw 10;
    case 1: throw 20;
    case 2: throw small_error;
    case 3: throw big_error;
    default: throw nasty_error;
    }
}

int main() {
    std::set_unexpected(abandon_ship);

    int i;
    std::cout << "Input: ";
    std::cin >> i;
    try {
	std::cout << "Output: " << f(i) << std::endl;
    } catch (int ex) {
	std::cout << "error number " << ex << std::endl;
    } catch (const char * error_msg) {
	std::cout << error_msg << std::endl;
    }
    try {
	ciao();                        // compiler can ignore the whole try-catch
    } catch (int ex) {
	// because this will never happen
	std::cout << "error number " << ex << std::endl;
    }
    try {
	goodbye();                        // compiler must deal with the try-catch blocks
    } catch (int ex) {
	std::cout << "error number " << ex << std::endl;
    }
}

7 Classes

7.1 Example: a String class

#include <cstddef>

class String {
public:
    String();
    String(const char *);
    String(const char * begin, const char * end);
    String(const char * begin, std::size_t len);
    String(const String &);

    ~String();

    std::size_t size() const;

    std::size_t append(const char *);

private:
    char * data;
    std::size_t string_size;
    std::size_t allocated_size;
};

7.2 Constructors and initialization

A constructor has code, just like any other methods. A constructor may also specify a list of initialization expressions for the data members. For example:

#include <string>

class named_object {
    std::string name;

public:
    named_object();
    named_object(const char * n);

    std::ostream & print(std::ostream & output);
};

named_object::named_object()
    : name("Nemo") {};

named_object::named_object(const char * n)
    : name(n) {};

Notice that the list defines initializations of the data members in terms of their specific constructors. In this case, name is of type std::string, which has a constructor that takes a const char * and that is invoked in both cases.

The direct base classes of a class can be considered data members, and therefore can also be initialized in the same way. For example:

class pixmap {
    // this section is, by default, private
public:
    pixmap(const char * n): name(n) {};

private:
    const char * name;
};

class ascii_pixmap : public pixmap { // class ascii_pixmap extends pixmap
public:
    ascii_pixmap(unsigned int width, unsigned int height);

private:
    unsigned int width;
    unsigned int height;
    char * data;
    char fg;
    char bg;
};

ascii_pixmap::ascii_pixmap(unsigned int w, unsigned int h)
    : pixmap("ascii"), width(w), height(h), fg('*'), bg(' ') {
    data = new char[width*height]; // WARNING: check allocation
    std::cout << "ascii_pixmap::ascii_pixmap() called." << std::endl;
}

7.3 Copy constructor

A copy constructor for a class T is a constructor that is declared to take a reference to an object or type T as parameter. More specifically T &, const T & (and other cases).

class vec2d {
public:
    double x;
    double y;

    vec2d() : x(0), y(0) {};                    // "default" constructor
    vec2d(double a, double b) : x(a), y(b) {};  // other constructor
    vec2d(const vec2d & v) : x(v.x), y(v.y) {}; // copy constructor
};

A copy constructor is invoked every time you initialize an object by copying another object (or r-value expression) of the same type. This happens explicitly in the examples below:

vec2d v1;                       // invokes default constructor
vec2d v2(10, 3.2);              // invokes the second constructor
vec2d v3(-2, 8);                // invokes the second constructor
vec2d v4 = v2;                  // invokes the copy constructor
vec2d v5(v3);                   // also invokes the copy constructor

The copy constructor is also invoked every time an object is initialized implicitly. This happens, for example, with the formal parameters of a function or method. For example, if one defines the method:

bool orthogonal(vec2d u, vec2d v) { // u and v are copy-constructed
    return (u.x*v.x + u.y*v.y == 0);
}

the following invocation of orthogonal implicitly calls the copy constructor:

if (orthogonal(v1, v3))
    std::cout << "v1 and v3 are orthogonal." << std::endl;
else
    std::cout << "v1 and v3 are not orthogonal." << std::endl;

7.4 Constructors and assignments

There is a difference between an assignment and an initialization. For example:

int i = 7;                      // this is an initialization
int j;
j = 8;                          // this is an assignment

The example above is based on int objects. However, the same is true for user-defined types—consistent with the principle that user-defined types should be used exactly like basic, predefined types. For example:

std::string s = "ciao";         // this is an initialization (constructor)
std::string t;                  // default constructor
t = "mamma";                    // assignment

And of course C++ allows the programmer to define class-specific assignment operators. This is what happens with the assignment of the standard string objects in the example above. For our vec2d example, we could define various operators, including the assignment operator. This is sometimes called operator overloading. We will discuss operator overloading in more detail later.

7.5 Complete example: constructors, copy-constructors, and assignment

The example below illustrates the syntax and semantics of constructors, including copy-constructors, and assignments (and other operators).

#include <iostream>

class vec2d {
public:
    double x;
    double y;

    vec2d() : x(0), y(0) {
	std::cout << "default constructor." << std::endl;
    };

    vec2d(double a, double b) : x(a), y(b) {
	std::cout << "constructor with a=" << a << " b=" << b << std::endl;
    };

    vec2d(const vec2d & v) : x(v.x), y(v.y){
	std::cout << "copy constructor with v.x=" << v.x << " v.y=" << v.y << std::endl;
    };

    vec2d & operator = (/* (*this) is the default first (left-hand) operand */ const vec2d & v) {
	x = v.x;
	y = v.y;
	std::cout << "assignment operator with v.x=" << v.x << " v.y=" << v.y << std::endl;
	return *this;
    };  

    vec2d operator - (const vec2d & v2) {
	return vec2d(x - v2.x, y - v2.y);
    }
};

std::ostream & operator << (std::ostream & output, const vec2d & v) {
    output << "(" << v.x << "," << v.y << ")";
    return output;
}

vec2d operator - (const vec2d & v) {
    return vec2d(-v.x, -v.y);
}

vec2d operator - (const vec2d & v1, const vec2d & v2) {
    return vec2d(v1.x-v2.x, v1.y-v2.y);
}

bool orthogonal(vec2d u, vec2d v) { // u and v are also copy-constructed
    return (u.x*v.x + u.y*v.y == 0);
}

int main() {
    vec2d v1;                   // invokes default constructor
    vec2d v2(10, 3.2);          // invokes the second constructor
    vec2d v3(-2, 8);            // invokes the second constructor
    vec2d v4 = v2;              // invokes the copy constructor
    vec2d v5(v3);               // also invokes the copy constructor

    v1 = -v4;
    std::cout << "v1 = " << v1 << std::endl;
    std::cout << "v4 = " << v4 << std::endl;
    if (orthogonal(v1, v3))
	std::cout << "v1 and v3 are orthogonal." << std::endl;
    else
	std::cout << "v1 and v3 are not orthogonal." << std::endl;
};

The output of the program above is:

default constructor.
constructor with a=10 b=3.2
constructor with a=-2 b=8
copy constructor with v.x=10 v.y=3.2
copy constructor with v.x=-2 v.y=8
constructor with a=-10 b=-3.2
assignment operator with v.x=-10 v.y=-3.2
v1 = (-10,-3.2)
v4 = (10,3.2)
copy constructor with v.x=-2 v.y=8
copy constructor with v.x=-10 v.y=-3.2
v1 and v3 are not orthogonal.

7.6 Virtual methods

The following code exemplifies the syntax and semantics of virtual methods.

class A {
public:
    virtual int f();
    virtual int g(int x);

    int a;
};

int A::f() {
    std::cout << "A::f() called" << std::endl;
    return 0;
}

int A::g(int x) {
    std::cout << "A::g(" << x << ") called" << std::endl;
    return x + 1;
}

class B : public A {
public:
    virtual int f();

    int b;
};

int B::f() {
    std::cout << "B::f() called" << std::endl;
    return 10;
}

int main() {
    A * a;
    B * b = new B();

    a = b;

    std::cout << a->f() << std::endl;
    std::cout << b->g(10) << std::endl;
}

8 Operator overloading

Imagine you are defining a matrix class to represent matrices in a mathematical library. In C you might do that as follows:

struct matrix {
    unsigned int cols;
    unsigned int rows;
    float * values;
};

You might then define and then use an addition operation as follows:

void matrix_add(struct matrix * A, const struct matrix * B) {
    /* computes A += B */
    if (A->cols != B->cols || A->rows != B->rows)
	return A;
    int * a = A->data;
    const int * a = B->data;
    for (unsigned int i = 0; i < A->rows; ++i)
	for (unsigned int j = 0; j < A->cols; ++j)
	    ,*a++ += *b++;
}

int main() {
    struct matrix A;
    struct matrix B;

    /* ... */
    matrix_add(&A, &B);
}

This solution would be pretty much identical in Java. However, in C++ you can more naturally define an addition operation by defining the += operator. You can do that on the same exact struct matrix defined above:

matrix & operator += (matrix & A, const matrix & B) {
    /* computes A += B and returns A */
    if (A.cols != B.cols || A.rows != B.rows)
	return A;
    int * a = A.data;
    const int * a = B.data;
    for (unsigned int i = 0; i < A.rows; ++i)
	for (unsigned int j = 0; j < A.cols; ++j)
	    *a++ += *b++;
    return A;
}

int main() {
    struct matrix A;
    struct matrix B;

    /* ... */
    A += B;
}

9 Container library

One of the great advantages of C++ over C is the utility of the container library. It is fair to say that the C standard library lacks even basic data structures and algorithms. On the contraty, the standard library of C++ is very rich and also versatile.

Let's look at some basic library features. The library covers strings.

#include <string>
#include <iostream>

int main() {
    std::string s;
    while (std::cin >> s) {
	std::string::size_type pos = s.find("ciao");
	if (pos == std::string::npos) {
	    std::cout << s << " does not contains the string `ciao'." << std::endl;
	} else {
	    std::cout << s << " contains the string `ciao' at position " << pos << std::endl;
	}
    }
}

As it turns out, strings are a lot like a container of the container library of the C++ standard library. So, let's look at perhaps the most basic container, namely the vector:

#include <vector>
#include <iostream>

int main() {
    std::vector<int> A;
    int x;

    while (std::cin >> x)
	A.push_back(x);

    std::cout << "I just read " << A.size() << " numbers:" << std::endl;
    for (std::vector<int>::iterator itr = A.begin(); itr != A.end(); ++i)
	std::cout << *itr << std::endl;
}

The vector class defines a generic vector. Generic here means with respect to the type of the elements in the vector, which in this example is int.. Thus vector is a template class, just like practically all container classes of the C++ Standard library. We now look a the idea of a template container a bit more closely.

9.1 A Template Library

How would you implement a container library in C? For example, how would you implement a list in C? You should first ask a fundamental question: a list of what?

Let's start with a list of integers. You would write something line the following:

/* a single-link list of integers */
struct list_int {
    int value;
    struct list_int * next;
};

struct list_int * list_int_add(struct list_int * l, int v) {
    struct list_int * res = malloc(sizeof(struct list_int));
    if (res) {
	res->value = v;
	res->next = l;
    }
    return res;
}

int list_int_find(struct list_int * l, int v) {
    for (; l; l = l ->next)
	if (l->value == v)
	    return 1;
    return 0;
}

Now, imagine you have to have another list, say, a list of double. What would you do? You would probably write the same exact code, except that you would change the value type to double:

/* a single-link list of doubles */
struct list_double {
    double value;
    struct list_double * next;
};

struct list_double * list_double_add(struct list_double * l, double v) {
    struct list_double * res = malloc(sizeof(struct list_double));
    if (res) {
	res->value = v;
	res->next = l;
    }
    return res;
}

int list_double_find(struct list_double * l, double v) {
    for (; l; l = l ->next)
	if (l->value == v)
	    return 1;
    return 0;
}

An alternative design would be to have a list of pointers to objects of any type. You can do that with a list of pointers to void.

/* a single-link list of doubles */
struct list_generic {
    void * value;
    struct list_generic * next;
};

struct list_generic * list_generic_add(struct list_generic * l, void * v) {
    struct list_generic * res = malloc(sizeof(struct list_generic));
    if (res) {
	res->value = v;
	res->next = l;
    }
    return res;
}

This works nicely. However, notice that each element in the list requires two separate allocations: one for the structural element of the list (struct list_generic), and one for the element object itself, which in the example above is delegated and therefore owned by the user.

Notice also that looking for an element is not as immediate either. Copying the same code as above would result in a comparison between pointers, which is typically not semantically meaningful:

int list_generic_find(struct list_generic * l, void * v) {
    for (; l; l = l ->next)
	if (l->value == v)
	    return 1;
    return 0;
}

Instead, we would have to also define a comparison function. We could do it like so:

int list_generic_find(struct list_generic * l, void * v, int (*compare)(void *, void *)) {
    for (; l; l = l ->next)
	if (compare(l->value, v))
	    return 1;
    return 0;
}

Fortunately, C++ also allows to define generic classes that can be then specialized, or instantiated for a specific type. So, in C++ you could write something like this:

/* a single-link list of doubles */
template <typename T> 
struct list {
    T  value;
    struct list * next;
};

template <typename T> 
struct list<T> * list_add(struct list<T> * l, const T & v) {
    struct list<T> * res = malloc(sizeof(struct list<T>));
    if (res) {
	res->value = v;
	res->next = l;
    }
    return res;
}

template <typename T> 
int list_find(struct list<T> * l, const T & v) {
    for (; l; l = l ->next)
	if (l->value == v)
	    return 1;
    return 0;
}

Or better yet, we can define everything within a template class:

/* a single-link list of doubles */
template <typename T> 
class list {
private:
    struct list_elem {
	T value;
	list_elem * next;
	list_elem(const T & v, list_elem * n)
	    : value(v), next(n) {};
    };

    list_elem * first;

public:
    list()
	: first(nullptr) {};

    void list_add(const T & v) {
	first = new list_elem(v, first);
    }

    bool list_find(const T & v) {
	for (const list_elem * i = first; i != nullptr; i = i->next)
	    if (i->value == v)
		return true;
	return false;
    }
};

Notice that this code requires that the template type (T) support an equality comparison operator (==). This is quite natural in C++, and of course in any case we can also support alternative designs with ad-hoc comparator functions or objects.

In fact, all of this is already implemented in the C++ container library.

Another fundamental concept in the container library is the concept of iterator. An iterator in a linear data structure is equivalent to a pointer in an array in C. One way to iterate over the elements of an array in C is as follows:

int A[100];

int print_array() {
    for (const int * i = A; i != A+100; ++i)
	printf("%d\n", *i);
}

int print_range(const int * begin, const int * end) {
    for (; begin != end; ++begin)
	printf("%d\n", *begin);
}

int main() {
    /* ... */
    print_range(A, A + 50);
    print_range(A + 50, A + 100);
}

The same pattern is pervasive in the use of the C++ standard library. For example:

#include <vector>

std::vector <int> A(100);

int print_array() {
    for (std::vector<int>::const_iterator i = A.begin(); i != A.end(); ++i)
	printf("%d\n", *i);
}

int print_range(std::vector<int>::const_iterator begin, std::vector<int>::const_iterator end) {
    for (; begin != end; ++begin)
	printf("%d\n", *begin);
}

int main() {
    /* ... */
    print_range(A.begin(), A.begin() + 50);
    print_range(A.begin() + 50, A.end());
}

Notice that in the example above we use iterators exactly like pointers also in the same form of pointer arithmetic. That is, for a pointer to an integer p, we can write p+10 to point to the tenth integer in the array of integers pointed to by p. Similarly, using the vector class of the standard C++ library, we can take an iterator to the beginning of the vector, we can increment an iterator, and we can de-reference an iterator to obtain the element. We can also move an iterator by a number of positions using classic pointer arithmetic. However, this latter operation is not available for all data structures. For example, we can not use it in a linked list. Still, in essence, the concept of iterator is identical also for a list:

#include <list>

std::list <int> L;

int print_list() {
    for (std::list<int>::const_iterator i = L.begin(); i != L.end(); ++i)
	printf("%d\n", *i);
}

int print_range(std::list<int>::const_iterator begin, std::list<int>::const_iterator end) {
    for (; begin != end; ++begin)
	printf("%d\n", *begin);
}

Author: Antonio Carzaniga

Created: 2019-05-21 Tue 23:02

Emacs 25.2.2 (Org mode 8.2.10)

Validate