Skip to content

Instantly share code, notes, and snippets.

@alexdrone
Last active April 13, 2020 19:25
Show Gist options
  • Save alexdrone/470a1d85084cd4a2878e171bf539aa8a to your computer and use it in GitHub Desktop.
Save alexdrone/470a1d85084cd4a2878e171bf539aa8a to your computer and use it in GitHub Desktop.
Notes for teaching Moden C++.
#include <iostream>
#include <stdlib.h>
#include <vector>
#include <array>
struct Vertex {
float x, y, z;
};
std::ostream& operator<<(std::ostream& stream, const Vertex& vertex) {
stream << vertex.x << "," << vertex.y << "," << vertex.z;
return stream;
}
// When using std::array we neet to have a template around functions that
// need to know the array size.
template<std::size_t SIZE>
void multiply_array(const std::array<int, SIZE>& arr, const int multiplier) {
for(auto& e : arr) {
e *= multiplier;
}
}
void arrays_demo() {
// Store Vertex object (that would result in a inline memory layout).
// Memory is contiguous / same cache line.
// ** Cons ** When the array is resized all of the object are going to be
// copied to the new allocated buffer.
// Moving instead of copying largely solves this particular issue bu there are
// still some copying which is not ideal.
std::vector<Vertex> vs;
// Memory is not contiguous.
// Useful for large collections
std::vector<Vertex*> vector_of_ptrs;
vs.push_back({1, 2, 3});
vs.push_back({2, 3, 4});
// Iterate the vector.
for (size_t i = 0; i < vs.size(); i++) {
std::cout << vs[i] << std::endl;
}
//.. or range iteration
for (const auto& /* or const Vertex& */ v : vs) {
std::cout << v << std::endl;
}
// ** note **
for (auto /* or Vertex */ v: vs) {
std::cout << v << std::endl;
}
// will result in each object being copied while iterating.
vs.erase(vs.begin() + 1); // Remove the item at index 1.
vs.clear(); // Remove all of the elements.
// ** optimization 1 **
std::vector<Vertex> vs2;
vs2.reserve(3); // Reserve memory for the vector ahead.
// ** emplace_back: Prevents the object from being copied from the
// caller stack to the vector contiguos mem by allocating it right away
// in the vector buffer.
vs2.emplace_back(Vertex{1, 2, 3}); // like push_back but no copies.
vs2.emplace_back(Vertex{1, 2, 3});
// ** static arrays. **
// std::array is for arrays that don't grow.
std::array<Vertex, 5> array;
array[0] = {1,2,3};
array[1] = {2,4,5};
}
#include <stdlib.h>
class Container {
public:
// ** Abstract class / No constructor **
// Adding 0 at the end makes it a pure virtual function.
// A class with a pure virtual function is abstract.
virtual size_t size() = 0;
// Virtual destructor.
virtual ~Container() {};
};
class Vec: public Container {
public:
// Usage:
// const auto v1 = new Vec{10};
// const auto v2 = new Vec(10);
Vec(size_t size): elements_{new double[size]}, size_{size} {}
// Construct from an inline list of args.
// Container *c = new Vec {10,20,30};
Vec(std::initializer_list<double> list):
// initialize the members first.
elements_{new double[list.size()]},
size_{list.size()} {
std::copy(list.begin(), list.end(), elements_);
}
virtual size_t size() override {
return size_;
}
// Destructor.
~Vec() {
delete[] elements_;
}
// const at the end of function declaration means non-mutating.
virtual void foo() const {
// size_ = 10; [ERROR] You cannot mutate the member in a const method.
foo_ = 42; // This is allowed because the member is mutable.
std::cout << "Foo in Vec" << std::endl;
}
private:
double* elements_;
size_t size_;
mutable int foo_;
};
//// is kind of:
//// if (const auto& v = dynamic_cast<SubVec *>(o)) { ... }
class SubVec: public Vec {
public:
// Inherit super constructor.
// subclasses don't automatically inherit constructors from their
// superclasses.
SubVec(std::initializer_list<double> list): Vec(list) { };
// override must be explicit in C++.
virtual void foo() const override {
std::cout << "Foo in SubVec" << std::endl;
}
void bar() {
std::cout << "bar:" << bar_ << std::endl;
}
private:
int bar_;
};
// Another example.
class ScopedVecPtr {
public:
ScopedVecPtr(Vec *vec): vec_{vec} { }
~ScopedVecPtr() {
delete vec_;
}
// Overrides the -> operator so that dereferencing a ScopedVecPtr
// returns the Vec pointer.
Vec *operator->() {
return vec_;
}
// const version of it.
const Vec *operator->() const {
return vec_;
}
private:
Vec *vec_;
};
struct SomeData {
int foo;
int bar;
};
void static_dynamic_cast_demo() {
// Cast and polymorphism.
Vec *vo = new Vec{10};
Vec *v2o = new SubVec{4};
vo->foo();
v2o->foo();
if (const auto vxx = dynamic_cast<SubVec *>(vo)) {
// Fails cast.
vxx->foo();
} else {
std::cout << "dynamic_cast: vo is not a Vector2" << std::endl;
}
if (const auto vxx = dynamic_cast<SubVec *>(v2o)) {
// Calls SubVec impl.
vxx->foo();
} else {
std::cout << "dynamic_cast: v2o is not a Vector2" << std::endl;
}
if (const auto vxx = dynamic_cast<Vec *>(v2o)) {
// Still calling the right SubVec 2 impl.
vxx->foo();
} else {
std::cout << "dynamic_cast: v2o is not a Vector" << std::endl;
}
if (const auto vxx = static_cast<SubVec *>(vo)) {
// Casts Vec to its subclass because and works
// would call SubVec impl even if the istance is a Vec.
// static_cast bypass the v-table.
vxx->foo();
// This would work too but it would access to some garbage data for
// _bar.
vxx->bar();
}
// stack allocated objects can be assigned to their constructor argument
// straight away.
ScopedVecPtr scoped = new Vec { 10 };
// Can use scoped as a Vec reference.
scoped->foo();
// ** Used arrow operator to get mem offset for a type **
int64_t offset1 = (int64_t)&((SomeData*)nullptr)->foo; // prints 0.
int64_t offset2 = (int64_t)&((SomeData*)nullptr)->bar; // prints 4.
// useful to binary serialize data.
}
#include <iostream>
// ** copying **
// By default, objects can be copied. This is true for objects of user-defined
// types as well as for builtin types. The default meaning of copy is memberwise
// copy: copy each member.
// copy semantics of structs and classes is identical.
struct Vec2 {
float x, y;
};
// Primitive bare-metal String class.
class Str {
public:
Str(const char *string) {
size_ = strlen(string);
buffer_ = new char[size_+1];
memcpy(buffer_, string, size_);
// null-terminated string.
buffer_[size_] = 0;
}
// ** prevent copy **
// Str(const Str& other) = delete;
// ** shallow copy constructor (default implementation) **
// Str(const Str& other): buffer_{other.buffer_}, size_{other.size_} { }
// An alternative implementation would be:
// Str(const Str& other) { memcpy(this, &other, sizeof(Str)) }
// **deep copy constructor **
Str(const Str& other): size_{other.size_} {
buffer_ = new char[size_+1];
// copies the other object buffer into this one.
memcpy(buffer_, other.buffer_, size_+1);
}
virtual ~Str() {
delete[] buffer_;
}
virtual char& operator[](unsigned int index) {
return buffer_[index];
}
private:
char *buffer_;
size_t size_;
// note: By marking a function as 'friend' it can access its private
// members.
friend std::ostream& operator<<(std::ostream& stream, const Str& string);
};
// Makes Str usable with cout <<...
std::ostream& operator<<(std::ostream& stream, const Str& string) {
stream << string.buffer_;
return stream;
}
// Every time the function is called the copying constructor is called.
void print_that_performs_a_deep_copy_of_the_arg(Str string) {
std::cout << string << std::endl;
}
// A readonly reference is passed to the function.
void print_that_does_not_copy(const Str& string) {
std::cout << string << std::endl;
}
void copying_demo() {
// ** stack allocation **
Vec2 a = {2, 3};
Vec2 b = a; // b is a copy of a.
b.x = 5; // This does not change the value of a.
// ** heap allocation **
Vec2 *ha = new Vec2{2, 3};
Vec2 *hb = ha; // Only the pointer is copied. (ha and hb points to the same storage).
hb->x = 5; // This changes the value of ha.x as well.
// ** objects **
Str s1 = "John";
Str s2 = s1; // copies s1 to s2.
// ** If there's no copy constructor it performs a shallow copy of members.
// buffer_ is still shared among the two classes.
// ** If Str(const Str& other) is defined it performs a deep copy.
// buffer_ is not shared.
// ** If there's no copy constructor:
std::cout << s1 << std::endl; // Prints "John"
std::cout << s2 << std::endl; // Prints "John"
// ** [CRASHES] We try to delete the same buffer_ twice.
}
class Copyable {
public:
Copyable(const Copyable& other) = default;
Copyable& operator=(const Copyable& other) = default;
// The implicit move operations are suppressed by the declarations above.
};
class MoveOnly {
public:
MoveOnly(MoveOnly&& other);
MoveOnly& operator=(MoveOnly&& other);
// The copy operations are implicitly deleted, but you can
// spell that out explicitly if you want:
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
};
class NotCopyableOrMovable {
public:
// Not copyable or movable
NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
NotCopyableOrMovable& operator=(const NotCopyableOrMovable&)
= delete;
// The move operations are implicitly disabled, but you can
// spell that out explicitly if you want:
NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&&)
= delete;
};
#include <iostream>
#include <stdlib.h>
#include <vector>
#include <array>
#include <algorithm>
#include <functional>
void hello() {
std::cout << "hello" << std::endl;
}
void print_value(int value) {
std::cout << value << std::endl;
}
// A function that takes a function pointer as an argument.
void for_each_ptr(const std::vector<int>& values, void(*do_function)(int)) {
for (const auto& value: values) {
do_function(value);
}
}
// Version of the function above that takes a std::function argument
// instead of a raw function ptr.
// function pointers don't work with lambdas that have a non-empty capture
// block.
void for_each(const std::vector<int>& values, std::function<void(int)> func) {
for (const auto& value: values) {
func(value);
}
}
void lambdas_demo() {
// ** function pointers **
// gets a pointer to the hello function.
auto function = hello; // &hello would work too.
function(); // call the function at the pointer.
// explicit type.
void(*func)() = hello; // similiar to objc block syntax.
// typedef'd function type.
typedef void(*HelloFunctionType)();
HelloFunctionType func2 = hello;
std::vector<int> values = {1, 2, 3, 4, 5};
for_each_ptr(values, print_value);
// ** lambdas **
// Whenever you have a function pointer argument you can replace it
// in the calling site with a lambda.
// [] is the capture block.
for_each_ptr(values, [](int value) {
std::cout << value << std::endl;
});
// [&]: all of the vars are captured by ref.
// [=]: all of the var are capture by value.
// [&a, b]: a is captured by ref, b by value.
// [this]: 'this' object is being captured.
int a = 5;
auto lambda_that_captures_a_by_value = [a](int value) {
std::cout << value + a << std::endl;
};
// for_each_ptr(values, lambda_that_captures_a_by_value); [ERROR]
// function pointers don't work with lambdas that have a non-empty capture
// block.
// We can replace the function pointer in the arg with std::function.
for_each(values, lambda_that_captures_a_by_value);
// if a lambda wants to mutate a variable that was passed in the
// capture block, it must be marked as 'mutable'.
int b = 2;
auto lambda_that_captures_b_by_ref_and_modifies_it = [&b](int value) mutable {
b = 10;
};
// example of usage in std
auto iterator = std::find_if(values.begin(), values.end(), [](int value) {
return value > 1;
});
auto found_value = *iterator;
}
#include <iostream>
int get_rvalue() {
return 10;
}
int& get_lvalue_ref() {
static int __lvalue_ref = 10;
return __lvalue_ref;
}
// can be called with lvalue or rvalue args.
void set_value(int value) { }
// can be called with lvalue only.
void set_value_with_lvalue_ref_arg(int& value) { }
// accepts [const lvalue_refs], hence lvalues and rvalues.
void set_value_with_const_lvalue_ref(const int& value) { }
// accepts lvalues only.
void print_name_with_lvalue_ref(std::string& name) {
std::cout << name << std::endl;
}
// accepts [const lvalue_refs], hence lvalues and rvalues.
void print_name_with_const_lvalue_ref(const std::string& name) {
std::cout << name << std::endl;
}
// accepts [rvalue_refs] only.
void print_name_with_rvalue_ref(std::string&& name) {
std::cout << name << std::endl;
}
// Overrides.
// accepts [const lvalue_refs], hence lvalues and rvalues.
void print_name(const std::string& name) {
std::cout << "[lvalue]" << name << std::endl;
}
// accepts [rvalue_refs] only.
// specialized override for temporary values only.
// useful with move semantics because the ref can be stolen.
void print_name(std::string&& name) {
std::cout << "[rvalue]" << name << std::endl;
}
void lvalue_rvalue_demo() {
// lvalue have a location, storage (usually left part of the expr.)
// rvalue are temporary values (right side of the expr.)
int i = 10; // i [lvalue] = 0 [rvalue].
// 10 = i; [ERROR] You cannot store anything in a [rvalue]
int a = i; // a [lvalue] = i [lvalue].
// [rvalue]s can be returned by a func call a well.
int i2 = get_rvalue(); // i [lvalue] = get_rvalue [rvalue].
// get_rvalue() = 5; [ERROR] You cannot store anything in a [rvalue].
// (Expression must be a modifiable lvalue).
get_lvalue_ref() = 42; // get_lvalue_ref [lvalue_ref] = 42 [rvalue]. Works fine.
// void set_value(int value) can be called with lvalue or rvalue args.
set_value(5);
set_value(i);
// ** You cannot take an [lvalue_ref] from an [rvalue].
set_value_with_lvalue_ref_arg(i);
// set_value_with_lvalue_ref_arg(5); [ERROR] Initial value of ref
//to non-const must be an lvalue.
// ** const **
// While you cannot take an [lvalue_ref] from an [rvalue] (e.g. int& a = 10)
// you CAN take an [const lvalue_ref] from an [rvalue].
// int& b = 10; [ERROR].
const int& b = 10; // b [lvalue_ref] = 10 [rvalue].
// (The compiler create a temp hidden storage for the rvalue to assign its ref)
// set_value_all(const int& value) accepts [const lvalue_refs],
// hence lvalues and rvalues.
// ** This is the preferred signature for refs. **
set_value_with_const_lvalue_ref(5);
set_value_with_const_lvalue_ref(i);
set_value_with_const_lvalue_ref(b);
std::string firstname = "John"; // firstname [lvalue] = "John" [rvalue]
std::string lastname = "Appleseed"; // lastname [lvalue] = "Appleseed" [rvalue]
std::string fullname = firstname + lastname; // fullname [lvalue] = (firstname + lastname) [rvalue]
print_name_with_lvalue_ref(fullname);
//print_name_with_lvalue_ref(firstname + lastname); // [ERROR].
print_name_with_const_lvalue_ref(fullname);
print_name_with_const_lvalue_ref(firstname + lastname);
// ** rvalue_refs **
// You can pass rvalue_refs using the && operator.
// e.g. void print_name(std::string&& name)
print_name_with_rvalue_ref(firstname + lastname); // Works, arg is a rvalue.
// print_name_with_rvalue_ref(fullname); // [ERROR], arg is a lvalue.
// ** overrides **
print_name(fullname); // Prints: "[lvalue] John Appleseed".
// accepts [rvalue_refs] only.
// specialized override for temporary values only.
// useful with move semantics because the ref can be stolen since we know
// that the resource is not owned by anybody.
print_name(firstname + lastname); // Prints: "[rvalue] John Appleseed".
}
#include <iostream>
#include <stdlib.h>
#include <vector>
#include <array>
#include <algorithm>
struct Point {
float x, y;
};
template<typename T>
void print(T value) {
std::cout << value <<std::endl;
}
template<typename T, size_t SIZE>
class Array {
public:
Array() {}
Array(std::initializer_list<T> list) {
size_t min_size = std::min(list.size(), SIZE);
size_t size = min_size - 1 < 0 ? 0 : min_size - 1;
std::copy(list.begin(), list.begin() + size, buffer_);
}
T& operator[](int index) {
return buffer_;
}
size_t size() {
return SIZE;
}
private:
T buffer_[SIZE];
};
void templates_demo() {
print(5);
Array<int, 5> array = {0, 1, 2, 3};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment