Lab #1

A development environment and a first C++ class

Cristiano De Michele

Today

Three hours, three parts:

  1. Set up a development environment — compiler, terminal, editor. On Windows: WSL 2, a real Linux inside Windows.
  2. Introduction to C++ (part I) — the object oriented paradigm: encapsulation, polymorphism, inheritance. From C to C++ with the examples N0, N1 and N2.
  3. Task: a vector class from scratch — pvector.hpp and its main in pvector.cpp: header guards, constructor and destructor, overloading of =, + and -, arguments passed by reference, this, const methods, using and constexpr.

pvector is the first brick of the simulation codes (Monte Carlo and Molecular Dynamics) we will write in the next labs: positions, velocities and forces of the particles will all be pvectors.

Part 1
Dev environment

What you need

Linux macOS Windows
Compiler g++ (GCC) clang++ (Apple), also callable as g++ g++ inside WSL 2
Terminal any Terminal / iTerm2 Ubuntu (WSL) / Windows Terminal
Editor your choice your choice VS Code + WSL extension
  • In this course we compile from the terminal, with one command: you see exactly what happens, and it works the same way everywhere.
  • Any editor is fine (VS Code, vim, emacs, …): what matters is to have a C++ compiler that supports at least C++17, the 2017 version of the C++ standard.
  • On Windows we do not use the native compilers (MinGW, Visual Studio): with WSL you get the same environment as on a Linux machine (and later the same packages, e.g. for the molgl visualizer).

Windows: what is WSL 2

WSL = Windows Subsystem for Linux. Version 2 runs a real Linux kernel in a lightweight virtual machine managed by Windows:

  • you get a Linux distribution (Ubuntu by default) with its terminal, apt, g++, make, gdb, …
  • no dual boot, no VirtualBox: Ubuntu is an app, it starts in a few seconds;
  • Linux and Windows see each other’s files;
  • graphical Linux programs open their windows on the Windows desktop (WSLg: Windows 11, or Windows 10 from build 19044).

Requirements: Windows 11, or Windows 10 version 2004 (build 19041) or later, and hardware virtualization enabled (it usually is on recent laptops).

Installing WSL 2

  1. Open PowerShell as administrator: right click on Start → Terminal (Admin) (Windows 11) or Windows PowerShell (Admin) (Windows 10).

  2. Run

    wsl --install

    It enables the required Windows features, installs WSL 2 and Ubuntu.

  3. Restart Windows.

  4. Open the Ubuntu app (Start menu). The first time it asks for a UNIX username and password: choose them and remember the password, it is the one sudo will ask for. While you type it nothing is shown: that is normal.

  5. Check, from PowerShell:

    wsl -l -v          # lists the distributions: Ubuntu must have VERSION 2

If something goes wrong

  • VERSION is 1 → wsl --set-version Ubuntu 2 (use the NAME printed by wsl -l -v), and for the future wsl --set-default-version 2.

  • Error about virtualization (e.g. 0x80370102) → enable it in the BIOS/UEFI (Intel VT-x or AMD-V / SVM), then retry.

  • wsl --install only prints the help (Windows 10 not up to date) → run Windows Update first. If it still does not work, enable the two features by hand from an administrator PowerShell, restart, run wsl --update (it installs the WSL 2 kernel) and wsl --set-default-version 2, then install Ubuntu from the Microsoft Store:

    dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
    dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
  • Keep WSL up to date: wsl --update; general status: wsl --status.

Inside Ubuntu: the compiler

From the Ubuntu terminal (the same commands on a native Ubuntu/Debian machine). apt is the package manager of Ubuntu; sudo runs a command as administrator and asks for your Linux password:

sudo apt update
sudo apt install build-essential gdb unzip   # g++, gcc, make, C/C++ headers, debugger, unzip
g++ --version                           # check

On macOS the compiler comes with the Xcode command line tools:

xcode-select --install
g++ --version        # it prints "Apple clang ...": fine, g++ is an alias of clang++

On Fedora: sudo dnf install gcc-c++ make gdb.

Later in the course we will also install the libraries for molgl, the program we use to look at the configurations (freeglut, libpng): not today.

