Session 1: Introduction

Code download

Download the code for the sessions from the GitHub page:
Note: If you are on Windows: place your code in a directory without spaces or accents in the path.
  • E.g. C:\Cours\2A\43043\csc43043ep-lab-code, and not C:\Céline\Mes Cours\csc43043ep-lab-code
Follow the compilation instructions: Installing compilation tools
in order to verify that your environment is properly configured.

Minimal Program

Code: 01_first_program/a_introduction
The organization of the directory files is as follows:
The file src/main.cpp contains the following code:
#include <iostream>
#include <cmath>

int main()
{
    std::cout << "Hello world" << std::endl;
    return 0;
}
We remind the following items:
General remarks:

Functions

Functions follow the following syntax
returnType functionName(type argumentName1, type argumentName2, etc.)
{
    ... function code
    return value;
}
Example:
int addition(int a, int b)
{
    return a+b;
}
The signature of a function must necessarily be declared before it is used. Otherwise there will be a compilation error.
Examples:
int addition(int a, int b)
{
    return a+b;
}

int main()
{
    // OK
    // The signature (and body)
    //  is declared before.
    int c = addition(5,3);
}
int addition(int a, int b);

int main()
{
    // OK
    // The signature is declared before.
    int c = addition(5,3);
}

int addition(int a, int b)
{
    return a+b;
}
int main()
{
    // KO - does not compile,
    //  the function "addition"
    //  is not declared.
    int c = addition(5,3);
}

int addition(int a, int b)
{
    return a+b;
}
> Add a function called "norm" that takes as parameters three values of type "float" representing the coordinates of a vector \((x,y,z)\) and returns the norm of this vector. Verify your function by displaying the results of a few specific cases. The function will have the following signature:
float norm(float x, float y, float z)
Some useful functions
// The square of a value
float x2 = x*x;
// The square root of a value
float y = std::sqrt(x);
// An arbitrary power p of a value x (power not necessarily integer, more generic but more costly function than the two above).
float y = std::pow(x, p);
// Warning: Do not use the ^ or ** symbols which are not power operations in C++.

Fundamental C++ Types (Reminders)

You will mainly use two fundamental types in your code:
You will also encounter the following types:
Note: The char type is guaranteed to be encoded on 1 byte. It can also be used to precisely define values in memory at the byte level.
Remarks:
int a = 5/2; // equals 2 (Euclidean division)
int b = 5%2; // equals 1 (remainder of Euclidean division)
// If you want to get 2.5, at least one of the two arguments must be a float
float c = 5/2.0f; // OK - 2.5
float d = 5.0f/2; // OK - 2.5
float e = float(5)/2; // OK - 2.5
// ... etc
auto a = 5;    // a is an int
auto b = 8.4f; // b is a float
auto c = 4.2;  // c is a double

Arrays/Vectors

The C++ standard library (called STL) defines generic types of vectors/arrays of values stored contiguously in memory.

  • The values of a std::array are stored in so-called stack memory which is more efficient, but limited in size (typically 2MB). The values of a std::vector are stored in so-called heap memory which can be slightly more costly during allocation and intensive use, but which gives access to your entire RAM.
Example of using a std::vector
#include <iostream>
#include <cmath>

// Add inclusion for std::vector and std::array
#include <vector>
#include <array> // this one is optional here

int main()
{
    // Create an empty vector containing integers 
    std::vector<int> vec;

    // the vector is empty at this state, its size is 0

    // Can add elements one after the other (the vector is resized automatically)
    vec.push_back(5);
    vec.push_back(6);
    vec.push_back(2);

    // Check the size of the vector
    std::cout << "Vector has " << vec.size() << " elements" << std::endl;

    // Elements of the vector can be accessed via 
    // vec[0], ..., vec[k], ..., vec[vec.size()-1].
    std::cout <<"first element: " << vec[0] << std::endl;

    // You can write over an indexed element
    vec[1] = 12; // now the second element will be 12

    // We can loop over all elements of a vector
    for (int k = 0; k < vec.size(); ++k) {
        std::cout << "Element " << k << " : " << vec[k] << std::endl;
    }

    // You should not access/write over an element beyond the size of the vector
    // Try to uncomment these two lines and see the result (the program may crash, but this is actually un undefined behavior that depends on your system).
    //  vec[8568] = 12;
    //  std::cout<<vec[8568]<<std::endl;

    // You can resize (or allocate an initial size) for the vector using .resize(N)
    vec.resize(10000);
    // previously existing elements (the 3 first values) are kept, and the new elements are initialized to 0

    std::cout << "New vector size: " << vec.size() << std::endl;
    
    // Now it is legit to access/write on element vec[8568]
    vec[8568] = 12;
    std::cout << "Element 8568 : " << vec[8568] << std::endl;

    return 0;
}
> Implement a function that generates and returns a std::vector of size \(N\geq 2\) passed as a parameter, whose content varies between 0 and 1 with an increment step of \(1/(N-1)\).
The function signature must be
std::vector<float> generate_vector(int N);
Verify some values of your vector
int main()
{
    std::vector<float> vec = generate_vector(101);
    std::cout<<vec[0]<<std::endl; // should be 0
    std::cout<<vec[100]<<std::endl; // should be 1
    std::cout<<vec[50]<<std::endl; // should be 0.5
    std::cout<<vec[25]<<std::endl; // should be 0.25

    return 0;
}
> Implement a function that computes the norm of a vector of generic size passed as a parameter. The function signature will be
float norm(std::vector<float> vec);
// or
// float norm(const std::vector<float>& vec);
Note that this is function overloading: two functions share the same name norm, but with different signatures (types and/or number of parameters). The compiler automatically chooses the correct version to call based on the arguments passed.
The first signature norm(std::vector<float> vec) passes the vector by copy: the entire content of the vector is duplicated in memory at each call. The second signature norm(const std::vector<float>& vec) passes the vector by constant reference: no copy is made, only a reference to the original vector is transmitted. The const keyword guarantees that the function will not modify the content of the vector. Passing by constant reference is preferred for large objects (vectors, strings, etc.) as it avoids a costly copy in memory and time.

