Outils pour utilisateurs

Outils du site


developpement:cpp:modern

Table des matières

Modern C++

Le C++ moderne existe depuis C++11, c'est-à-dire depuis 2011.

Certains IDE d'intérêt pour C++:

  • CodeLite (gratuit et multi-plateforme)
  • CLion (payant et multi-plateforme)
  • Windows: Visual Studio, Visual Studio Code
  • macOS: XCode

Ressources

Setup Linux

sudo apt install build-essential

Compile by Command-Line Interface

Having a main.cpp file in current directory:

$ g++ -Wall -std=c++14 main.cpp -o main
  • -Wall: All warnings
  • -std: standard to use
  • -o output name of executable

Structure d'un programme C++

Preprocessor Directives

#include <iostream>
#include "myfile.h"
 
#if
#elif
#else
#endif
 
#ifdef
#ifndef
#define
 
#line
#error
#pragma

Comments

// Single line comment
 
/*
  Multi-line comment
*/

main() function

Deux versions:

int main() {
  // code
  
  return 0;
}
int main(int argc, char *argv[]) {
  // code
  
  return 0;
}

argc est le argument count et argv sont les paramètres passés par le système d'exploitation.

Namespaces

  • std est le nom pour namespace standard
  • Scope resolution operator: ::

Utiliser un namespace:

std::cout << "Some text";
using namespace std;

// ... code

cout << "Some text";

Qualified using namespace variant:

using std::cout;  // Utiliser seulement ce qui est nécessaire

// ... code

cout << "Some text";

Variables

VariableType variableName;

Règles:

  • On doit déclarer la variable avant de l'utiliser
  • Peut contenir des lettres, chiffres, underscores
  • Doit commencer par une lettre ou un underscore
    • Ne peut pas commencer par un chiffre
  • On ne peut utiliser un keyword C++
  • On ne peut pas redéclarer un nom de variable déjà déclaré dans le scope
  • Les noms de variables sont sensibles à la casse
int age; // uninitialized
 
int age = 21; // C-like initialization
int age = (21); // Constructor initialization
int age {21}; // C++11 list initialization syntax

Global variables

#include <iostream>
 
int age {18}; // Global
 
int main() {
  int age {16}; // local, shadowing global one 
}

Primitive Data Types

Liste non-exhautive (voir Introduction to fundamental data types) :

  • short
  • int (signed / unsigned)
  • long (signed / unsigned)
  • char
  • bool

Initialization:

char middle_name {'J'};  // single quote for char