WSL: where to keep your files

  • Work in your Linux home (~, i.e. /home/<your Linux user>), e.g. ~/csm, not on the Windows disk /mnt/c/...: compiling and running from /mnt/c is much slower.

  • From Linux, the Windows disk is under /mnt/c. To unpack the zips you downloaded with the browser from the course page:

    mkdir -p ~/csm && cd ~/csm
    unzip /mnt/c/Users/<your Windows user>/Downloads/project_classes.zip   # -> ~/csm/project
    unzip /mnt/c/Users/<your Windows user>/Downloads/cpp_examples.zip     # -> ~/csm/examples

    mkdir -p: no error if the folder already exists; a && b: run b only if a succeeded.

  • From Windows, the Linux files are in the Linux entry of File Explorer, or at \\wsl.localhost\Ubuntu\home\<your Linux user>.

  • Quick trick: explorer.exe . opens the current Linux folder in File Explorer.

The editor: Visual Studio Code

If you do not have a favourite editor, use Visual Studio Code (free, all systems). On Windows install it on Windows, not inside Ubuntu.

Suggested extensions (Extensions panel, Ctrl+Shift+X, on macOS Cmd+Shift+X):

Extension What it gives you
WSL (Microsoft) — Windows only VS Code works on the Linux files and runs the Linux g++
C/C++ (Microsoft) IntelliSense: completion, errors underlined while you type, info on hover, Go to Definition (F12); the debugger
GitHub Copilot AI suggestions and chat; sign in with a GitHub account: Copilot Free has a monthly limit, Copilot Student is free for verified students (GitHub Education)

VS Code: opening the project

  • File → Open Folder… and choose the project folder (~/csm/project): VS Code works on a folder, and writes its settings in .vscode/ inside it.

  • On Windows, open it from the Ubuntu terminal instead:

    cd ~/csm/project
    code .

    The first time VS Code installs its server inside Linux; bottom left you then read WSL: Ubuntu. Install the C/C++ extension when it proposes Install in WSL.

  • IntelliSense and the compiler can disagree: what counts is what g++ says when you compile.

  • Copilot: useful to understand an error or a piece of code. But the point of the labs is that you write pvector.

Compiling and running in VS Code

  • The integrated terminal (Terminal → New Terminal): a Linux terminal (in WSL, in Ubuntu) in the project folder. Type the same g++ command as outside. In the first weeks, do it this way: you see what happens.

  • The ▷ button at the top right of the editor, Run C/C++ File (C/C++ extension): with the file with main open (pvector.cpp, not pvector.hpp), choose C/C++: g++ build and debug active file (on macOS clang++). VS Code compiles the active file, runs it, and writes the recipe in .vscode/tasks.json.

  • That recipe does not use our options: add them at the beginning of "args" in .vscode/tasks.json:

    "args": [
        "-std=c++17", "-Wall",
        ...the arguments that are already there ("-g", "${file}", ...)
    ],
  • Ctrl+Shift+B (macOS Cmd+Shift+B, Terminal → Run Build Task) compiles with the same recipe; F5 starts the debugger (we will use it later).

Check: compile and run

hello.cpp:

#include <iostream>
int main()
{
  std::cout << "Hello from C++!" << std::endl;
  return 0;
}
g++ -std=c++17 -Wall -o hello hello.cpp   # compile
./hello                                    # run: Hello from C++!

std::cout is explained in the next slide. ./hello: the program hello of the current folder (.); Linux does not look for programs there by itself.

  • -std=c++17 selects the C++ standard; -o hello the name of the executable.
  • -Wall turns on most of the useful warnings of the compiler (an unused variable, for example: Example 1): always use it, and read them.
  • -O2 (optimization) will be important for the simulations, not today.

If ./hello prints the line, the environment is ready.

Printing in C++: std::cout

In C++ you print with the stream std::cout (header <iostream>), not printf:

#include <iostream>
#include <string>
int main()
{
  int n = 3;  double t = 0.5;
  std::string name = "pvector";
  std::cout << "n = " << n << ", t = " << t << "\n";
  std::cout << name << std::endl;
}

Output:

n = 3, t = 0.5
pvector
  • << (the shift operator of C, redefined for the output: an example of the operator overloading of Part 2) sends the pieces one after the other; the compiler knows the type of each, so no %d or %f. std::endl ends the line.
  • std::string (header <string>) is the string type of C++: a class (a type defined by a library, Part 2), assigned with = and printed with <<.
  • std:: says that cout, endl and string belong to the C++ standard library; why the prefix is needed (namespaces) we will see in a later lab.
  • printf still works (header <cstdio>), but cannot print C++ objects.

