C++

Table of Contents

Syntax

Functions

Pass-by-reference and pass-by-value

By reference does NOT mean that we are working with a pointer inside the function! It simply means that operations made to the argument is made to the original value it refers to.

By value means what it usually means; makes a copy when passed to the function.

void by_val(char a) {
  a = 'c';
}

void by_ref(char& a) {
  a = 'b';
}


int main() {
  char a = 'a';
  std::cout << a << '\n';
  by_val(a);
  std::cout << a << '\n';
  by_ref(a);
  std::cout << a << '\n';
}

Class

friend class

The friend declaration appears in a class body and grants a function or another class access to private and protected members of the class where the friend declaration appears.

See here for examples and more on this.

const at then end of a function

You can think of every class function implicitly being passed a pointer to the class instance, like so print(Foo* this), which allows us to access the fields of the instance.

Now, what do you do if you need a class method where you only want to be able to read the fields, NOT mutate them? We add a const at the end! This can be thought of as print(const Foo* this).

class Foo {
public:
  void print() const;
};

void Foo::print() const {
  // do yah stuff
}

A good explanation can be found here.

virtual base class

class A { public: void Foo() {} };
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};

In the example above, the virtual keyword makes sure that for class D the inheritance from class A corresponds to single "instance" of A, not two, as would be the case if we didn't have the virtual keyword.

So with regular inheritance, you believe you have:

  A
 / \
B   C
 \ /
  D

But in the memory layout, you have:

A   A
|   |
B   C
 \ /
  D

The virtual keyword makes sure that you will have the first memory-layout, as you probably intended. Otherwise, calling a function which is inherited from A would be ambigious.

See here for more.

virtual functions

Simply functions which we allow to be overriden by sub-classes.

Pointers

This is a really good write-up on common practices for using pointers in C++.

Basically, these are the ones which are available to you:

std::unique_ptr is a smart pointer that owns and manages another object

through a pointer and disposes of that object when the unique_ptr goes out of scope.

  • C++11: unique_ptr included in standard
  • C++14: provided the convenient way of creating new objects: make_unique<A>(), instead of having to do unique_ptr<A>(new A()), as we do below.
#include <iostream>
#include <memory>
#include <cassert>

struct A {
 A() { std::cout << "Creating A!" << '\n'; };
 ~A() { std::cout << "Destructing A!" << '\n'; };

 void marco() { std::cout << "Marco!" << '\n'; };
 void polo() { std::cout << "Polo!" << '\n'; };
};


std::unique_ptr<A> pass_through(std::unique_ptr<A> a_ptr) {
 a_ptr->marco();
 return a_ptr;
}


int main() {
 std::cout << "Unique Ownership Semantics Demo\n";
 std::cout << "________________________________\n\n";
 {
 std::unique_ptr<A> p = std::unique_ptr<A>(new A()); // p is a unique_ptr that owns a A
 auto q = pass_through(std::move(p)); 
 assert(!p); // now p owns nothing and holds a null pointer
 q->polo();   // and q owns the A object
 } // ~A called here
}

It's worth mentioning that in the above code, the unique_ptr is actually constructed / destructed multiple times since we're passing and returning by copy, not by reference, but the instance of A lives on until leaving the scope!

Keywords and specifiers

constexpr

Declares that it is possible to evaluate the value of the function or variable at compile time. This is really useful for things like:

  • Constants
  • Evaluation of constants, i.e. const float oneeighty = DegreesToRadians(180.0f) will be evaluated at compile-time.
  • Since C++ templates do not have something like trait in Rust, we cannot be sure that a type or class used in the template actually support the operations used. We really would like to discover such a misuse at compile time. The expression below would make it so that if we call max with arguments not supporting the comparison operation, we will get a compile-error instead of runtime-error!

    template< typename Type >
    constexpr Type max( Type a, Type b ) { return a < b ? b : a; }
    
Interesting example

If you compile the code below, then run objdump -C -t <obj_file> or nm <obj_file> | c++filt you'll actually see no mention of the arr_size function!

#include "iostream"

// here we're passing an array by reference
template <typename T, int N>
constexpr T arr_size(T (&array)[N]) {
  return sizeof(array) / sizeof(array[0]);
}


int main() {
  constexpr int arr[] = {-1, 2, 4, -3, 5, 2, -5, 2};
  constexpr size_t size = arr_size(arr);

  std::cout << size << '\n';
}

While if you do the same for the following code:

#include "iostream"

template <typename T, int N>
T arr_size(T (&array)[N]) {
  return sizeof(array) / sizeof(array[0]);
}