Export ASCII

Code: 01_first_program/b_ascii_tree
We now place ourselves in the context of the directory 01_first_program/b_ascii_tree. Open the code in this directory and compile it following the same steps as above.
> In the main.cpp file, complete the function void print_tree(int levels) to display on the command line an ASCII tree whose height is given as a parameter. The tree should resemble the example below:
/*
* Example for `levels = 4` ,   `levels = 5` :
* 
*    *                                *
*   ***                              ***
*  *****                            *****
* *******                          *******
*   |||                           *********
*                                    |||
*/
Hints : > Then modify your function so that it returns the ASCII content in the form of a std::string. Note that one can concatenate std::string with the operator + and +=, or append at the end of the string with the function [string].append([to_add]). The display in the main function will then be performed with the following call:
int main() {
    std::cout<< print_tree(4) <<std::endl;
    return 0;
}
> Create a function that exports a Christmas tree to a text file. We can consider the following function signature:
// filename: name of the output file
// level: height of the tree
void export_tree_file(const std::string& filename, int level);

ASCII Histogram

Code: 01_first_program/c_ascii_histogram
In this exercise, we want to count the occurrences of each word in a text file, then display the result as an ASCII histogram. The file src/input.txt contains a text that will serve as input.

std::map

To associate each word with its number of occurrences, we use the std::map<Key, Value> container from the standard library (requires #include <map>). A std::map is a dictionary that associates a key with a value (similar to a Python dictionary).
#include <map>
#include <string>

std::map<std::string, int> histogram;

// Insertion / update of an entry
histogram["hello"] = 1;

// Increment: if the key does not yet exist,
//   it is automatically created with value 0, then incremented.
histogram["hello"]++; // now equals 2
histogram["world"]++; // created with value 1

// Iterating over the map (keys are sorted alphabetically)
for (const auto& it : histogram) {
    std::cout << it.first << " : " << it.second << std::endl;
    // it.first  = the key   (std::string)
    // it.second = the value (int)
}

Reading a text file

To read a file, we use std::ifstream (requires #include <fstream>). The >> operator allows reading words one by one (automatically separated by spaces and line breaks).
#include <fstream>
#include <string>

std::ifstream file("src/input.txt");

// Check if the file was opened successfully
if (!file) {
    std::cerr << "Error: unable to open the file" << std::endl;
}

// Read word by word
std::string word;
while (file >> word) {
    std::cout << word << std::endl; // displays each word
}

Exercises

> Implement the function build_word_histogram that reads a text file and builds a std::map counting the occurrences of each word. The function signature is:
std::map<std::string, int> build_word_histogram(const std::string& filename);
Steps:
> Implement the function print_histogram that displays the content of the map as a horizontal ASCII histogram. The function signature is:
void print_histogram(const std::map<std::string, int>& hist);
For each entry in the map, display the word, then the | character, then as many asterisks as the number of occurrences.
Example output (excerpt):
a | ************
and | *********
children | ******
the | ******************
Hints:

PPM Image Export

Code: 01_first_program/d_ppm_image
In this part, we want to export a simple image in the PPM (Portable PixMap) format. It is an extremely minimalist image format, convenient for beginners because it requires no external libraries. We will use the ASCII variant of the format, called P3 (the pixel values are written as numbers in a text file).

Image structure in memory

We use the following structure:
The function set_pixel(x,y, r,g,b) writes the components at the correct position in the array.

Format PPM (P3)

A PPM ASCII file follows this format:
P3 <Nx> <Ny>
255
r g b  r g b  r g b ...
...

Conversion float -> int

Our pixels are stored as floats between 0 and 1, while the PPM file requires integers between 0 and 255. We can use a conversion such as:
> Complete the following function to export an image_structure image in PPM ASCII (P3) format. Signature:
void export_ppm(const image_structure &img, const std::string &filename);
Constraints:
Hints:
// Creating a file
std::ofstream out(filename);

// Writing
out << "some text" << "\n";

Quick tests

Once export_ppm is implemented, you can create small images (e.g. 32x32) and visually verify the result.
> Do a first test: The file output.ppm should be openable by some image software (or easily converted).
Note. If your viewer does not read PPM directly, you can use tools such as GIMP.

Generating simple procedural images

> Create a function that generates a horizontal black → white gradient:
assets/gradient.jpg
> Create a checkerboard image:
assets/checkerboard.jpg
> Create a noise image (random_noise):
float rand_float = static_cast <float> (rand()) / static_cast <float> (RAND_MAX);
assets/random_noise.jpg
> Create a colored disk centered in the image:
assets/disk.jpg