Part 2
Introduction to C++

Object oriented (OO) paradigm

Key concepts

Encapsulation

  • Separate implementation from interface.
  • Attributes (data members) and methods (functions) can be public (→ interface) or private (→ implementation, hidden to user).
  • No need to know/remember internals of all objects.

Methods/functions act on objects and return objects (as an operator does) ⇓

Polymorphism — functions/methods can behave differently depending on the objects they act on: function overloading, operator overloading (e.g. + - / *), pointers.

Example: \(a + b\) (scalars) or \(\vec a + \vec b\) (vectors): operator “+” overloading.

Inheritance — in OO paradigm we have custom types called classes and we can enrich a class with more attributes and methods from base one.

Example: custom types

From a non OO language (C) to an OO language (C++)

  • C language can be thought as a subset of C++.
  • In C you have objects (variables) of several different types such as char, int, float, double and operators that act on them (such as ++, --, +, *, etc.).
  • C++ introduces custom types (similar to struct) called “classes” and it allows operators and methods to act on them.

Note

In C you had struct which could embed attributes but not methods. Hence classes can be also thought as a generalization of “struct”.

Instantiation

int, double, float ← fundamental types of C language.

If one writes

double d;   // double: type/class    d: instance of class "double", i.e. an OBJECT

Assume you have a custom type, i.e. a class, called vector which represents a vector. You can instantiate it as follows:

vector v;   // v is an instance of this class, i.e. an object

If we have

vector a, b, c;

and we “overloaded” the operators = and +, we can write

c = a + b;

The goal of the first labs

Similarly we can overload other operators such as ==, ++, --, *, /, *=, etc.

And this is precisely the goal of the first part of these hands-on classes, i.e.

learn how to write your own class to handle vectors and matrices for computations.

Example 0: ex0_basic_c_types.c

examples/N0_from_c_to_cpp/ex0_basic_c_types.c

#include<stdio.h>
int main(void)
{
  /* basic types */
  int a, b, c, i;
  double d, e, f;
  /* double is a class */
  /* d, e and f are instances of the class double */ 
  a=1;
  b=2;
  c = a + 2;

  d = 1.2;
  e = 2.3;
  f = d + e;
  printf("a=%d b=%d c=%d\n", a, b, c);
  printf("d=%f e=%f f=%f\n", d, e, f);

  /* vectors */

  double va[3]={1.2, 1.3, -1.0};
  double vb[3]={0.2, 0.1, 0.4}; 
  double vc[3];
  // vc = va + vb  
  for (i = 0; i < 3; i++)
    {
      vc[i] = va[i] + vb[i];
    }
  printf("va=(");
  for (i = 0; i < 3; i++)
    {
      printf("%f", va[i]);
      if (i < 2)
        printf(",");
    } 
  printf("\n");
  printf("vb=(");
  for (i = 0; i < 3; i++)
    {
      printf("%f", vb[i]);
      if (i < 2)
        printf(",");
    } 

  printf("\n");
  printf("vc=va+vb=(");
  for (i = 0; i < 3; i++)
    {
      printf("%f", vc[i]);
      if (i < 2)
        printf(",");
    } 
  printf(")\n");
}

Example 0: compile and run