int main() {
  constexpr int arr[] = {-1, 2, 4, -3, 5, 2, -5, 2};
  size_t size = arr_size(arr);

  std::cout << size << '\n';
}

You will find mentioning of the function! This is due to the fact that in the first example, the compiler computes the size of the array at compile-time, which means we can throw away the entire function afterwards (since it isn't used further).

Concepts

Smart pointers

Just a test bro

int N = 10;
std::vector<int> v;

for (int i = 0; i < N; i++) {
  v.push_back(i);
}

for (int i : v) {
  if (i % 2 == 1) {
      std::cout << i << std::endl;
  }
  else {
    std::cout << i << ' ';
  }
}
%matplotlib inline
import matplotlib.pyplot as plt

xs, ys = zip(*t)
plt.plot(xs, ys)

Matrix implementation in C++

template <class T>
using Vector = std::vector<T>;

template <class T>
using Matrix = Vector< Vector<T> >;

int main() {
Matrix<int> A;
const int N = 10;

for (int i = 0; i < N; i++) {
  Vector<int> a;
  for (int j = 0; j < N; j++) {
    a.push_back(j);
  }
  A.push_back(a);
}

for (Vector<int> row : A) {
  for (int entry : row) {
    std::cout << entry << ' ';
  }
  std::cout << std::endl;
}
}

Class impl - better

#include <iostream>
#include <vector>
#include "tuple"


template <class T>
using Vector = std::vector<T>;

template <class T>
using Matrix = Vector< Vector<T> >;

template <class T>
class VectorClass {
public:
  std::vector<T> data;

  size_t shape() const { return data.size(); }
  void print() const;

  VectorClass<T>& operator+= (const VectorClass<T>& rhs) {
    if (shape() != rhs.shape()) throw "Vectors do not have same dimension.";

    for (size_t i = 0; i < data.size(); i++) {
      this->data[i] = this->data[i] + rhs.data[i];
    }

    return *this;
  }

  friend VectorClass<T> operator+ (VectorClass<T> lhs, const VectorClass<T>& rhs) {
    lhs += rhs;
    return lhs;
  }
};

/**
 * Prints the data of the vector.
 */
template <class T>
void VectorClass<T>::print() const {
  std::cout << "Printing vector" << std::endl;
  for (T entry : data) {
    std::cout << entry << ' ';
  }
  std::cout << std::endl;
}

/**
 * Represents a matrix.
 */
template <class T>
class MatrixClass {
private:
  Vector<size_t> _shape;
public:
  // MatrixClass() { };
  MatrixClass(Vector<Vector<T> > data_);
  MatrixClass(Vector<size_t> shape_);

  Vector<Vector<T> > data;
  const Vector<size_t>& shape() const {return _shape; };

  void print();

  // operator overloadings
  MatrixClass<T>& operator+= (const MatrixClass<T>& rhs) {
    if (shape() != rhs.shape()) throw "Matrices not of same shapes.";
    if (shape().size() < 1) return this;

    for (auto dim : shape()) {
      for (size_t i = 0; i < dim; i++) {
        // Assumes we've already implemented addition for Vector<T>
        this->data[i] = this->data[i] + rhs.data[i];
      }
    }

    return *this;
  }

  friend MatrixClass<T>& operator+ (MatrixClass<T> lhs, const MatrixClass<T>& rhs) {
    lhs += rhs;
    return *lhs;
  }
};

template <class T>
MatrixClass<T>::MatrixClass(Vector<Vector<T> > data_) {
  data = data_;  // assign to the property data
  _shape.push_back(data.size());
  if (data.size() > 0) _shape.push_back(data[0].size());
}

template <class T>
MatrixClass<T>::MatrixClass(Vector<size_t> shape_) {
  _shape = shape_;
  // allocate mem for all rows
  data.reserve(_shape[0]);

  // allocate mem for each entry on each row
  if (_shape.size() > 1) {
    for (int row_idx = 0; row_idx < _shape[0]; row_idx++) {
      data[row_idx].reserve(_shape[1]);
    }
  }
}

template <class T>
void MatrixClass<T>::print() {
  std::cout << "Printing class" << std::endl;
  for (Vector<T> row : this->data) {
    for (T entry : row) {
      std::cout << entry << ' ';
    }
    std::cout << std::endl;
  }
}


int main() {
  const int N = 10;

  Vector<int> row1 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  Vector<Vector<int> > rows {row1, row1, row1, row1, row1};
  auto B = new MatrixClass<int>(rows);
  B->print();

  auto a = new VectorClass<int>();
  a->data = row1;

  auto b = new VectorClass<int>();
  b->data = row1;

  auto c = *a + *b;
  c.print();
}