long people_on_earth = 7'600'000'000; // sera un overflow
long long people_on_earth {7'600'000'000}; // ' pour séparer les miliers, depuis C++14

float car_payment {401.23};

long double large_amount {2.7e120};

bool game_over {false};
cout << "Value of game_over is " << game_over << endl; // Value of game_over is 0

sizeof

sizeof(int);

int age {31};
sizeof(age);

sizeof(long long);

Constantes de minimums et maximums:

  • CHAR_MAX / CHAR_MIN
  • INT_MAX / IN_MIN
  • LONG_MAX
  • LLONG_MAX
  • etc

Constants

  • Constantes litérales
  • Declared constants
  • Constant expressions
  • Enumerated constants
  • Defined constants

Literal constants:

12 - integer
12U - unsigned int
12L - long
12LL - long long

12.1 - double
12.1F - float
12.1L - long double

Caracter literal constants : \n, \r, etc

Declared constants:

const int age {31};  // Declared constant
#define pi 3.1415926   // defined constant, ne pas utiliser

Arrays

Multi-dimentional arrays

int movie_rating [3][4]
{
  {0,3,4,5},
  {0,2,5,5},
  {0,3,1,5}
};

Tableau statique

  • Taille fixe d'éléments d'un même type
  • La taille est fixé au moment de la compilation et ne peut pas grandir (sauf C99)
  • Les éléments sont stockés de façon séquentielle et en continu
  • Chaque élément a son propre emplacement
    • Accessible via sa position
    • Ont un index de base 0

Points importants:

  • Les arrays n'est pas un pointeur
    • <type>[size], e.g. int arr[] est int[5]
  • Mais accessible via la syntaxe de pointeurs
  • Un array peut être assigné à un tableau
    • Le pointeur détient l'adresse du premier élément
    • Les autres éléments peuvent être accédés via l'index ou en incrémentant le pointeur
int arr1[5];  // tableau non initialisé
int arr2[5]{}; // éléments initialisés avec 0
int arr3[5]{ 1, 2, 3, 4, 5 }; // initialisé avec les valeurs spécifiés
int *p = arr3;
*(p + 2) = 800; // Le 3e élément (index 2) aura la valeur 800

Passer un tableau à une fonction

void Print(int *ptr, int size) {
    for (int i = 0; i < size; ++i) {
        std::cout << ptr[i] << ' ';
        //std::cout << *(ptr + i) << ' '; // moins lisible, donc la façon précédente est préférable
    }
}
 
Print(arr3, sizeof(arr3) / sizeof(int));

En tant que référence:

template<typename T, int size>
void Print(T(&ref)[size]) {
    for (int i = 0; i < size; ++i) {
        std::cout << ref[i] << ' ';
    }
}
 
int(&ref)[5] = arr3;
Print(arr);

Vectors

#include <vector>

vector <char> vowels;
vector <int> test_scores;

Characters and Strings

  • C-Style characters
  • C++

C-Style Characters

C-Style Strings

  1. Sequence of characters
    1. Contiguous in memory
    2. Implemented as an array of characters
    3. Terminated by a null character
      1. null → character with a value of zero
    4. Referred to as zero or null terminated strings

String Literals

  1. Sequence of characters in double quotes
  2. Constant
  3. Terminated with null character

Fonctions

#include <cctype>

Test Functions
isalpha(c) True if c is a letter
isalnum(c) True if c is a letter or a digit
isdigit(c) True if c is a digit
islower(c) True if c is a lowercase letter
isprint(c) True if c is a printable character
ispunct(c) True if c is a punctuation character
isupper(c) True if c is a an uppercase letter
isspace(c) True if c is a whitespace
Conversions
tolower(c)
toupper(c)
char my_name[] = {"My name"};
char my_name[8];
 
my_name = "Frank";   // Error
strcpy(my_name, "Frank");  // OK

Exemples de fonctions C-Style

#include <cstring>
 
char str[80];
 
strcpy(str, "Hello ");
strcat(str, "there");  // Concatenate
cout << strlen(str);  //11
strcmp(str, "Another"); // > 0

cstdlib

#include <cstdlib>

Inclus des fonctions pour convertir des chaines C-Style à integer, float, long, etc.

C++ Strings

std::string is a class in the STL

  • #include <string>
  • std namespace
  • contiguous in memory
  • dynamic size
  • Work with input and output streams
  • Lots of useful member functions
  • Our familiar operators can be used
  • Can be easily converted to C-Style Strings if needed
  • Safer
#include <string>
using namespace std;
 
string s1;  // Empty
string s2 {"My name"};
string s3 {s2};
string s4 {"My name", 3};  // "My "
string s5 {s3, 0, 2};      // "My"
string s6 (3, '-');        // "---"
 
// Concatenation
 
string part1 {"C++"};
string part2 {"is a powerful"};
string sequence = part1 + " " + part2 + " language";
 
sequence = "C++" + " is powerful";  // Illegal: two C-Style strings can't concatenate this way

Regex

Définir un regex:

std::regex regex(R"(\d{2}));

Functions

Using builtin functions:

#include <iostream>
#include <cmath>
 
using namespace std;
 
int main() {
    double num {};
 
    cout << "Enter number (double): ";
    cin >> num;
 
    cout << "The sqrt of " << num << " is: " << sqrt(num) << endl;
 
    return 0;
}
#include <iostream>
#include <cmath>
#include <ctime>
 
using namespace std;
 
int main() {
    int random_number {};
    size_t count {10};
    int min {1}; // lower bound (inclusive)
    int max {6}; // upper bound (inclusive)
 
    cout << "RAND_MAX on my system is: " << RAND_MAX << endl;
    srand(time(nullptr));
 
    for (size_t i {1}; i <= count; ++i) {
        random_number = rand() % max + min;
        cout << random_number << endl;
    }
 
    return 0;
}

Function definition

  • Name
    • Same rules as for variables
    • Should be meaningful
    • Usually a verb or a verb phrase
  • Parameter list
  • Return type
  • Body
 
int function_name () {
  statements(s);
  return 0;
}
 
void function_name (int a, std::string b) {
  statements(s);
  return; // optional
}

Les fonctions doivent être définies avant d'être appelées.

Function Prototypes

Define functions before calling them:

  • OK for small programs
  • Not a practical solution for larger programs

Use function prototypes:

  • Tells the compiler what it needs to know without a full function definition
  • Also called forward declarations
  • Placed ate the beginning of the program
  • Also used in our own header files (.h)
int function_name(); // prototype, no parameters
int function_name(int); // prototype
int function_name(int a); // we can specify parameter name or not
 
int function_name(int a) {
  statements(a);
  return 0;
}

Function parameters

Dans la définition d'une fonction, on parle de paramètres, mais quand on appelle la fonction on parle d'arguments.

Quand on passe des données à une fonction, elles sont passées par valeur par défaut.

Formal vs actual parameters:

  • Formal parameters: the paramters defined in the function header
  • Actual parameters: the paramter used in the function call, the arguments

Default Argument Values

Dans le prototype:

double calc_cost(double base_cost, double tax_rate = 0.06);
void print(int = 100);

On peut le faire aussi dans l'implémentation de la fonction, mais pas aux deux endroits. Il est préférable de le faire sur le prototype.

Les valeurs par défaut doivent être placés à la fin de la liste des paramètres.

double calc_cost(double base_cost = 100.0, double tax_rate); // ne fonctionne pas

Overloading Functions

int add_numbers(int, int);
double add_numbers(double, double);
 
int main() {
    cout << add_numbers(10, 20) << endl;
    cout << add_numbers(10.0, 20.0) << endl;
    return 0;
}

Passing Arrays to Functions

Les tableaux sont passés par référence, donc le changement de valeur dans la fonction a un effet sur les valeurs du tableau externe.

#include <iostream>
#include <cmath>
 
using namespace std;
 
void zero_array(int numbers [], size_t size);
void print_array(int numbers [], size_t size);
 
int main() {
    int my_numbers[] = { 1,2,3,4,5 };
    zero_array(my_numbers, 5);
    print_array(my_numbers, 5);
 
    return 0;
}
 
void zero_array(int numbers [], size_t size) {
    for (size_t i {0}; i < size; ++i) {
        numbers[i] = 0;
    }
}
 
void print_array(int numbers [], size_t size) {
    for (size_t i {0}; i < size; ++i) {
        cout << numbers[i] << endl;
    }
}

Pour nous aider, selon les cas, on peut définir la fonction avec un paramètre const:

void print_array(const int numbers [], size_t size) {
  // ...
  numbers[0] = 0; // Mènera à une erreur du compilateur
}

Pass by reference

void pass_by_ref1(int &num);
 
 
void pass_by_ref1(int &num) {
    num = 1000;
}
 
 
int main() {
    int num {10};
 
    cout << "num before calling pass_by_ref1: " << num << endl; // 10
    pass_by_ref1(num);
    cout << "num after calling pass_by_ref1: " << num << endl; // 1000
 
    return 0;
}

Scope rules

  • C++ uses scope rules to determine where an identifier can be used.
  • C++ uses static or lexical scoping
  • Local or Block scope
  • Global scope

Local or Block scope:

  • Identifiers declared in a block { }
  • Fuynction parameters haave block scope
  • Only visibke within the block where declared
  • Function local var are only active while the fn is executing
  • Local varibales are not preserved between function calls
  • With nested blocks inner blocks can see but outer blocks cannot see in.

Static local variables:

  • Declared with static qualifier
    • static int value {10};
  • Value is preserved between function calls
  • Only initialized the first time the function is called

Global scope:

  • Identifier declared outside any function or class
  • Visible to all parts of the program after the global identifier has been devlared
  • Global constant are OK
  • Best practice: don't use global variables
using namespace std;
 
void local_example();
void global_example();
void static_local_example();
 
int num {300};    // Global variable - declared outside any class or function
 
int main() {
    int num {100};  // Local to main
 
    {   // creates a new level of scope
        int num {200};  // local to this inner block
    }
    return 0;
}

Inline functions

  • Function calls have a certain amount of overhead
  • We saw what happens on the call stack
  • Sometimes we have simple functions
  • We can suggest to the compiler to compile them inline
    • Avoid function call overhead
    • generate inline assembly code
    • faster
    • could cause code bloat
  • Compilers optimizations are very sophisticated
    • Will likely inline even witout any explicit suggestion
inline int add_numbers(int a, int b) { // definition
  return a + b;
}
 
int main() {
  int result {0};
  result = add_numbers(100, 200); // call
  return 0;
}

Recursive Functions

A recursive function is a function that calls itself, either directly or indirectly through another function.

#include <iostream>
 
using namespace std;
 
unsigned long long factorial(unsigned long long);
 
unsigned long long factorial(unsigned long long n) {
    if (n == 0) {
        return 1;              // base case
    }
 
    return n * factorial(n-1); // recursive case
}
 
int main() {
    cout << factorial(3) << endl;       // 6
    return 0;
}
#include <iostream>
 
using namespace std;
 
unsigned long long fibonacci(unsigned long long n);
unsigned long long fibonacci(unsigned long long n) {
    if (n <= 1) {
        return n; // base cases
    }
 
    return fibonacci(n-1) + fibonacci(n-2); // recursion
}
 
int main() {
    cout << fibonacci(5) << endl;   // 5
    return 0;
}

Function Pointers

float Add(float x, float y)
{
  return x + y;
}
 
float Substract(float x, float y)
{
  return x - y;
}
 
int main()
{
  float(*FnPtr)(float, float);  // Declaration of function pointer FnPtr
  FnPtr = Add;  // or '&Add', both works
  float result = FnPtr(3.1f, 8.3f);
  std::cout << result << std::endl;
 
  // We can reassign FnPtr, provided that function referenced has same signature
  FnPtr = &Substract;  // Using amphersand here, more verbose, but again, same as without it
}

Another example:

const char* GetErrorMessage(int errorNo)
{
  switch (errorNo)
  {
    case 0:
      return "Error 0";
    case 1:
      return "Error 1";
    case 2:
      return "Error 2";
    default:
      return "Unknown error"  
  }
}
 
int main() 
{
  const char* (*Pfn)(int) = GetErrorMessage;
  std::cout << Pfn(1) << std::endl;
}

Function pointers as Arguments

using PFN = float(*)(float, float);
 
float Operation(float x, float y, PFN pfn)
{
    if (pfn == nullptr)
    {
        std::cout << "Invalid operation" << std::endl; 
        return 0;
    }
    float result = pfn(x, y);
 
    return result;
}
 
int main()
{
    Operation(1, 2, Add);
}

Instead of declaring a function pointer type, we can do inline in function, but declaring a type might be preferable:

float Operation(float x, float, y, float(*pfn)(float, float))
{
    // implementation
}

Two ways of declaring a function pointer type:

typedef float(*PFN)(float, float);
 
float Operation(float x, float y, PFN) { ... }

or

using PFN = typedef float(*)(float, float);
 
float Operation(float x, float y, PFN pfn) { ... }

Pointers and References

Qu'est-ce qu'un pointeur:

  • C'est une variable dont la valeur est une adresse mémoire

Qu'est-ce qui peut être à cette adresse ?

  • Une autre variable
  • une fonction

Pour utiliser la valeur dont le pointeur pointe, on doit connaitre son type.

Pourquoi utiliser des pointeurs ?

  • Opérations sur des tableaux plus efficacement
  • On peut allouer de la mémoire dynamiquement dans le heap (ou le free store)
    • Cette mémoire n'a pas de nom de variable
    • La seule façon d'y accéder est par un pointeur
  • Avec l'OO le polymorphisme fonctionne grâce aux pointeurs
  • On peut accéder à des adresses mémoire spécifiques.

Déclarer des pointeurs

variable_type *pointer_name; // syntaxe générale
 
int *int_ptr;
double* double_ptr;
char *char_ptr;
string *string_ptr;

Les deux possibilités de l'astérisque sur le type ou sur le nom sont acceptés. Généralement on le met sur le nom de variable/pointer.

Initialiser des pointeurs qui pointent nul part.

  • Toujours initialiser les pointeurs
  • Les pointeurs non initialisés contiennent des données invalidées (garbage data)
  • Initialiser un pointeur à zero ou sur nullptr (C++11) représente l'adresse zero.
    • Implique que le pointeur pointe nul part
int *int_ptr {};
int *int_ptr {nullptr};

Endianess

  • Little endian: Le dernier byte d'une adresse est stocké en premier (c'est le cas avec Windows)
  • Big endian: Le premier byte d'une adresse est stocké en premier

Illustration du Little Endian:

Ici on voit que l'adresse du pointeur ptr est 0x001FFCC0 et sa valeur est l'adresse de data, c'est-à-dire 0x001FFCCC. Mais dans la mémoire, si on inspecte la valeur à cet endroit, on verra CC FC 1F 00.

Accéder/Stocker à l'adresse du pointeur

Il faut utiliser l'opérateur adresse (address operator): &

int num{10};
cout << "Value of num is: " << num << endl; // 10
cout << "sizeof of num is: " << sizeof num << endl; // 4
cout << "Address of num is: " << &num << endl; // 0x7fffd0421194 (garbage)

sizeof d'un pointeur

  • Ne pas confondre la taille de la variable pointeur au type du pointeur
  • Tous les pointeurs dans un programme on la même taille
  • Ils peuvent pointer sur des types vraiment gros ou vraiment petits
int *p;
cout << "\nValue of p is: " << p << endl;   // 0x7f8ccaa45048 (garbage)
cout << "Address of p is: " << &p << endl;  // 0x7ffd39db3f70
cout << "sizeof of p is: " << sizeof p<< endl; // 8
 
p = nullptr;
cout << "\nValue of p is: " << p << endl; // 0

Stocker une adresse dans un pointeur

Le compilateur va s'assurer que l'adresse stockée dans un pointer est du bon type.

int score{10};
double high_temp{100.7};
 
int *score_ptr {nullptr};
 
score_ptr = &score;
cout << "Value of score is: " << score << endl;
cout << "Address of score is: " << &score << endl;
cout << "Value of score_ptr is: " << score_ptr << endl;
score_ptr = &high_temp;     // Compiler error

String

string s1 {"Some string"};
string *p1 {&s1};

Déréférencement d'un pointeur

Accéder à la valeur où le pointeur pointe → deferencing a pointer

Par exemple, si un pointeur nommé score_ptr a une adresse valide, on peut donc accéder à sa valeur à l'adresse contenue dans score_ptr en utilisant l'opérateur de déférencement: *.

int score {100};
int *score_ptr {&score};
 
cout << *score_ptr << endl; // 100
 
*score_ptr = 200;
cout << *score_ptr << endl;  // 200
cout << score << endl;       // 200

Dynamic Memory Allocation

Allocating storage from heap at runtime.

  • We don't often know how much storage we need until we need it
  • We van allocate storage for a variable at runtime
  • Recall C++ arrays
    • We had to explicitly provide the size and it was fixed
    • But vectors grow and shrink dynamically
  • We can use pointers to access newly allocated heap storage
int *int_ptr {nullptr};
int_ptr = new int;        // allocate the int on the heap
cout << int_ptr << endl;  // use it
delete int_ptr;           // release it

Utilisation de new []:

int *array_ptr {nullptr};
size_t size{0};
 
cout << "How big do you want the array? ";
cin >> size;
 
array_ptr = new int[size];
 
// ...
 
delete [] array_ptr;

Lien entre un tableau et un pointeur

int scores[] {100, 95, 89};
int *score_ptr {scores};

Pointer subscript notation

cout << score_ptr[0] << endl; // 100
cout << score_ptr[1] << endl; // 95
cout << score_ptr[2] << endl; // 89

Pointer offset notation

cout << *score_ptr << endl;       // donne 100, mais exemple addr est 0x73f610
cout << *(score_ptr + 1) << endl; // donne 95, addr est 0x73f614 (ajoute 4 car 4 est le size d'un int
cout << *(score_ptr + 2) << endl; // donne 89, addr est 0x73f618

Arithmétique des pointeurs

  • ++: incrémente le pointer pour pointer vers le prochain élément (du tableau)
    • int_ptr++
  • --: décrémente le pointer pour pointer vers l'élément précédent (du tableau)
    • int_ptr--
int scores[] {100, 95, 89, 68, -1}; // -1 est une valeur sentinelle
int *score_ptr {scores};
 
while (*score_ptr != -1) {
  cout << *score_ptr << endl;
  score_ptr++;
}
 
// Ou:
 
while (*score_ptr != -1) {
  cout << *score_ptr++ << endl;
}

Constant pointers

On ne peut pas changer la valeur (l'adresse) du pointeur constant.

int high_score {100};
int low_score {65};
int *const score_ptr {&high_score};
 
*score_ptr = 86;  // OK
score_ptr = &low_score; // Error

Passing pointers to a function

void double_data(int *int_ptr) {
   *int_ptr *= 2;	
}
 
int main() {
    int value {10};
    int *int_ptr {nullptr};
 
    cout << "Value: " << value << endl; // 10
    double_data(&value);
    cout << "Value: " << value << endl; // 20
 
    int_ptr = &value;
    double_data(int_ptr);
    cout << "Value: " << value << endl; // 40    
}
voi display(vector<string> *v) {
  (*v).at(0) = "Funny";
}

Returning a pointer from a Function

int *create_array(size_t size, int init_value = 0) {
   int *new_storage {nullptr};
   new_storage = new int[size];   
   for (size_t i{0}; i < size; ++i)
      *(new_storage + i) = init_value;
   return new_storage;
}

Pitfalls

  • Uninitialized pointers
  • Dangling pointers
  • Not checking if new failed to allocate memory
  • Leaking memory

Reference

Qu'est-ce qu'une référence:

  • An alias for a variable
  • Must be initialiazed to a variable when declared
  • Cannot be null
  • Once initialized, cannot be made to refer ro a different variable
  • Very useful as function parameters
  • Might be helpful to think of a reference as a constant pointer that is automatically deferenced
int num {100};
int &ref {num};
 
vector<string> stooges {"Larry", "Moe", "Curly"};
 
for (auto str: stooges)     
    str = "Funny";              // str is a COPY of the each vector element
 
for (auto str:stooges)        // No change
    cout << str << endl;
 
for (auto &str: stooges)  // str is a reference to each vector element
   str = "Funny";
 
for (auto const &str:stooges)   // notice we are using const
   cout << str << endl;            // now the vector elements have changed

L-values and R-values

L-value:

  • Values that have names and are addressable
  • Modifiable if they are not constants
int x {100}; // x is an l-value
string name; // name is an l-value
 
100 = x; // 100 is not a l-value

R-Values can be assigned to l-values explicitly

int x {100}; // 100 is a r-value
int y = x + 200 // (x+200) is a r-value

Quand utiliser un pointeur vs une référence

  • Pass-by-value
    • When the function does not momdify the actual parameter, and
    • the parameter is small and efficient to copy like simple types (int, char, double, etc)
  • Pass-by-reference using a pointer
    • When the function does modify the actual parameter, and
    • the parameter is expensive to copy and
    • It's OK to the pointer to contain nullptr value
  • Pass-by-reference using a pointer to const
    • When the function does not modify the actual parameter, and
    • the parameter is expensive to copy and
    • It's OK to the pointer to contain nullptr value
  • Pass-by-reference using a const pointer to const
    • When the function does not modify the actual parameter, and
    • the parameter is expensive to copy and
    • It's OK to the pointer to contain nullptr value
    • We don't want to modify the pointer itself
  • Pass-by-reference using a reference
    • When the function does modify the actual parameter, and
    • the parameter is expensive to copy and
    • The parameter will never be nullptr
  • Pass-by-reference using a const reference
    • When the function does not modify the actual parameter, and
    • the parameter is expensive to copy and
    • The parameter will never be nullptr

OOP - Classes et objets

Definition

class Account {
  std::string name;
  double balance;
 
  bool withdraw(double amount);
  bool deposit(double amount);
};

Création d'objets

Account account1;
Account account2;
 
Account accounts[] { account1, account2 };
 
std::vector<Account> accounts { account1 };
accounts.push_back(account2);

Access Class Members

If we have an object (dot operator)

Account account1;
 
account1.balance;
account1.deposit(100.0);

If we have a pointer to an object (member of a pointer operator)

  • Deference thhe pointer then use the dot operator
Account *account1 = new Account();
 
(*account1)->balance;
(*account1)->deposit(100.0);
  • Or use the member of pointer operator (arrow operator)
Account *account1 = new Account();
 
account1->balance;
account1->deposit(100.0);

Access modifiers

class Player {
private:
  std::string name;
  int health;
  int xp;
public:
  void talk(std::string text_to_say);
  bool is_dead();
};

Implementing Member methods

  • Very similar to how we implement functions
  • Member methods have access to member attributes
    • So you don't need to pass them as arguments!
  • Can be implemented inside the class declaration
    • Implicitly inline
  • Can be implemented outside the class declaration
    • Need to use Class_name::method_name
  • Can separate specification from implementation
    • .h file for class declaration
    • .cpp file for the class implementation

Account.h

class Account {
private:
    // attributes
    std::string name;
    double balance;
 
public:
    // methods
    // declared inline
    void set_balance(double bal) { balance = bal; }
    double get_balance() { return balance; }
 
    // methods will be declared outside the class declaration
    void set_name(std::string n);
    std::string get_name();
 
    bool deposit(double amount);
    bool withdraw(double amount);
};

Account.cpp

#include "Account.h"
 
void Account::set_name(std::string n) {
    name = n;
}
 
std::string Account::get_name() {
    return name;
}
 
bool Account::deposit(double amount) {
    // if verify amount
    balance += amount;
    return true;
}
 
bool Account::withdraw(double amount) {
    if (balance-amount >= 0) {
        balance -= amount;
        return true;
    } else {
        return false;
    }
}

main.cpp

// Section 13
// Implementing member methods 2
#include <iostream>
#include "Account.h"
 
using namespace std;
 
int main() {
    Account frank_account;
    frank_account.set_name("Frank's account");
    frank_account.set_balance(1000.0);
 
    if (frank_account.deposit(200.0)) {
        cout << "Deposit OK" << endl;
    }
    else {
        cout << "Deposit Not allowed" << endl;
    }
 
    return 0;
}

Include guard

#ifndef _ACCOUNT_H_
#define _ACCOUNT_H_
 
// Account class declaration
 
#endif

Ou

#pragma once

Constructeurs et Destructeurs

Constructors:

  • Special member method
  • Invoked during object creation
  • Useful for initialization
  • Same name as the class
  • No return type is specified
  • Can be overloaded

Destructors:

  • Special member method
  • Same name as the class proceeded with a tilde (~)
  • Invoked automatically when an object is destroyed
  • No return type and no parameters
  • Only one desctructor is allowed per class - cannot be overloaded
  • Useful to release memory and other ressources
class Player {
private:
   std::string name;
   int health;
   int xp;
public:
    void set_name(std::string name_val) { 
        name = name_val; 
    }
 
    // Overloaded Constructors
 
    Player() { 
        cout << "No args constructor called"<< endl;
    }
 
    Player(std::string name) { 
        cout << "String arg constructor called"<< endl;
    }
 
    Player(std::string name, int health, int xp) {
        cout << "Three args constructor called"<< endl; 
    }
 
    ~Player() {
        cout << "Destructor called for " << name << endl; 
    }
};

Creating objects

{
  Player slayer;
  Player frank {"Frank", 100, 4};
  Player hero {"Hero"};
  // use the objects
} // 4 destructors called
 
Player *enemy = new Player("Enemy", 100, 0);
delete enemy; // destructor called

If we don't provide constructor and/or destructor, C++ will automatically provide default constructor/destructor that are empty.

Default constructor

  • Does not expect any arguments
    • Also called the no-args constructor
  • If we write no constructors at all for a class, C++ will generate a Default Constructor that does nothing
  • Called when you instantiate a new object with no arguments
Player frank;
Player *enemy = new Player;

If implemented

// Section 13
// Default Constructors
#include <iostream>
#include <string>
 
using namespace std;
 
class Player {
private:
   std::string name;
   int health;
   int xp;
 
public:
    void set_name(std::string name_val) { 
        name = name_val; 
    }
 
    std::string get_name() {
        return name;
    }
 
    Player() {
        name = "None";
        health = 100;
        xp = 3;
    }    
};
 
int main() {
    Player hero; // this is calling the default constructor, initializing name, health and xp
    return 0;
}

Overloading Contructor

  • Classes can have as many constructors as necessary
  • Each must have a unique signature
  • Default constructor is no longer compiler-generated once another constructor is declared

Constructor initialization lists

  • So far, all data member values have been set in the constructor body
  • Constructor initialization lists
    • are more efficient
    • initialization list immediately follows the parameter list
    • initializes the data members as the object is created
    • order of initialization is the order of declaration in the class
class Player {
private:
   std::string name {"XXXXXXX"};
   int health;
   int xp;
public:
// Overloaded Constructors
    Player();
    Player(std::string name_val);
    Player(std::string name_val, int health_val, int xp_val);
};
 
Player::Player() 
    : name{"None"}, health{0}, xp{0} {
}
 
Player::Player(std::string name_val) 
   : name{name_val}, health{0}, xp{0} {
}
 
Player::Player(std::string name_val, int health_val, int xp_val) 
    : name{name_val}, health{health_val}, xp{xp_val} {
    // empty body, but code can added obviouly
}
 
int main() {   
    Player empty;
    Player frank {"Frank"};
    Player villain {"Villain", 100, 55};
 
    return 0;
}

Delegating Constructors

  • Often the code for constructors is very similar
  • Duplicated code can lead to errors
  • C++ allows delegating constructors
    • Code for one constructor can call another in the initialization list
    • avoids duplicating code
#include <iostream>
#include <string>
 
using namespace std;
 
class Player {
private:
   std::string name;
   int health;
   int xp;
public:
// Overloaded Constructors
    Player();
    Player(std::string name_val);
    Player(std::string name_val, int health_val, int xp_val);
};
 
Player::Player() 
    : Player {"None", 0, 0} {
        cout << "No-args constructor" << endl;
}
 
Player::Player(std::string name_val) 
   : Player {name_val, 0, 0}  {
           cout << "One-arg constructor" << endl;
}
 
Player::Player(std::string name_val, int health_val, int xp_val) 
    : name{name_val}, health{health_val}, xp{xp_val} {
            cout << "Three-args constructor" << endl;
}
 
int main() {
    Player empty;
    Player frank {"Frank"};
    Player villain {"Villain", 100, 55};
 
    return 0;
}

Default Constructor Parameters

  • Can often simplify our code and reduce the number of overloaded constructors
  • Same rules apply with non-member functions
#include <iostream>
#include <string>
 
using namespace std;
 
class Player
{
private:
   std::string name;
   int health;
   int xp;
public:
    Player(std::string name_val = "None", int health_val = 0, int xp_val = 0);
  //  Player() {}    // Will cause a compiler error
};
 
Player::Player(std::string name_val, int health_val, int xp_val) 
    : name{name_val}, health{health_val}, xp{xp_val} {
            cout << "Three-args constructor" << endl;
}
 
int main() {   
    Player empty;
    Player frank {"Frank"};
    Player hero {"Hero", 100};
    Player villain {"Villain", 100, 55};
 
    return 0;
}

Copy Constructor

  • When objects are copied, C++ must create a new object from an existing object
  • When is a copy of an object?
    • Passing object value as a parameter
    • Returning an object from a function by value
    • Constructing one object based on another of the same class
  • C++ must have a way of accomplishing this so it provides a compiler-defined copy constructor if you don't

Pass object by-value

Player hero {"Hero", 100, 20};
 
void display_player(Player p) {
  // p is a COPY of hero in this example
  // ... use p
  // Desctructor for p will be called
}
 
display_player(hero);

Return object by value

Player enemy;
 
Player create_super_enemy() {
  Player an_enemy {"Super Enemy", 1000, 1000};
  return an_enemy; // A copy of 'an_enemy' is returned.
}
 
enemy = create_super_enemy();

Construct one object based on another

Player hero {"Hero", 100, 100};
 
Player another_hero {hero}; // A copy of 'hero' is made

Default Copy Constructor

  • If you don't provide your own way of copying objects by value, then the compiler provides a default way of copying objects
  • Copies the values of each data member to the new object
    • Default memberwise copy
  • Perfectly fine in many cases
  • Beware if you have a pointer data member
    • Pointer will be copied
    • Not what it is pointing to
    • Shallow vs Deep copy

Best pratices

  • Provide a copy constructor when your class has raw pointer members
  • Provide a copy constructor with a const reference parameter
  • Use STL classes as the already provide copy constructors
  • Avoid using raw pointer data members if possible

Declaring the Copy Constructor

Type::Type(const Type &source);
Player::Player(const Player &player);

Implementation:

Player::Player(const Player &source)
  : name{source.name},
    health {source.health},
    xp {source.xp} {
}

Shallow Copy

Default Copy Constructor

  • Memberwise copy
  • Each data member is copied from the source object
  • The pointer is copied, but not what it points to (shallow copy)
  • Problem: when we release the storage in the destructor, the other object still refers to the released storage.

Deep Copy

Deep copy: create new storage and copy values.

Deep::Deep(const Deep &source) {
  data = new int; // allocate storage
  *data = *source.data;
}

Deep copy constructor - delegating constructor

Deep::Deep(const Deep &source)
  : Deep {*source.data} {
    cout << "Copy constructor - deep" << endl;
}

Move Constructor

  • Sometimes when we execute code, the compiler creates unnamed temporary values
    int total {0};
    total = 100 + 200;
    • 100 + 200 is evaluated and 300 stored in an unnamed temp value
    • The 300 is then stored in the variable total
    • Then the temp value is discarded
  • The same happens with objects as well

When is it useful?

  • Sometimes copy constructors are called many times automatically due to the copy semantics of C++
  • Copy constructors doing deep copying can have a significant performance bottleneck
  • C++11 introduced move sementics and the move constructor
  • Move constructor moves an object rather than copy it
  • Optional but recommended when you have a raw pointer
  • Copy elision - C++ may optimize copying away completely (RVO → Return Value Optimization)

R-Values:

  • Used in moving semantics and perfect forwarding
  • Move semantics is all about r-value references
  • Used by move constructor and move assignment operator to efficiently move an object rather than copy it
  • R-Value reference operator: &&
#include <iostream>
#include <vector>
 
using namespace std;
 
class Move {
private:
    int *data;
public:
    void set_data_value(int d) { *data = d; }
    int get_data_value() { return *data; }
 
    Move(int d); // Constructor
    Move(const Move &source); // Copy Constructor
    Move(Move &&source) noexcept; // Move Constructor
    ~Move(); // Destructor
};
 
Move::Move(int d)  {
    data = new int;
    *data = d;
    cout << "Constructor for: " << d << endl;
}
 
// Copy ctor
Move::Move(const Move &source)
    : Move {*source.data} {
        cout << "Copy constructor  - deep copy for: " << *data << endl;
}
 
//Move ctor
Move::Move(Move &&source) noexcept 
    : data {source.data} {
        source.data = nullptr;
        cout << "Move constructor - moving resource: " << *data << endl;
}
 
Move::~Move() {
    if (data != nullptr) {
        cout << "Destructor freeing data for: " << *data << endl;
    } else {
        cout << "Destructor freeing data for nullptr" << endl;
    }
    delete data;
}
 
int main() {
    vector<Move> vec;
    vec.push_back(Move{10});
    vec.push_back(Move{20});
    // ..
    vec.push_back(Move{80});
    return 0;
}

The this pointer

  • this is a reserved keyword
  • Contains the address of the object - so it's a pointer to the object
  • Can only be used in class scope
  • All member access is done via the this pointer
  • Can be used by the programmer
    • to access data member and methods
    • to determine if two objects are the same
    • can be deferenced (*this) to yield the current object

Const with classes

const Player villain {"Villain", 100, 55};

comme paramètre:

void display_player_name(const Player &p) {
  cout << p.get_name() << endl;
}
 
display_player_name(villain);  // ERROR : compiler asumes that get can modify object/name

Const methods:

class Player {
  private:
    // ...
  public:
    std::string get_name() const;
    // ...
};

Static class members

  • Class data members can be declared as static
    • A single data member that belongs to the class, not the objects
    • Useful to store class-wide information
  • Class function can be declared as static
    • Independent of any objects
    • Can be called using the class name
class Player {
  private:
    static int num_players;
  public:
    static int get_num_players();
};
 
int Player::num_players = 0;

Static method has only access to static members:

int Player::get_num_players() {
  return num_players;
}

Struct vs Classes

  • In addition to define a class we can declare a struct
  • struct comes from the C programming language
  • Essentially the same as a class expect
    • Members are public by default (in classes, members are private by default)
class Person {
  std::string name;
  std::string get_name(); // Why if name is public?
};
 
Person p;
p.name = "Joe";  // Compiler error - private
cout << p.get_name(); // Compiler error - private
struct Person {
  std::string name;
  std::string get_name(); // Why if name is public?
}
 
Person p;
p.name = "Joe";  // OK, public
cout << p.get_name(); // OK, public

Another example of struct:

struct Person {
  std::string name;
  int age;
}
 
Person p1 {"Curly", 31};
Person p2 {"Moe", 39};

Friends of a class

  • Friend
    • A function or a class that has access to private class member
    • And, that function of a class is NOT a member of the class it is accessing
  • Function
    • Can be regular non-member functions
    • Can be member methods of another class
  • Class
    • Another class can have access to private class members

Considerations:

  • Friendship must be granted, NOT taken
    • Declared explicitly in the class that is granting friendship
    • Declared in the function prototype with the keyword friend
  • Friendship is not symmetric
    • If A is a friend of B, B is NOT necessary a friend of A (again, must be granted)
  • Friendship is not transitive
    • If A is a friend of B, AND
    • B is a friend of C
    • then A is NOT a friend of C
class Player {
    friend void display_player(Player &p);
    std::string name;
    int health;
    int xp;
  public:
    // ...
};
void display_player(Player &p) {
  std::cout << p.name << std::endl;  // can change private members
  std::cout << p.health << std::endl;
  std::cout << p.xp << std::endl;
}

Member function of another class:

class Player {
    friend void Other_class::display_player(Player &p);
    std::string name;
    int health;
    int xp;
  public:
    // ...
};
class Other_class {
  ...
  public:
    void display_player(Player &p) {
      std::cout << p.name << std::endl;
      // ...
    }
};

Another class as a friend

class Player {
    friend class Other_class;
};

Operator Overloading

What is Operator Overloading?

  • Using traditional operators such as +, =, * etc with user-defined types
  • Allows user defined types tp behave similar to build-in types
  • Can make code more readable and writable
  • Not done automatically (except for assignment operator), they must be explicitly defined

See Introduction to operator overloading for more details.

Copy assignment operator

  • C++ provides a default asignment operator used for assigning one object to another. ­
    Mystring s1 {"Joe"};
    Mystring s2 = s1; // NOT assignment, same as Mystring s2{s1};
    s2 = s1;  // assignment
  • Default is memberwise assignment (shallow copy), if we have a raw pointer data memeber, we must deep copy

Syntax:

Type &Type::operator=(const Type &rhs);

Example:

Mystring &Mystring::operator=(const Mystring &rhs);
 
s2 = s1; // we write this
s2.operator=(s1); // operator= method is called

Implementation:

Mystring &Mystring::operator=(const Mystring &rhs) {
  if (this == &rhs) {
    return *this;
  }
 
  delete [] str;
  str = new char[std::strlen(rds.str + 1];
  std::strcopy(str, rhs.str);
 
  return *this;
}

Move Assignment Operator (=)

  • You cna choose to overload the move assignment operator
    • C++ will use the copy assignment operator if necessary
      Mystring s1;
      s1 = Mystring {"Joe"}; // Move assignment
  • If we have raw pointer we should overload the move assignment operator for efficiency
Type &Type::operator=(Type &&rhs);

Instead of copying data to a new memory location, we steal the pointer of the source (RHS).

Declaration in .h file:

Mystring(Mystring &&rhs); // move constructor, old version, not present, only for demonstration
Mystring &operator=(Mystring &&rhs); // move constructor, overloaded

Implementation:

Mystring &Mystring::operator=(Mystring &&rhs) {
  std::cout << Using move assignment" << std::endl;
 
  if (this == && &rhs) {
    return *this;
  }
 
  delete [] str;
 
  str = rhs.str;
  rhs.str = nullptr;
 
  return *this;
}

Unary operator as member methods (++, --, -, !)

Syntax:

ReturnType Type::operatorOp();

Examples:

Number Number::operator-() const;
Number Number::operator++() const; // pre-increment
Number Number::operator++(int) const; // post-increment
bool Number::operator!() const;

Inheritance

What is it and why is it used?

  • Provides a method for creating new classes from existing classes
  • The new class contains the data and behaviours of the existing class
  • Allows for reuse of existing classes
  • Allows us to focus on the common attributes among a set of classes
  • Allows new classes to modify behaviors of existing classes to make it unique
    • Without actually modifying the original class
class Account {
  // balance, deposit, withdraw
};
 
class Savings : public Account {
  // interest rate, specialized withdraw
};

Terminology

  • Inheritance
    • Process of creating new classes from existing classes
    • Reuse mechanism
  • Single inheritance
    • A new class is created from another 'single' class
  • Multiple inheritance
    • A new class is created from two (or more) other classes
  • Base class (parent class, super class): The class being extended or inherited from
  • Derived class (child class, sub class):
    • The class being created from Base class
    • Will inherit attributes and operations from Base class
  • “Is-A” relationship
    • Public inheritance
    • Derived classes are sub-types of their Base classes
    • Can use a derived class object whereever we use a base class object
  • Generalization: Combining similar classes into single, more general class based on common attributes
  • Specialization: Creating new classes from existing classes proving more specialized attributes or operations
  • Inheritance of Class Hierachies
    • Organization of our inheritance relationships

C++ Derivation Syntax

class Base {
  // Base class members
};
 
class Derived: access-specifier Base {
  // Derived class members
};

Constructors and Destructors

  • A derived class inherits from its Base class
  • The Base part of the Derived class must be initialized before the Derived class is initialized
  • When a derived object is created:
    • Base class constructor executes
    • then Derived class contructor executes

Passing Arguments to base constructors

class Base {
public:
  Base();
  Base(int);
  // ...
};
 
Derived::Derived(int x)
  : Base(x) { // optional initializers for Derived
  // code
};

Complete implementation:

#include <iostream>
 
using namespace std;
 
class Base {
private:
    int value;
public:
   Base() : value {0}  { 
            cout << "Base no-args constructor" << endl; 
    }
    Base(int x)  : value {x} { 
            cout << "Base (int) overloaded constructor" << endl;
    }
   ~Base() { 
       cout << "Base destructor" << endl;
    }
};
 
class Derived : public Base {
private:
    int doubled_value;
public:
    Derived()
        : Base {}, doubled_value {0} {
            cout << "Derived no-args constructor " << endl; 
    }
    Derived(int x) 
        : Base{x},  doubled_value {x * 2} { 
            cout << "Derived (int) constructor" << endl; 
    }
    ~Derived() { 
        cout << "Derived destructor " << endl; 
    } 
};
 
int main() {
   //  Derived d;
   Derived d {1000};
    return 0;
}

Copy constructor

  • Can invoke Base copy constructor explicitly
    • Derived object other will be sliced.
Derived(const Derived &other)
    : Base(other), doubled_value {other.doubled_value} {
        cout << "Derived copy constructor" << endl;     
}

Redefining Base Class Methods

  • Derived class can directly invoke Base class methods
  • Derived class can override or redefine Base class methods
  • Very powerful in the context if polymorphism
class Account {
public:
    void deposit(double amount) { balance += amount; }
};
 
class Savings_Account: public Account {
public:
  void Savings_Account::deposit(double amount) { // Redefine Base class method
    amount = amount + (amount * int_rate/100); 
    Account::deposit(amount);                   // invoke call Base class method
  }
};

Static Binding of method calls

Base b;
b.deposit(10.0);  // Base::deposit
 
Derived d;
d.deposit(10.0);  // Derived::deposit
 
Base *.ptr = new Derived();
ptr->deposit(10.0);  // Base::deposit

Multiple Inheritance

  • A derived class inherits from two or more Base classes at the same time
  • The Base classes may belong to unrelated class hierarchies
  • A Department Chair:
    • Is-A Faculty and
    • Is-A Administrator
class Department_Chair
  : public Faculty, public Administrator {
 
  // code  
};
  • Some compelling use-cases
  • Easily misused
  • Can be very complex

Polymorphism

  • Fundamental to Object-Oriented programming
  • Polymorphism
    • Compile-time / early binding / static binding (all refers to same concept)
    • Runtime / late binding / dynamic binding
  • Runtime Polymorphism
    • Being able to assign different meanings to the same function at runtime
  • Allows us to program more abstractly
    • Think general vs specific
    • Let C++ figure out which function to call at runtime
  • Not the default in C++, runtime polymorphism is acheived via
    • Inheritence
    • Base class pointers or references
    • virtual functions

An non-polymorphic example - Static Binding

Account a;
a.withdraw(1000);    // Account::withdraw()
 
Savings b;
b.withdraw(1000);    // Savings::withdraw()
 
Checking c;
c.withdraw(1000);    // Checking::withdraw()
 
Trust d;
d.withdraw(1000);    // Trust::withdraw()
 
Account *p = new Trust();
p->withdraw(1000);   // Account::withdraw
                     // should be
                     // Trust::withdraw()

void display_account(const Account &acc) {
  acc.display();  // will always use Account::display
}
 
Account a;
display_account(a);
 
Savings b;
display_account(b);
 
Checking c;
display_account(c);
 
Trust d;
display_account(d);

An polymorphic example - Dynamic Binding

withdraw method is virtual in Account

Account a;
a.withdraw(1000);    // Account::withdraw()
 
Savings b;
b.withdraw(1000);    // Savings::withdraw()
 
Checking c;
c.withdraw(1000);    // Checking::withdraw()
 
Trust d;
d.withdraw(1000);    // Trust::withdraw()
 
Account *p = new Trust();
p->withdraw(1000);   // Trust::withdraw

display method is virtual in Account

Below, the display_account() will always call the display method based on the object's type at runtime.

void display_account(const Account &acc) {
  acc.display();
}
 
Account a;
display_account(a);
 
Savings b;
display_account(b);
 
Checking c;
display_account(c);
 
Trust d;
display_account(d);

Code example

Example using static binding:

#include <iostream>
 
class Base {
public:
  void say_hello() const {
    std::cout << "Hello - I'm a Base class object" << std::endl;
  }
};
 
class Derived: public Base {
public:
  void say_hello() const {
    std::cout << "Hello - I'm a Derived class object" << std::endl;
  }
};
 
void greetings(const Base &obj) {
  std::cout << "Greetings: ";
  obj.say_hello();
}
 
int main() {
    Base b;
    b.say_hello(); // "Hello - I'm a Base class object"
 
    Derived d;
    d.say_hello(); // "Hello - I'm a Base class object"
 
    greetings(b); // "Greetings: Hello - I'm a Base class object"
    greetings(d); // "Greetings: Hello - I'm a Base class object"
 
    Base *ptr = new Derived();
    ptr->say_hello(); // "Hello - I'm a Base class object"
 
    std::unique_ptr<Base> ptr1 = std::make_unique<Derived>();
    ptr1->say_hello(); // "Hello - I'm a Base class object"
 
    delete ptr;
 
    return 0;
}

Using a Base class pointer

Account *p1 = new Account();
Account *p2 = new Savings();
Account *p3 = new Checking();
Account *p4 = new Trust();
 
vector<Account *> accounts {p1, p2, p3, p4};
 
for (auto acc_ptr: accounts) {
  acc_ptr->withdraw(1000);
}
 
// delete pointers
#include <iostream>
#include <vector>
 
class Account {
public:
    virtual void withdraw(double amount) {
        std::cout << "In Account::withdraw" << std::endl;
    }
    virtual ~Account() {  }
};
 
class Checking: public Account  {
public:
    virtual void withdraw(double amount) {
        std::cout << "In Checking::withdraw" << std::endl;
    }
 
    virtual ~Checking() {  }
};
 
class Savings: public Account  {
public:
    virtual void withdraw(double amount) {
        std::cout << "In Savings::withdraw" << std::endl;
    }
    virtual ~Savings() {  }
};
 
class Trust: public Account  {
public:
    virtual void withdraw(double amount) {
        std::cout << "In Trust::withdraw" << std::endl;
    }
    virtual ~Trust() {  }
};
 
int main() {
    std::cout << "\n === Pointers ==== " << std::endl;
    Account *p1 = new Account();
    Account *p2 = new Savings();
    Account *p3 = new Checking();
    Account *p4 = new Trust();
 
    p1->withdraw(1000);
    p2->withdraw(1000);
    p3->withdraw(1000);
    p4->withdraw(1000);
 
    std::cout << "\n === Array ==== " << std::endl;
    Account *array [] = {p1, p2, p3, p4};
    for (auto i=0; i<4; ++i) {
        array[i]->withdraw(1000);
    }
 
    std::cout << "\n === Array ==== " << std::endl;
    array[0] = p4;
    for (auto i=0; i<4; ++i) {
        array[i]->withdraw(1000);
    }
 
    std::cout << "\n === Vector ==== " << std::endl;
    std::vector<Account *> accounts {p1, p2, p3, p4};
    for (auto acc_ptr: accounts) {
        acc_ptr->withdraw(1000);
    }
 
    std::cout << "\n === Clean up ==== " << std::endl;
    delete p1;
    delete p2;
    delete p3;
    delete p4;
 
    return 0;
}

Virtual Functions

  • Redefined functions are bound statically
  • Overriden functions are bound dynamically
  • Virtual functions are overriden
  • Allow us to treat all objects generally as objects of the Base class

Declaring virtual functions:

  • Declare the function you want to override as virtual in the Base class
  • Virtual functions are virtual all the way down the hierarchy from this point
  • Dynamic polymorphism only via Account class pointer or reference
class Account {
public:
  virtual void withdraw(double amount);
  // ...
};

Declaring virtual functions (derived classes):

  • Override the function in the Derived class
  • Function signature and return type must match exactly
  • Virtual keyword not required but is best practice
  • If you don't provide overriden version it is inherited from it's base class
class Checking: public Account {
public:
  virtual void withdraw(double amount);
  // ...
};

Virtual Destructors

  • Problems can happen when we destroy polymorphic objects
  • If a derived class is destroyed by deleting its storage via the base class pointer and the class a non-virtual destructor, then the behavior is undefined in the C++ standard.
  • Derived objects must be destroyed in the correct order starting ate the correct destructor

Solution / Rule:

  • If a class has a virtual function, always provide a public virtual destructor
  • If base class destructor is virtual, then all derived class destructors are also virtual
class Account {
public:
  virtual void withdraw(double amount);
  virtual ~Account();
  // ...
};
 
class Savings: public Account  {
public:
    virtual void withdraw(double amount);
    virtual ~Savings() {  }
};

Using the Override Specifier

  • We can override Base class virtual functions
  • The function signature and return must be exactly the same
  • If they are different, then we have redefinition not overriding
  • Redefinition is statically bound
  • Overriding is dynamically bound
  • C++11 provides an override specifier to have the compiler ensure overriding

Illustrated problem:

#include <iostream>
 
class Base {
public:
    virtual void say_hello() const {
        std::cout << "Hello - I'm a Base class object" << std::endl;
    }
    virtual ~Base() {}
};
 
class Derived: public Base {
public:
    virtual void say_hello() {             // Notice I forgot the const
        std::cout << "Hello - I'm a Derived class object" << std::endl;
    }
    virtual ~Derived() {}
};
 
 
int main() {
    Base *p1 = new Base();
    p1->say_hello();
 
    Derived *p2 = new Derived();
    p2->say_hello();
 
    Base *p3 = new Derived();
    p3->say_hello();
 
    return 0;
}
  • say_hello method signatures are different
  • So Derived redefines say_hello instead of overriding it

In the output, third line should say Hello - I'm a Derived class object:

Hello - I'm a Base class object
Hello - I'm a Derived class object
Hello - I'm a Base class object

Correction:

class Derived: public Base {
public:
    virtual void say_hello() const override {
        std::cout << "Hello - I'm a Derived class object" << std::endl;
    }
    virtual ~Derived() {}
};

The final specifier

C++11 provides the final specifier

  • When used at the class level, it prevents a class from being derived from
  • When used at the method level, it prevents virtual method from being overriden in derived classes

Syntax:

class MyClass final {
  // ...
};

Syntax on derived class:

class Derived final: public Base {
  // ..
}

Method level

class A {
public:
  virtual void do_something();
};
 
 
class B: public A {
public:
  virtual void do_something() final;
};
 
 
class C: public B {
public:
  virtual void do_something();  // Compiler error: Can't override
};

Using Base class references

  • We can also use Base class references with dynamic polymorphism
  • Useful when we pass objects to functions that expects a Base class reference
Account a;
Account &ref = a;
ref.withdraw(100);  // Account::withdraw
 
Trust t;
Account &ref1 = t;
ref1.withdraw(100);  // Trust::withdraw
void do_withdraw(Account &account, double amount) {
  account.withdraw(amount);
}
 
Account a;
do_withdraw(a, 100);  // Account::withdraw
 
Trust t;
do_withdraw(t, 100);  // Trust::withdraw

Pure Virtual Functions and Abstract Classes

Abstract class:

  • Cannot instantiate objects
  • These classes are used as base classes in inheritance hierarchies
  • Often referred to as Abstract Base Classes

Concrete class:

  • Used to instantiate objects from
  • All their member functions are defined

Abstract Base Class:

  • Too generic to create objects from
  • Serves as parent to Derived classes that may have objects
  • Contains at least one pure virtual function

Pure virtual function:

  • Used to make a class abstract
  • Specified with =0 in its declaration
    virtual void function() = 0;
  • Typically do not provide implementation
  • Derived classes must override the base class
  • If the Derived class does not override, then it is also abstract
  • Used when it doesn't make sense for a base class to have an implementation
    • But concrete classes must implement it.
    • Example with Shape:
      virtual void draw() = 0;

Abstract Classes as Interfaces

  • An abstract class taht has only pure functions
  • These functions provide a general set of services to the user of the class
  • Provided as public
  • Each subclass is free to implement these services as needed
  • Every service (method) must be implemented
  • The service type information is strictly enforced

Printable example:

  • C++ does not provide true interfaces
  • We use abstract classes and pure virtual function to achieve it
  • Suppose we want to be able to provide Printable support for any object, we wish witout knowing its implementation at compile time
    std::cout << any_object << std::endl;
    • any_object must conform to the Printable interface
class Printable {
  friend ostream &operator << (ostream &, const Printable &obj);
public:
  virtual void print(ostream &os) const = 0;
  virtual ~Printable() {};
  // ...
};
 
ostream &operator<<(ostream &os, const Printable &obj) {
  obj.print();
  return os;
}

To be Printable:

class AnyClass: public Printable {
  virtual void print(ostream &os) override {
    os << "Hi from AnyClass";
  }
};

Usage:

AnyClass *ptr = new AnyClass();
cout << *ptr << endl;
 
void function1 (AnyClass &obj) {
  cout << obj << endl;
}
 
void function2 (Printable &obj) {
  cout << obj << endl;
}
 
function1(*ptr);  // "Hi from AnyClass"
function2(*ptr);  // "Hi from AnyClass"

We can use capital I plus underscore to designate an Interface:

class I_Shape {
  // ...
};

Smart Pointers

Issues with raw pointers:

  • C++ provides absolute flexibility with memory management
    • Allocation
    • Deallocation
    • Lifetime management
  • Some potentially serious problems
    • Uninitialized (wild) pointers
    • Memory leaks
    • Dangling pointers
    • Not exception safe
  • Ownership:
    • Who owns the pointer?
    • When should a pointer be deleted?

What are smart pointers

  • They are objects
  • Can only point to heap-allocated memory
  • Automatically call delete when no longer needed
  • Adhere to RAII principles
  • C++ Smart Pointers:
    • Unique Pointers (unique_ptr)
    • Shared Pointers (shared_ptr)
    • Weak Pointers (weak_ptr)
    • Auto Pointers (auto_ptr) → deprecated
  • Defined by class templates
    • Wrapper around a raw pointer
    • Overloaded operators
      • Dereference (*)
      • Member selection (->)
      • Pointer arithmetic not supported (++, --, etc)
    • Can have custom deleters

Requires include:

#includes <memory>
{
  std::unique_ptr<SomeClass> ptr = ...;
  ptr->method();
  cout << (*ptr) << endl;
}
 
// Out of scope, ptr will be destroyed automatically when no longer needed

RAII - Resource Aquisition Is Initialization

  • Common idiom or pattern used in software design based on container object lifetime
  • RAII objects are allocated on the stack
  • Resource Aquisition
    • Open file
    • Allocate memory
    • Aquire a lock
  • Is Initialization
    • The resource is aquired in a constructor
  • Resource relinquishing
    • Happens in the destructor
      • Close the file
      • Deallocate the memory
      • Release the lock

Unique Pointer

unique_ptr

  • Simple smart pointer
  • unique_ptr<T>
    • Points to an object of type T on the heap
    • It is unique - there can only be one unique_ptr<T> pointing to the object on the heap
    • Owns what it points to
    • Cannot be assigned or copied
    • Can be moved
    • When the pointer is destroyed, what it points to is automatically destroyed
{
  std::unique_ptr<int> p1 { new int {100} };
  std::cout << *p1 << std::endl; // 100
  *p1 = 200;
  std::cout << *p1 << std::endl; // 200
}

Methods:

  • ptr.get() → get address
  • ptr.reset()ptr is now nullptr

Using move:

std::unique_ptr<int> p1;
std::unique_ptr<int> p2 = { new int {100}};
p1 = p2; // Compiler Error
p1 = std::move(p2);  // p2 is now ''nullptr''

make_unique

Since C++14

{
  std::unique_ptr<int> p1 = make_unique<int>(100);
 
  std::unique_ptr<Account> p2 = make_unique<Account>("Joe", 500);
 
  auto p3 = make_unique<Player>("Hero", 100, 100);
}

Another example:

std::vector<std::unique_ptr<Account>> accounts;
 
accounts.push_back( make_unique<Checking_Account>("James", 1000));
accounts.push_back( make_unique<Savings_Account>("Billy", 4000, 5.2));
accounts.push_back( make_unique<Trust_Account>("Bobby", 5000, 4.5));
 
for (const auto &acc: accounts) {
    std::cout << *acc << std::endl;
}

Shared Pointer

shared_ptr

  • Provides shared ownership of heap objects
  • shared_ptr<T>
    • Points to an object of type T on the heap
    • It is not unique - there can many shared_ptrs pointing to the same object on the heap
    • Establishes shared ownership relationship
    • Can be assigned or copied
    • Can be moved
    • When the use count is zero, the managed object on the heap is destroyed
{
  std::shared_ptr<int> p1 { new int {100} };
  std::cout << *p1 << std::endl; // 100
  *p1 = 200;
  std::cout << *p1 << std::endl; // 200
}

use_count

std::shared_ptr<int> p1 { new int {100} };
std::cout << "Use count: "<< p1.use_count() << std::endl; 		// 1
 
std::shared_ptr<int> p2 { p1 };                    // shared ownwership
std::cout << "Use count: "<< p1.use_count() << std::endl; 		// 2
 
p1.reset();	// decrement the use_count; p1 is nulled out
std::cout << "Use count: "<< p1.use_count() << std::endl; 		// 0 
std::cout << "Use count: "<< p2.use_count() << std::endl; 		// 1

make_shared

std::shared_ptr<int> p1 = std::make_shared<int>(100); // use_count: 1
std::shared_ptr<int> p2 { p1 };                       // use_count: 2
std::shared_ptr<int> p3;
p3 = p1;                                              // use_count: 3

Weak Pointer

weak_ptr

  • Provides non-owning “weak” reference
  • weak_ptr<T>
    • Points to an object of type T on the heap
    • Does not participate in owning relationship
    • Always created from a shared_ptr
    • Does not increment or decrement reference use count
    • Used to prevent strong reference cycles which could prevent objects from being deleted

Problem

Circular or cyclic reference

  • A refers to B
  • B refers to A
  • Shared strong ownership prevents heap deallocation

#include <iostream>
#include <memory>
 
using namespace std;
 
class B;    // forward declaration
 
class A {
    std::shared_ptr<B> b_ptr;
public:
    void set_B(std::shared_ptr<B> &b) {
        b_ptr = b;
    }
    A() { cout << "A Constructor" << endl; }
    ~A() { cout << "A Destructor" << endl; }
};
 
class B {
    std::shared_ptr<A> a_ptr;
public:
    void set_A(std::shared_ptr<A> &a) {
        a_ptr = a;
    }
    B() { cout << "B Constructor" << endl; }
    ~B() { cout << "B Destructor" << endl; }
};
 
int main() {
    shared_ptr<A> a  = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->set_B(b);
    b->set_A(a);
 
    return 0;
}

Output:

A Constructor
B Constructor

Solution

Use one pointer as a weak pointer

#include <iostream>
#include <memory>
 
using namespace std;
 
class B;    // forward declaration
 
class A {
    std::shared_ptr<B> b_ptr;
public:
    void set_B(std::shared_ptr<B> &b) {
        b_ptr = b;
    }
    A() { cout << "A Constructor" << endl; }
    ~A() { cout << "A Destructor" << endl; }
};
 
class B {
    std::weak_ptr<A> a_ptr;     // make weak to break the strong circular reference
public:
    void set_A(std::shared_ptr<A> &a) {
        a_ptr = a;
    }
    B() { cout << "B Constructor" << endl; }
    ~B() { cout << "B Destructor" << endl; }
};
 
int main() {
    shared_ptr<A> a  = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->set_B(b);
    b->set_A(a);
 
    return 0;
}

Output:

A Constructor
B Constructor
A Destructor
B Destructor

Custom Deleters

  • Sometimes when we destroy a smart pointer, we need more than just destroy the object on the heap
  • These are special cases
  • C++ smart pointers allow you to provide custom deleters
  • Lots of ways to achieve this:
    • Functions
    • Lambdas
    • Etc.
void my_deleter(Test *ptr) {
  cout << "In my custom deleter" << endl;
  delete ptr;
}
 
shared_ptr<Test> ptr { new Test{}, my_deleter };

With lambda expression:

shared_ptr<Test> ptr (new Test{100}, [] (Test *ptr) {
  cout << "In my custom deleter" << endl;
  delete ptr;
});

Exception Handling

Basic concepts

Exception Handling:

  • dealing with extraordinary situations
  • indicates that an extraordinary situation has been detected or has occured
  • program can deal with the extraordinary situations in a suitable manner

What causes exceptions?

  • Insufficient resources
  • Missing resources
  • Invalid operations
  • Range violations
  • Underflows / overflows
  • Illegal data and many others

Exception safe: when code handles exceptions

Terminology

Exception: an object or primitive type that signales that an error occured

Throwing an exception (raising an exception):

  • Code detects that an error has occured or will occur
  • The place where the error occured may not know how to handle error
  • Code can throw an exception describing the error to another part of the program that knows how to handle the error

Catching an exception (handle exception):

  • Code that handles the exception
  • May or may not cause the program to terminate

C++ syntax

throw:

  • Throws an exception
  • Followd by an argument

try { code that may throw an exception }:

  • We place code that may throw an exception in a try block
  • if the code throws an exception, the try block is exited
  • the thrown exception is handled by a catch handler
  • if no catch handler exists, the program terminates

catch(Exception ex) { code to handle exception }:

  • Code that handles exception
  • can have multiple catch handlers
  • may or may not cause the program to terminate
#include <stdexcept>
...
throw std::runtime_error{"An error occured"};

I/O and Streams

Standard Template Library (STL)

  • A library of powerful, reusable, adaptable, generic classes and functions
  • Implemented using C++ templates
  • Implements common data structures and algorithms
  • Huge class library
  • Developed by Alexander Stepanov (1994)

At it's core, it's:

  • Assortment of commonly used containers
  • Known time and size complexity
  • Tried and tested - reusability
  • Consistent, fast and type-safe
  • Extensible

Elements of the STL:

  • Containers
    • Collection of objects or primitive types (array, vector, deque, stack, set, map, etc.)
    • Algorithms
      • Functions for processing sequences of elements from containers (find, max, count, accumulate, sort, etc)
    • Iterators
      • Generate sequeneces of elements from containers (forward, reverse, by value, by reference, constant, etc)

Simple example

#include <vector>
#include <algorithm>
 
std::vector<int> v {1, 5, 3};
 
// sort
 
std::sort(v.begin(), v.end());
for (auto elem: v) {
  std::cout << elem << std::endl;
}
 
// reverse
 
std::reverse(v.begin(), v.end());
for (auto elem: v) {
  std::cout << elem << std::endl;
}
 
// accumulate
int sum {};
sum = std::accumulate(v.begin(), v.end(), 0);

Types of containers

  • Seqence containers
    • array, vector, list, forward_list, deque
  • Associative containers
    • set, multi set, map, multi map
  • Container adapters
    • stack, queue, priority queue

Types of iterators

  • Input iterators → from the container to the program
  • Output iterators → from the program to the container
  • Forward iterators → navigate one item at a time in one direction
  • Random access iterators - directly access a container item

Types of algorithms

  • About 60 algorithms in the STL
    • Non-modifying
    • Modifying

Generic Programming with macros

  • Generic programming
    • “Writing code that works with a variety of types as arguments, as long as those arguments types meet specific syntactic and semantic requirements” - Bjarne Stoustrup
  • Macros
  • Function templates
  • Class templates

Macros (#define)

  • C++ preprocessor directive
  • No type information
  • Simple substitution

Note: using macros is not necessary good practice.

#define MAX_SIZE 100
#define PI 3.14159
 
if (num > MAX_SIZE) {
  // handle
}
 
double area = PI * r * r; 

Macro with arguments

#define MAX(a, b) ((a > b) ? a : b)
 
std::cout << MAX(10, 20) << std::endl; // 20

Generic Programming with Function Templates

What is a C++ template?

  • Blueprint
  • Function and class templates
  • Allow plugging-in any data type
  • Compiler generates the appropriaate function/class from the blueprint
  • Generic programming / meta-programming

max function as a template function

  • We need to tell the compiler this is a template function
  • We also need to tell that T is the template parameter
    template <typename T>
    T max(T a, T b) {
      return (a > b) ? a : b;
    }
  • We may also use class instead of typename
    template <class T>
    T max(T a, T b) {
      return (a > b) ? a : b;
    }
  • Now the compiler can generate the appropriate function from the template
  • This happens at compile-time
    int a {10};
    int b {20};
     
    std::cout << max<int>(a, b);
  • Many times, the compiler can deduce the type and the template parameter is not needed
    std::cout << max(a, b);
  • We can use almost any type we need
    • In the example of max function, using greater-than operator, the operator must be overloaded for the type. Example Player class with score, could be overloaded with this in mind
template <typename T1, typename T2>
void func(T1 a, T2 b) {
  // implementation
}

Example with struct:

template <typename T>
T min(T a, T b) {
    return (a < b) ? a : b;
}
 
template <typename T1, typename T2>
void func(T1 a, T2 b) {
    std::cout << a << " " << b << std::endl;
}
 
struct Person {
  std::string name;
  int age;
  bool operator<(const Person &rhs) const {
    return this->age < rhs.age;
  }
}
 
std::ostream &operator<<(std::ostream &os, const Person &p) {
    os << p.name;
    return os;
}
 
Person p1 {"Curly", 31};
Person p2 {"Moe", 39};
 
Person p3 = min(p1, p2);
std::cout << p3.name << " is younger" << std::endl; // Curly is younger
 
func(p1, p2); // Curly Moe
template <typename T>
void my_swap(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}

Generic Programming with Class Templates

Template classes are typically completely contained in header files. So, we would have the template class in Item.h and no Item.cpp file would be used.

template <typename T>
class Item {
private:
    std::string name;
    T   value;
public:
    Item(std::string name, T value) : name{name}, value{value} 
    {}
    std::string get_name() const {return name; }
    T get_value() const { return value; }
};
 
Item<int> item1 {"Joe", 100};

Creating a Generic Array Template Class

Next code is for demonstration, most of the time use std::array instead.

template <typename T, int N>
class Array {
    int size {N};           // how do we get the N???
    T values[N];        // the N needs to ne known at compile-time!
 
    friend std::ostream &operator<<(std::ostream &os, const Array<T, N> &arr) {
        os << "[ ";
        for (const auto &val: arr.values)
            os << val << " ";
        os << "]" << std::endl;
        return os;
    }
public:
    Array() = default;
    Array(T init_val) {
        for (auto &item: values)
            item = init_val;
    }
    void fill(T val) {
        for (auto &item: values )
            item = val;
    }
    int get_size() const {
        return size;
    }
    // overloaded subscript operator for easy use
    T &operator[](int index) {
        return values[index];
    }
};

STL Containers

Containers:

  • Data structures that can store object of almost any type
    • Template-based classes
  • Each container has member functions
    • Some are specific to the container
    • Others are available to all containers
  • Each container has an associated header file
    • #include <container_type>

Containers - Common

Function Description
Default constructor Initializes an empty container
Overloaded constructor Initializes containers with many options
Copy constructor Initializes a container as a copy of another container
Move constructor Moves existing container to new container
Destructor Destroys a container
Copy assignment (operator=) Copy one container to another
Move assignment (operator=) Move one container to another
size Returns the number of elements in a container
empty Returns boolean of if it's empty or not
insert Insert an element into the container

Other functions: swap, erase, clear, begin, end, rbegin, rend, cbegin, cend, crbegin, crend.

Container elements

What types of elements can we store in containers?

  • A copy of the elements will be stored in the container
    • All primitives are OK
  • Element should be
    • Copyable ans assignable (copy ctor / copy assignment)
    • Moveable for efficiency (move ctor / move assignment)
  • Ordered associative containers must be able to compare elements
    • operator<, operator==

STL Iterators

  • Allows abstracting an arbitrary container as a sequence of elements
  • They are objects that work like pointers by design
  • Most container classes can be traversed with iterators

Declaring iterators

  • Iterators must be declared base on the container type they will iterate over
    • Syntax: container_type::iterator_type iterator_name;
    • Examples:
      std::vector<int>::iterator it1;
      std::list<std::string>::iterator it2;
      std::map<std::string, std::string>::iterator it3;
      std::set<char>::iterator it4;

Iterator begin and end methods

std::vector<int> vec {1, 2, 3};

std::vector<int>::iterator it = vec.begin();
// or
auto it = vec.begin();

Using iterators

std::vector<int> vec {1, 2, 3};
std::vector<int>::iterator it = vec.begin();
 
while (it != vec.end()) {
  std::cout << *it << " ";
  ++it;
}
// prints: "1 2 3"
 
for (auto it = vec.begin(); it != vec.end(); it++) {
  std::cout << *it << " ";
}

Set:

#include <set>
 
std::set<char> suits {'C', 'H', 'S', 'D'};
auto it = suits.begin();
 
while (it != suits.end()) {
  std::cout << *it << " " << std::end;
  ++it;
}
// prints: "C H S D"

Reverse iterator:

std::vector<int> vec {1, 2, 3};
std::vector<int>::reverse_iterator it = vec.begin();
 
while (it != vec.end()) {
  std::cout << *it << " ";
  ++it;
}
// prints: "3 2 1"

Other iterators

  • const_iterator (cbegin(), cend())
  • reverse_iterator (rbegin(), rend())
  • Constant reverse iterator (crbegin(), crend())

STL Algorithms

  • STL Algorithms work on sequences of container elements provided to them by iterator
  • SQL has many common and useful algorithms
  • Too many to describe in this section
  • Many algorithms require extra information in order to do their work
    • Functors (function objects)
    • Function pointers
    • Lambda expressions (C++11)

Algorithms and iterators

#include <algorithm>
  • Different containers support different types of iterators
    • Determines the types of algorithms supported
  • All STL algorithms expect iterators as arguments
    • Determines the sequence obtained from the container

Example with find

  • The find algorithm tries to locate the first occurrence of an element in a container
  • Lots of variations
  • Returns an iterator pointing to the located eleemnt or end()
std::vector<int> vec {1, 2, 3};
 
auto loc = std::find(vec.begin(), vec.end(), 3);
 
if (loc != vec.end()) {
  std::cout << *loc << std::endl; // 3
}

Example with for_each

struct Square_Functor {
  void operator()(int x) { // overload () operator
    std::cout <<  x * x << " ";
  }
};
 
Square_Functor square;  // Function object
 
std::vector<int> vec {1, 2, 3};
 
std::for_each(vec.begin(), vec.end(), square);

Using a function pointer

void square(int x) {
  std::cout <<  x * x << " ";
}
 
std::vector<int> vec {1, 2, 3};
 
std::for_each(vec.begin(), vec.end(), square);

Using a lambda expression

std::vector<int> vec {1, 2, 3};
 
std::for_each(vec.begin(), vec.end(),
  [](int x) { std::cout <<  x * x << " "; } // lamda
);

Other algorithms

Some other, not all:

  • std::count()
  • std::count_if()
  • std::replace()
  • std::all_of()
  • std::transform(), ex: std::transform(str1.begin(), str1.end(), ::toupper)

Sequence Containers

Associative Containers

  • Sets
  • Maps

Container Adaptors

  • Stack
  • Queue
  • Priority Queue

Associative Containers

  • Associative containers
    • Collection of stored objects that allow fast retrieval using a key
    • STL provides Sets and Maps
    • Usually implemented as a balanced binary tree or hashsets
    • Most operations are very efficient
  • Sets
    • std::set
    • std::unordered_set
    • std::multiset
    • std::unordored_multiset

Set

Set → See CPP Reference on std::set

  • Similar to a mathematical set
  • Ordered by key
  • No duplicate elements
  • All iterators available and invalidate when correspoinding element is deleted
#include <set>

std::set<int> s {1, 2, 3, 4, 5};

std::set<std::string> stooges {
  std::string {"Larry"},
  "Moe",
  std::string {"Curly"}
};

s = {4, 6, 8, 8, 10}; // second '8' is ignored

s.insert(7); // returns std::pair, ex: [7, true] (true/false = successfully inserted)
s.insert(8);  // will ignore if already exists in set

Autres méthodes:

  • s.size()
  • s.max_size
  • No concept of front or back
  • s.find()
  • s.count(1)
  • s.clear()
  • s.empty()

Map

Map → See CPP Reference on std::map

  • Similar to dictionary
  • Elements are stored as Key-Value pairs (std::pair)
  • Ordered by key
  • No duplicate elements (keys are unique)
  • All iterators available and invalidate when correspoinding element is deleted
#include <map>

std::map<int> s {1, 2, 3, 4, 5};

std::map<std::string, int> m1 {
  {"Larry", 18},
  {"Moe", 25}
};

std::map<std::string, std::string> m2 {
  {"Larry", "King"},
  {"Moe", "Bar"}
};


std::pair<std::string, std::string> p1 = {"James", "Mechanic"};

m2.insert(p1);

m2.insert(std::make_pair("Roger", "Ranger"));


m2["Roger"] = "Cook"; // update
m2["Frank"] = "Singer"; // insert, key doesn't exists

Autres méthodes:

  • m.erase(“Frank”)
  • m.clear()
  • m.empty()

Autres containers

Container Adpators

Lambda Expressions

Since C++11

Motivation

Prior to C++11:

  • Function objects and function pointers
  • We often write many short function that control algorithms
  • The short functions would be encapsulated in small classes to produce function objects
  • Many times, the classes and functions are far removed from where they are used, leading to modification, maintenance and testing issues
  • Compiler cannot effectively inline these functions for efficiency

Structure of a Lamda Expression

Syntax:

Source

[] () { std::cout << "Hi"; }(); // immediately executed
 
auto l = [] (int x) { std::cout << x; };
 
l(10);

Using pointers

Capture List

An empty capture list would result in a stateless lambda expression.

Examples

Using lambda expression as function parameters

#include <functional>
 
// C++14 -> void below is return type, int is parameter type
void foo(std::function<void(int)> l) {
  l(10);
}
 
// Also C++14
void foo(void (*l)(int)> l) {
  l(10);
}
 
// C++20
void foo(auto l) {
  l(10);
}

Returning lambda expression from functions

#include <functional>
 
std::function<void(int)> foo() {
  return [] (int x) { std::cout << x; };
}
 
// or
 
void (*foo())(int) {
  return [] (int x) { std::cout << x; };
}
 
// or
 
auto foo() {
  return [] (int x) { std::cout << x; };
}

With predicate

void print_if(std::vector<int> nums, bool (*predicate)(int)) {
  for (int i: nums) {
    if (predicate(i)) {
      std::cout << i;
    }
  }
}
 
int main() {
  std::vector<int> nums {1, 2, 3};
 
  print_if(nums, [] (auto x) { return x % 2 == 0; });
  print_if(nums, [] (auto x) { return x % 2 != 0; });
 
  return 0;
}

Stateful Lamba Expressions

Using non-empty capture list.

Transformation of lambda expression by the compiler

Lamda definition

int y {10};
 
auto l = [y] (int x) { std::cout << x + y; };

Compiler-generated closure

class CompilerGeneratedName {
private:
  int y;
public:
  CompilerGeneratedName(int y): y{y} {};
 
  void operator() (int x) const {
    std::cout << x + y;
  }
}

const in generated class avoid modifying member values.

For a large number of capture variables:

  • [=] → Default capture by value
  • [&] → Default capture by reference
  • [this] → Default capture this object by reference
  • [=, &x] → Default capture by value, but capture x by reference
  • Other variations exists

Lambdas and the STL

Some code samples to show lambda uses with STL.

for_each

Non-modifying sequence operation, displays each element of nums.

std::vector<int> nums {10, 20, 30, 40, 50};
 
std::for_each(nums.begin(), nums.end(), [] (int num) {
    std::cout << num << " ";
});

transform

std::vector<int> test_scores {93, 88, 75, 68, 65};
int bonus_points {5};
 
std::transform(test_scores.begin(), test_scores.end(), test_scores.begin(), [bonus_points] (int score) {
    return score += bonus_points;
});
 
// Display updated test_scores
for (int score : test_scores) {
  std::cout << score << " ";
}

Enumerations

What is an enumeration?

  • A user-defined type that models a set of contant integral values
    • Days of the week
    • Ths suits in a deck of cards (Clubs, Hearts, Spades, Diamonds)
    • State of a system
    • Etc

Structure

enum-key enum-name : enumerator-type { };

Simplest enumerator:

enum {Red, Green, Blue}; // Implicit initialization:
                         // Red = 0, Green = 1 and Blue = 2
enum {Red = 1, Green = 2, Blue = 3}; // Explicit initialization
enum {Red = 1, Green, Blue}; // Explicit/Initialisation initialization
                             // Green = 2 and Blue = 3

Enumetator type is deduced with width of bits required.

Anonymus enum provides no type safety:

enum {Ref, Green, Blue};
 
int my_color;
my_color = Green;  // valid
my_color = 4;      // valid

Named:

enum Color {Ref, Green, Blue};
 
Color my_color;
my_color = Green;  // valid
my_color = 4;      // not valid

Unscoped Enumerations

enum State { Engine_Failure, Inclement_Weather, Nominal };
 
std::underlying_type_t<State> user_input;
std::cin >> user_input;
 
switch (user_input) {
  case Engine_Failure:
    state = State(user_input);
    break;
  case Inclement_Weather:
    state = State(user_input);
    break;
  case Nominal:
    state = State(user_input);
    break;
  default:
    std::cout << "User input is not a valid state.";
 
}

With operator overload:

enum State { Engine_Failure, Inclement_Weather, Nominal };
 
std::istream& operator>>(std::istream& is, State& state) {
  std::underlying_type_t<State> user_input;
 
  is >> user_input;
 
  switch (user_input) {
    case Engine_Failure:
      state = State(user_input);
      break;
    case Inclement_Weather:
      state = State(user_input);
      break;
    case Nominal:
      state = State(user_input);
      break;
    default:
      std::cout << "User input is not a valid state.";
  }
 
  return is;
}
 
// ...
State state;
std::cin >> state;

Scoped Enumerations

Enumerator is qualified.

enum class enum-name : enumerator-type {};

Problem:

enum Whale { Blue, Beluga, Gray };
enum Shark { Greatwhite, Hammerhead, Bull };
 
if (Beluga == Hammerhead) {
  std::cout << "A beluga whale is equivalent to a hammerhead shark.";
}

Name clashes, here with Blue:

enum Whale { Blue, Beluga, Gray };
enum Shark { Greatwhite, Hammerhead, Bull, Blue }; // Error: Blue already defined

Solution:

enum class Whale { Blue, Beluga, Gray };
 
Whale whale = Whale::Beluga;
 
switch (whale) {
  case Whale::Blue:
    std::cout << "Blue whale";
    break;
  case Whale::Beluga:
    std::cout << "Beluga whale";
    break;
  case Whale::Gray:
    std::cout << "Gray whale";
    break;
  default:
    std::cout << "Unknown whale";
}

Using scoped enumerator values

enum class Item { Milk = 350, Bread = 250, Apple = 132 };
 
int milk_code = int(Item::Milk);                    // milk_code = 350
// or
int milk_code = static_cast<int>(Item::Milk);
 
int total = int(Item::Milk) + int(Item::Bread);     // total = 600
std::cout << underlying_type_t<Item>(Item::Milk);   // 350

Assert

#include <cassert>
...
assert(!Empty());

Ressources

developpement/cpp/modern.txt · Dernière modification : 2022/12/14 15:46 de sgariepy