gcc -Wall -o ex0 ex0_basic_c_types.c && ./ex0
a=1 b=2 c=3
d=1.200000 e=2.300000 f=3.500000
va=(1.200000,1.300000,-1.000000
vb=(0.200000,0.100000,0.400000
vc=va+vb=(1.400000,1.400000,-0.600000)
  • For double we write f = d + e; for vectors vc = va + vb is not possible: every component has to be spelled out with a loop.
  • Printing a vector takes another loop, repeated three times (for va, vb and vc).
  • gcc compiles C, g++ C++: these two examples are C files.
  • The comment “double is a class” is an analogy: in C++ double is a fundamental type, but it behaves like a class here (objects and operators).

Example 1: ex1_vec_with_funcs.c

examples/N0_from_c_to_cpp/ex1_vec_with_funcs.c

#include<stdio.h>
void sumvec(double va[3], double vb[3], double vc[3]);

void printvec(double vec[3], char* txt);

int main(void)
{
  /* basi types */
  int i;
  double va[3]={1.2, 1.3, -1.0}, vb[3]={0.2, 0.1, 0.4}, vc[3];
  
  /* ... or better with functions */
  sumvec(va, vb, vc);
  printvec(va, "va");
  printvec(vb, "vb");
  printvec(vc, "vc=va+vb");
}


void printvec(double vec[3], char* txt)
{
  int i;
  printf("%s=(", txt);
  for (i = 0; i < 3; i++)
    {
      printf("%f", vec[i]);
      if (i < 2)
        printf(",");
    } 
  printf(")\n");
 
}

void sumvec(double va[3], double vb[3], double vc[3])
{
  int i;
  for (i=0; i < 3; i++)
    {
      vc[i] = va[i] + vb[i];
    }
}

Example 1: compile and run

gcc -Wall -o ex1 ex1_vec_with_funcs.c && ./ex1
va=(1.200000,1.300000,-1.000000)
vb=(0.200000,0.100000,0.400000)
vc=va+vb=(1.400000,1.400000,-0.600000)
  • Less repetition, but sumvec(va, vb, vc) is not vc = va + vb.
  • Reminder of C: an array passed to a function is not copied, the function works on the caller’s array; this is why sumvec can fill vc. char* txt is a C string (compiled with g++ it warns: in C++ "va" is a const char*).
  • Data (double[3]) and functions acting on them live separately: nothing tells the compiler that sumvec belongs to “vectors”.
  • What we want: a type vector that carries its data and its operations.
  • -Wall also warns that i in main is never used: warning: unused variable 'i'.

Example 2: ex2_a_cpp_class.hpp

examples/N0_from_c_to_cpp/ex2_a_cpp_class.hpp

#include<iostream>
#include<string>
class myclass 
{
//private:
  int a;
  std::string txt; // private member (an int)
  void show_private(void) const // private method/function 
    {
      std::cout << "no\n!" << std::endl;
    }
public:

void show(void) const
  {
    show_private();  
    std::cout << txt << " " << a << " " << std::endl;
  }

myclass() // constructor
  {
    // constructor (called when creating object)
    txt = "Hallo World";
    std::cout << "Object created" << std::endl;
    a=2;
  }

~myclass() // destructor
  {
    std::cout << "Object destroyed" << std::endl;
  }
};
  • Data (a, txt) and functions, the methods, which use data and other methods directly (show() calls show_private()); const: show does not change the object (N1).
  • private is the default in a class (not in a struct): main can call C.show(), not C.show_private() nor read C.a (error: 'int myclass::a' is private ...).
  • Constructor myclass(): same name as the class, no return type, runs when the object is created; destructor ~myclass(): runs when it is destroyed.

Example 2: ex2_a_cpp_class.cpp

examples/N0_from_c_to_cpp/ex2_a_cpp_class.cpp

#include "./ex2_a_cpp_class.hpp"
int main(void)
{
  myclass C; // a new type!


  C.show();
}
g++ -std=c++17 -Wall -o ex2 ex2_a_cpp_class.cpp
./ex2
Object created
no
!
Hallo World 2 
Object destroyed
  • myclass C;: the class name is already a type (in C you would write struct). A method is called with a dot, as for the fields of a C struct: C.show().
  • Constructor and destructor are never called explicitly: the compiler calls them when C is born and when it goes out of scope (at the end of main).
  • "no\n!" contains a newline: that is why no and ! are on two lines.
  • The class is in a header (.hpp), main in a .cpp that #includes it: we compile only the .cpp. #include "..." is for our own files (looked for first in the folder of the file), #include <...> for the standard headers.

C struct vs C++ class

C

struct vec {
  double v[3];
};

struct vec sumvec(struct vec a,
                  struct vec b);

struct vec a, b, c;
a.v[0] = 1.0;     /* direct access */
c = sumvec(a, b);

C++

class pvector {
  double v[3];     // private
public:
  void set(int i, double x);
  // ... and the operator +,
  //     which we write in Part 3
};

pvector a, b, c;
a.set(0, 1.0);    // through the interface
c = a + b;        // operator overloading
  • Both define a type, and a, b, c are instances of it (objects).
  • The class also contains the operations, and decides what the user can touch.
  • a.v[0] = 1.0 on a pvector does not compile: v is private. If tomorrow we change how the data are stored, the code that uses pvector does not change.
  • Overloading: the same name (of a function or operator) with arguments of different types; the compiler picks the version from the types. a + b on two pvectors is an overloaded +.

Example N1: myvec.hpp

#include<iostream>
class myvec
{
  // private members
  int v[3]; // array of integers
  int len; // allocated int
public:
  // constructor
  myvec()
    {
      len=3;
    }
  // destructor
  ~myvec()
    {
      // do nothing
    }

  // assign value to (i+1)-th element of the array
  void set(int i, int val)
    {
      v[i] = val;
    }

  // get (i+1)-th element of array
  int get(int i) const // pass implicitly argument (*this) with const modifier, 
                       //it means that this method does not change the state of the object
    {
      return v[i];
    }
  // get actual length of array
  int length(void) const
    {
      return len;
    }
 };

Example N1: myvec.cpp

examples/N1_a_first_vec_class/myvec.cpp

#include "./myvec.hpp"
int main(int argc, char** argv)
{
  // class instantiation
  myvec v;

  // initialize array elements
  for (int i=0; i < v.length(); i++)
    {
      v.set(i, 0);
    }
  // print out value of third element
  std::cout << "v[2]=" << v.get(2) << std::endl;

  // set 2-th element  
  v.set(1, 1);

  // set 3-th element  
  v.set(2, -2);

  std::cout << "v[1]=" << v.get(1) << " v[2]=" << v.get(2) << std::endl;
}
g++ -std=c++17 -Wall \
    -o myvec myvec.cpp
./myvec
v[2]=0
v[1]=1 v[2]=-2
  • const method (get, length): it does not change the object. Every method receives implicitly the object it is called on (the *this of the comment, see Part 3); const makes it read-only. argc, argv: command line, as in C.

Example N2: vecder.hpp

examples/N2_a_derived_vec_class/vecder.hpp

#include<iostream>
#include"../N1_a_first_vec_class/myvec.hpp"
class vecder: public myvec
{
public: 
  // we add just a show public method to this derived class
  void show(void) const
    {
      std::cout << "(";
      for (int i = 0; i < length(); ++i) { 
        // note that length() is a public method of base class 
          {
            std::cout << get(i); // get() is another public method of public class
          }
        if (i < length()-1)
          std::cout << ",";
        else
          std::cout << ")" << std::endl;
      }
    }
};
  • Inheritance — class vecder: public myvec: vecder inherits everything from myvec, and its public interface stays public; it adds the method show().
  • Writing v[i] in show() would not compile: v is private in myvec, not even a derived class sees it. The way out (protected) is for next lab.

Example N2: vecder.cpp

examples/N2_a_derived_vec_class/vecder.cpp

#include "./vecder.hpp"
int main(int argc, char** argv)
{
  vecder vd;
  for (int i=0; i < vd.length(); i++)
    {
      vd.set(i, i); // initialize elements of vector vd
    }
  vd.show(); // show content of vector vd
}
g++ -std=c++17 -Wall -o vecder vecder.cpp && ./vecder
(0,1,2)
  • set() and length() come from the base class myvec, show() from vecder.
  • vecder has no constructor: the one generated by the compiler calls myvec() first, which sets len=3. The base part is always built first.

Part 3
The class pvector

The task

Write from scratch an efficient class for vectors, with methods and overloaded operators: pvector.hpp (the class) and pvector.cpp (a main to test it), in the folder project/ of project_classes.zip (course page).

The header begins with a TODO list: [*] = to do today, [ ] = next labs.

// TODO LIST:
// * = TODO, X = done, E = exercise
// [*] standard constructor/destructor
// [*] constructor 
// [*] show()
// [*] overloading of = assignment 
// [*] overloading of + operator for addition of vectors
// [*] overloading of - operator for substraction of vectors
// [*] sum() method to sum two vectors
// [*] get( ) (get i-th element)
// [*] set( , ) (set i-th element)
// [ ] constructor with overloading->initializer_list
// [ ] overloading of += and -= operators with vectors
// ...

The skeleton already contains all the signatures, and operator= is implemented: your job is to fill the empty bodies.

#include and the header guard

pvector.hpp begins and ends like this:

#ifndef _PVECTOR_      // if the macro _PVECTOR_ is not defined yet...
#define _PVECTOR_      // ...define it, and read the rest of the file
...
#include<iostream> // input/output
#include<cmath>
#include<string>
...
class pvector
{ ... };
#endif                 // end of the #ifndef
  • #include is a preprocessor directive: before compiling, the content of the header is copied in its place. <iostream> gives std::cout, <string> gives std::string, <cmath> the math functions (sqrt, … for norm() later).
  • #ifndef, #define, #endif are also preprocessor directives: together they form the header guard, next slide.

Why the header guard

  • A class may be defined only once in each .cpp. If a .cpp includes the same header twice, also through another header, the class is defined twice. Example N2 without guards: vecder.hpp already includes myvec.hpp, so a main that includes both stops with error: redefinition of 'class myvec'.
  • The header guard prevents it: the first time _PVECTOR_ is not defined, so the file is read and the macro defined; the second time everything between #ifndef and #endif is skipped. Each header needs its own name.

The skeleton: NT and ntype

1#define NT 3                // STRATEGIES TO MAKE CLASS GENERIC
2//constexpr int NT=3;

//typedef float ntype;      // as in C
3using ntype=double;

class pvector
{
  ntype v[NT];              // private member: the NT components
public:
  ...
};
1
A macro: the preprocessor replaces the text NT with 3 before compiling. No type, no scope.
2
constexpr: a typed constant that the compiler can compute at compile time — so it can size an array. In C++ it is the preferred way.
3
using defines an alias of a type, exactly like typedef. Change this single line to float and the whole class works in single precision.
  • Dimension and type are fixed in one place each: with NT equal to 2 you get vectors in the plane, with no other change in the class.
  • In a few labs templates will make both parameters of the class itself: pvector<double,3>, pvector<float,2>.

Constructors and destructor

pvector()  { ... }   // void constructor: pvector P;
~pvector() { ... }   // destructor
  • The void constructor (also called default constructor) is the only constructor you write today: it runs every time you write pvector P;, and it must leave the vector in a known state. Without it the components would contain garbage, like a local double v[3] in C.

  • Initialize the NT components with values of your choice — not necessarily zero: all 0, v[i] = i, … Then change them with set().

    pvector()                 // void constructor
      {
        for (int i=0; i < NT; i++)
          v[i] = ...;         // a value of your choice
      }
  • The destructor has nothing to do here: v is a fixed-size array, it disappears with the object. Leave it empty (we will need it when we allocate memory dynamically).

show(): default arguments and const

// Note this method does not change data of object hence it is declared "const"
void show(std::string s="") const    // default argument
  {
    ...   // print s, then (v[0],v[1],v[2])
  }
V.show("V=");   // V=(1,2,3)    for a vector V with components 1, 2, 3
V.show();       // (1,2,3)      s takes the default value ""
  • Default argument: if the caller omits it, s is "".

  • const method: it only reads the object. The compiler checks it (assigning v[i] inside show() is an error), and only const methods can be called on a const object:

    const pvector PC;
    PC.show("PC=");   // compiles only because show() is const
  • Use std::cout with a loop over the NT components, and a comma between them.

Arguments passed by reference

void twice_val(int x)  { x = 2*x; }   // by value: x is a COPY, the caller sees nothing
void twice_ref(int &x) { x = 2*x; }   // by reference: x is an alias of the argument

int a = 3;
twice_val(a);   // a is still 3
twice_ref(a);   // a is now 6     (no & in the call)
  • In a declaration int &x means reference; in an expression &a is still, as in C, the address of a.

In the class all the vector arguments are passed as const pvector&:

pvector sum(const pvector& V2) const    // the first const protects V2,
                                        // the last one the object (const method)
  • &: no copy of the object (for a vector of 3 doubles it is cheap, but think of a configuration of \(10^5\) particles…).
  • const: the method promises not to modify V2; the compiler enforces it.
  • Inside a method of pvector you can read the private members of another pvector: V2.v[i] is legal. Access is per class, not per object.

operator= and this

The one method already implemented in the skeleton:

1pvector& operator=(const pvector& V2)   // A = B  is  A.operator=(B)
  {
    for (int i=0; i < NT; i++)
      v[i] = V2.v[i];                   // v[i] is A's, V2.v[i] is B's
2    return (*this);
  }
1
A = B is just a nicer way to write the method call A.operator=(B): A is the object the method acts on, B the argument.
2
this is a pointer: the address of the object the method has been called on (A). As in C, * applied to an address gives the object, so *this is A itself. v[i] is a shorthand for (*this).v[i].
  • It returns A itself by reference (pvector&): A = B = C; works as for double — it is A = (B = C) — and no copy of the vector is made.
  • It is not const: it changes the object.
  • Even without writing it, A = B works: the compiler generates a copy member by member, as for C structs. We write it to see how an operator is defined.

sum(), operator+ and operator-

pvector sum(const pvector &V2) const      // C = A.sum(B)
pvector operator+(const pvector& V2) const // C = A + B   is   C = A.operator+(B)
pvector operator-(const pvector& V2) const // C = A - B

Recipe, the same for the three:

  1. create a local vector VT;
  2. fill it component by component, using v[i] (of A) and V2.v[i] (of B);
  3. return VT;
  • They are const: A + B must change neither A nor B.
  • They return by value (pvector, not pvector&): VT is a local object, destroyed when the method ends. A reference to it would point to a dead object (g++ warns: reference to local variable returned).
  • sum() and operator+ do the same thing: the operator is just nicer to read. Once one is written, the other can call it.

get() and set()

ntype get(int i) const          // returns the i-th component
ntype set(int i, ntype val)     // sets the i-th component to val
  • get is const, set is not: why?
  • set in the skeleton returns an ntype: return val (or change it to void, as in myvec).
  • Components are numbered from 0 to NT-1, as in C. Nothing checks the index: get(3) reads outside the array (undefined behaviour).

Read the warnings

The empty bodies of the skeleton compile: for a method that should return a value and has no return the compiler only warns no return statement in function returning non-void [-Wreturn-type]. But when the program calls it, it crashes (Illegal instruction) or worse (undefined behaviour): fill a method before testing it.

The main: pvector.cpp

The skeleton only declares three vectors. Test every method, for example:

#include "./pvector.hpp"
int main(void)
{
  pvector A, B, C;     // the void constructor runs for the three vectors
  C.show("C=");        // the values set by pvector()

  A.set(0, 1.0);  A.set(1, 2.0);   A.set(2, 3.0);
  B.set(0, 0.5);  B.set(1, -1.0);  B.set(2, 2.0);
  A.show("A=");
  B.show("B=");
  std::cout << "A(1)=" << A.get(1) << "\n";

  C = A + B;      C.show("A+B=");
  C = A - B;      C.show("A-B=");
  C = A.sum(B);   C.show("A.sum(B)=");

  pvector E, F;
  E = F = A;      // chained assignment: operator= returns pvector&
  E.show("E=");

  const pvector PC;
  PC.show("PC=");   // ok only because show() is const
  return 0;
}

Compile, run, compare

cd ~/csm/project
g++ -std=c++17 -Wall -o pvector pvector.cpp
./pvector

Expected output, if pvector() sets all components to 0 (otherwise C= and PC= show the values you chose):

C=(0,0,0)
A=(1,2,3)
B=(0.5,-1,2)
A(1)=2
A+B=(1.5,1,5)
A-B=(0.5,3,1)
A.sum(B)=(1.5,1,5)
E=(1,2,3)
PC=(0,0,0)
  • No warnings with -Wall: if there are, read them — they are usually a bug.
  • Change ntype to float: everything must still compile and run. With NT 2 the class still works, but this main does not: set(2, ...) writes outside the array, and nothing checks it.

Classic pitfalls

  • ; after the class: class pvector { ... };. Without it: error: expected ';' after class definition.
  • pvector P(); is not an object: it declares a function P returning a pvector. Write pvector P;.
  • Returning a reference to a local object in operator+: it compiles with a warning (reference to local variable 'VT' returned), then with g++ it usually crashes (Segmentation fault): undefined behaviour.
  • Missing const on show() or get(): then they cannot be called on a const pvector, nor on an argument const pvector& V2: passing 'const pvector' as 'this' argument discards qualifiers.
  • Missing return in a non-void method: only a warning, then a crash (see get() and set()).
  • Header guard missing, or with the name already used by another header: error: redefinition of 'class ...' as soon as the header is included twice.

Checklist for today

Next lab: protected members, more overloading (+=, -=, scalar product, norm(), vector times scalar), function overloading. Later: a second constructor, with initializer_list, to write pvector V = {1,2,3};.