Last active
April 13, 2020 19:25
-
-
Save alexdrone/470a1d85084cd4a2878e171bf539aa8a to your computer and use it in GitHub Desktop.
Notes for teaching Moden C++.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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. | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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". | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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