title: “📖 Notes on MSFM - C++”
date: 2019-12-04
tags: notes
mathjax: true
C++ is a complied
(vs interpreted: python), general-purpose
(vs domain-specific: HTML) programming language created by Danish programmer Bjarne Stroustrup
as an extension to C.
A compiler translate a high level language into a low level language and create an executable program.
#include "foo.hpp"
Compile with g++ at the command line:
$ g++ toto.cpp
$ g++ toto.cpp -E (show c pre-processor)
$ g++ toto.cpp --verbose (ask compile to give different steps)
Running the complied result:
$ /a.exe
The C++ standard library
is a collection of classes and functions, represented by different headers. For example, include the <iostream>
header to handle input and outputs and other non-standard headers using double quoto.
#include <iostream>
#include "foo.h"
define N 4
std::cout << N + 2; // show 6
In C++, function, class and variable can only be declared once. We use guards
to make sure we do not duplicate declaration in multiple files.
#ifndef "foo.h"
#define "foo.h"
Some classes and functions are grouped under the same name, which divides the global scope into sub-scopes, each with its own namespaces.
Functions and classes in the C++ standard library are defined in the std
namespace. For example, the cin
(standard input), cout
(standard output) and end
(end line) objects.
char c;
std::cin >> c;
std::cout << c;
std::endl;
Alternatively, we can use using namespace std;
.
Every variable has to have a type
in C++, and the type has to be declared and cannot be changed. There are fundamental types and user-defined types (classes)
Characters In computer, each bit
stores a binary (0/1) value. A byte
is 8 bits. The computer stores characters in a byte using the ASCII format.
Numbers The computer stores numbers in binary format with bits. The leftmost
bit is used to store the sign of a number. (See twos-complement method). Real values are stored using a mantissa
and an exponent:
Note that very few values can be exactly represented, and how close we can get depends on the number of bits available.
Type | Size (Bytes) | Value Range |
---|---|---|
bool | 1 | true or false |
char | 1 | -128 to 127 |
short | 2 | -32,768 to 32,767 |
int | 4 | -2,147,483,648 to 2,147,483,647 |
float | 4 | 3.4E +/- 38 |
double | 8 | 1.7E +/- 308 |
C++ is a strongly typed
language, which means type errors needs to be resolved for all variables at compile
time.
Every console application has to have a main()
function, which takes no argument and returns an integer value by default.
A function that adds two numbers:
#include <iostream>
using namespace std;
int Add(int a, int b)
{
return a+b;
}
int main()
{
int result = Add(2, 3);
cout << " Result: " << result << endl;
}
Overloading
allows 2 or more functions to have the same name, but they must have different input argument types
.
Function object, or functors
, are objects that behave like functions, are functions with state.
A regular function looks like this:
int AddOne(int val)
{
return val+1;
}
int result = AddOne(2)
A function object implementaion:
class AddOne
{
public:
int operator()(int& val)
{
return val+1;
}
};
AddOne addone;
int val = 2;
int result = addone(val)
Lambdas is a new feature introduced in C++11, which is an inline function that can be used as a parameter or local object.
[] (string s) // [] is the lambda introducer/capture clause
{
cout << s << endl;
}
Example 1
vector<int> v{1, 3, 2, 4, 6};
for_each(v.cbegin(), v.cend(), //range
[](int elem) {cout << elem << endl;}) //lambda
Example 2
vector<int> v{1, 3, 2, 4, 6};
transform(v.begin(), v.end(),
v.begin(), [] (int elem) {return elem * elem});
Example 3
vector<Person> ppl;
sort(ppl.begin(), ppl.end(),
[](const Person& p1, const Person&p2)
{
if (p1.GetAge() < p2.GetAge()) return true;
else return false;
});
The keyword extern means the function is declared in another file.
extern int foo(int a);
int main() { return foo(100); }
C++ provides inline
funcitons such that the overhead of a small function can be reduced. When inline function is called the entire code of the function is inserted at the point of the inline function call.
Use typedef
keyword to define a type alias.
typedef double OptionPrice;
typedef double StockPrice;
typedef double Strike;
OptionPrice BSPrice(StockPrice S, Strike K)
Standard operations:
Arithmetic: +, -, *, /
Comparison: <, >, <=, >=
Negate: !
Equality, non Equality: ==, !=
Logical and, or, &&, ||
Assignment: =
Modulo: %
Increment, Decrement: i++, i--
Multiple Operations: i += 1, i -= 1, i *= 1, i /= 1
Note the difference between i++
and ++i
i++; // return (old) i and increment i
++i; // increment i and return new i
Use the const
keyword to define a constant value. The compiler
will stop any attempt to alter the constant values.
Since C++ is a strongly typed language, it is preferred to use const int N = 4
, instead of #define N 4
, as the former defines a type.
Example 1 A reference is an alias for a variable and cannot rebind to a different variable. We can change val
by changing ref
:
int val = 10;
int& ref = val;
ref = 20; // this will change val to 20
Example 2 We can also bind a const reference to a const object. An error will be raised if attempt to change the value or the reference.
const int val = 10;
const int& ref = val;
val = 20; // error
ref = 20; // error
Example 3 We can also bind a const reference to a non-const object, thereafter we can NOT change the object using the reference.
int val = 10;
const int& ref = val;
val = 20; // ok
ref = 20; // error
Pass By Value In a function, we can pass an argument by either value
or reference
. When passing by value
, the variable x
will NOT be changed. In this case, we waste time to both create a copy inside the function and memory to store the copy
void DoubleValue(int number)
{
number = number * 2;
}
int main()
{
int x = 5;
DoubleValue(x);
cout<<"x = "<<x<<endl;
}
x = 5
Pass By Reference When passing by reference
(by adding &
in the function argument parameter), the variable x
WILL be changed.
void DoubleValue(int& number)
{
number = number * 2;
}
int main()
{
int x = 5;
DoubleValue(x);
cout<<"x = "<<x<<endl;
}
x = 10
Pass By Const Reference We add const when we do not want the specific function argument to be tempered when passed by reference. In this example, there will be a compiler error as we are trying to change the const reference number
in the function.
void DoubleValue(const int& number)
{
number = number * 2; // error, cannot change const ref "number"
}
int main()
{
int x = 5;
DoubleValue(x);
cout<<"x = "<<x<<endl;
}
In computer memory, each stored values has an address associated with it. We use a pointer
object to store address of another object and access it indirectly.
There are two pointer operator:
&
: address of
operator, used to get the address of an object*
: de-reference
operator, used to access the objectExample 1
int* ptr = nullptr; // initiate an empty pointer
int* ptr = &val; // initiate ptr with the address of val
*ptr = 20; // change val using the ptr pointer
Example 2 If the object is const, a pointer cannot be used to change it.
const int val = 10;
const int* ptr = &val;
*ptr = 20; // error
Example 3 You can have a pointer that itself is const
int val = 10;
int* const ptr = &val;
*ptr = 20; // ok
int val2 = 20;
ptr = &val2 // error, as the pointer is const
C++ allows implicit
and explicit
conversions of types.
short a = 1;
int b;
b = a; // implicit conversion
b = (int) a; // explicit conversion
However, the traditional explicit type-casting allows conversions between any types, and leads to run-time error. To control these conversions, we introduce four specific casting operators:
dynamic_cast<new_type>( )
: used only with pointers (and/or references to objects); can cast a derived
class to its base
class; base-to-derived
conversions are allowed only with polymorphic
base classclass Base {virtual void foo() {} };
class Derived : public Base { };
int main() {
Derived* derived_ptr;
Base* base_ptr = dynamic_cast<Base*> (derived_ptr);
Base* base_ptr_2 = new Derived;
Derived* derived_ptr_2 = dynamic_cast<Derived*> (base_ptr_2);
// ok, base class polymorphic
Base* base_ptr_3 = new Base;
Derived* derived_ptr_3 = dynamic_cast<Derived*> (base_ptr_3);
// will not work, derived_ptr_3 will be assigned a nullptr
std::cout << "derived_ptr_2: " << derived_ptr_2 << std::endl;
std::cout << "derived_ptr_3: " << derived_ptr_3 << std::endl;
return 0;
}
derived_ptr_2: 0x7fa5cec00630
derived_ptr_3: 0x0
static_cast < new_type>( )
: used only with pointers (and/or references to objects); can cast base-to-derived
or derived-to-base
, but no safety check at run-time;Base* base_ptr_3 = new Base;
Derived* derived_ptr_3 = static_cast<Derived*> (base_ptr_3);
// not nullptr this time, but lead to error when de-referencing derived_ptr_3
derived_ptr_3: 0x7fc3d7400690
reinterpret_cast <new_type>( )
: convert pointer to another unrelated class; often lead to unsafe de-referencingclass A {};
class B {};
A* a = new A;
B* b = reinterpret_cast<B*> (a);
const_cast <new_type>( )
: remove/set the constant-ness of an objectAn array is a fixed collection of similar kinds of items that are stored in a contiguous block in memory. We define the size of the array at creation, and the array index starts a 0 in C++.
int a[10];
int a[] {1, 2, 3} // uniform initializer syntax
The address of the array is the same as the address of the first element of the array. Therefore, we can access an array using pointer increment - very efficient.
int a[10];
int* ptr = &a[0]; // the same as int* ptr = a
int a0 = a[0]; // the same as int a0 = *ptr
int a3 = a[3]; // the same as int a3 = *(ptr+3) or *(a+3)
Dynamic memory allocation
is necessary when you do NOT know the size of the array at compile time. We use a new
keyword paired with a delete
keyword.
int* a = new int[10];
delete[] = a; // correct. this tells the CPU that it needs to clean up multiple variables instead of a single variable
delete a; // incorrect. using this version will lead to a memory leak.
Dynamic allocate a matrix with cast.
#include <iostream>
void func(double** a) {
* a = new double[16];
}
int main() {
int (* a)[4];
func( (double**)&a );
for (int i=0; i<4; i++) {
for (int j=0; j<4; j++) {
a[i][j] = 1;
}
}
for (int i=0; i<4; i++) {
for (int j=0; j<4; j++) {
std::cout << a[i][j] << " " ;
}
std::cout << std::endl;
}
}
1 1 1 1
1 1 1 1
1 1 1 1
1 1 1 1
`
A C++ library
is a package of reusable code typically with these two components:
There are two types of c++ libraries: static
and dynamic
libraries.
static
library has a .a
(.lib
on Windows) extension and the library codes are complied as part of the executable - so that user only need to distribute the executable for other users to run the file with a static library.dynamic
library has a .so
(.dll
on Windows) extension and is loaded at run times. It saves space as many program can share a copy of dynamic library code, and it can be upgraded to new versions without replacing all the executables using it.if (condition_1)
{
statement1;
}
else if (condition_2)
{
statement2;
}
else
{
statement2;
}
A switch statement tests an integral or enum value against a set of constants. we can NOT use a string in the switch statement.
int main()
{
int value = 0;
cin >> value;
switch(value)
{
case 0:
cout << "value is zero";
break; // if remove this break, it will also show case 1 even if value is 0
case 1:
cout << "value is one";
break;
default:
cout << "value is not 0 or 1";
}
}
While loop:
int n = 0;
while (n < 10)
{
cout << " n: " << n << endl;
n = n + 1;
}
Do while loop:
do {
cout << "Enter number (0 to end): ";
cin >> n;
cout << "You entered: " << n << "\n";
} while (n != 0);
For loop:
for (unsigned int n = 0; n < 10; ++n)
{
cout << "n: " << n << endl;
}
For loop with two variables:
for (unsigned int i = 0, j = 0; i < 10 && j < 10; ++i, j+=2)
{
cout << "i:" << i << ", j:" << j << endl;
}
The enum
(enumerated) type is used to define collections of named integar constants.
enum CurrencyType {USD, EUR, GBP};
cout << USD << " " << EUR << " " << GBP;
0 1 2
enum CurrencyType {USD, EUR=10, GBP};
cout << USD << " " << EUR << " " << GBP;
0 10 11
A class
achieve data abstraction
and encapsulation
.
Define a customer class with member variable and function.
class Customer
{
public:
Customer(); // default constructor
Customer(string name, string address);
~Customer(); // destructor, to free up resources
string GetName();
string GetAddress();
void SetAddress(string address);
private:
string name_;
string address_;
};
Instantiate Customer class instances to represent different customer.
Customer c1("Joe", "Hyde Park");
Customer c2("Jim", "Chicago");
Customer c3("John", "New York");
// Use `.` to access member function.
c1.GetName()
c2.SetAddress("Beijing")
There are three protection levels to keep class data member internal to the class.
inherit
from that class.A constructor
is a special member functions used to initialize the data members when an object is created. This is an example to use initializer list
to create more efficient constructors
Customer::Customer()
: name_(""),
address_("")
{
// name_ = "";
// address_ = "";
}
Customer::Customer(string name, string address)
: name_(name),
address_(address)
{}
Customer::~Customer()
{}
There are several ways to create objects on a computer:
Automatic/Stack int a;
Dynamic Allocated
int* ptr = new a[10];
malloc/free
Summarized in a table from geeksforgeeks
Parameter | Stack | Heap |
---|---|---|
Basic | Memory is allocated in a contiguous block | Memory is allocated in any random order |
Allocated and de-allocation | Automatic by compiler instructions | Manual by programmer |
Cost | Less | More |
Access time | Faster | Slower |
Main issue | Shortage of memory | Memory leak/fragmentation |
We use ->
to access free-store object’s member functions:
Customer* c = new Customer("Joe", "Chicago");
c->GetName()
c->SetAddress("New York")
A const object
can only invoke const member function
on the class. A const member function is not allowed to modify any of the data members on the object on which it is invoked. However, if a data member is marked mutable
, it then can be modified inside a const member function.
const Customer c1("Joe", "Hyde Park");
cout << c1.GetName(); // ok if GetName() is a const member function.
We use static
keyword to associate a member with the class, as oppose to class instances. A static data member can NOT be accessed directly using a non-static member function.
Static member variables can NOT be initialized through the class constructor, rather, they are initialized once outside the class body. However, a const static member variable can be initialized within the class body.
class Counter
{
public:
Counter();
static int GetCount();
static void Increment();
private:
static int count_; // non-const static need to be initialized outside
const static int count_2_ = 0; // const static can be initialized within
};
int Counter::count_ = 0;
Counter c;
c.Increment(); // or Counter::Increment()
Every non-static member function has access to a this
pointer, which is initialized with the address of the object when the member function is invoked.
double Currency::GetExchangeRate()
{
return exchangeRate_;
return this->exchangeRate_; // equivalent
return (*this).exchangeRate_; // equivalent
}
We use the copy constructor to construct an object from another already constructed object of the same type.
class Customer
{
Customer(const Customer& other);
};
Customer::Customer(const Customer& other)
: name_(other.name_)
address_(other.address_)
{}
Customer c2(c1);
We use the assignment operator to assign an object of the same type.
class Customer
{
Customer& operator=(const Customer& other);
};
Customer& Customer::operator=(const Customer& other)
{
if (this != &other) //checking for self assignment
{
name_ = other.name_;
address_ = other.address_;
}
//return the object on which the function was invoked
return (*this);
}
The default copy constructor and assignment operator provides shallow copy
, which copies each member of the class individually. For pointer member, the shallow copying copies the address of the pointer, resulting in both members pointing to the same object on the free store.
A deep copy
, however, creates a new object on the free store and copy the contents of the object the original pointer is pointing to.
Deep Copy copy constructor
Customer::Customer(const Customer& other)
:name_(other.name_),
address_(other.address_),
account_(new Account(other.account_->GetAccountNumber(),
other.account_->GetAccountBalance()))
{}
Deep Copy assignment operator
Customer& Customer::operator=(const Customer& other)
{
if (this != &other)
{
name_ = other.name_;
address_ = other.address_;
delete account_;
account_= new Account(other.account_->GetAccountNumber(),
other.account_->GetAccountBalance());
}
return (*this);
}
There are 3 operations that control the copies of an object: copy constructor, assignment operator, and destructor. If you define one of them, you will most likely need to define the other two as well.
The Singleton
design pattern makes sure only one instance of an object of a given type is instantiated in a program, and provides a global point of access to it
Instance()
to create the objectclass CurrencyFactory
{
public:
static CurrencyFactory* Instance();
Currency CreateCurrency(int currencyType);
private:
CurrencyFactory();
static CurrencyFactory* instance_;
};
CurrencyFactory* CurrencyFactory::Instance()
{
if (!instance_)
instance_ = new CurrencyFactory;
return instance_; // no more than one CurrencyFactory object.
}
Currency CurrencyFactory::CreateCurrency(int currencyType)
{
switch(currencyType)
{
case EUR:
return Currency("EUR", 0.7901);
case GBP:
return Currency("GBP", 0.6201);
case CAD:
return Currency("CAD", 1.1150);
case AUD:
return Currency("AUD", 1.1378);
default:
return Currency("USD", 1.0);
}
}
#include "CurrencyFactory.h"
int main()
{
cout << "Enter amount in USD:";
double amount;
cin >> amount;
cout << "Enter currency to convert to (ECU/GBP/CHF/JPY): ";
string symbol;
cin >> symbol;
double convertedAmount = 0.0;
Currency currency = CurrencyFactory::Instance()->CreateCurrency(symbol);
cout << currency.ConvertFromUSD(amount) << endl;
}
Classes related by inheritance
form a hierachy consisting of base and derived classes. The derived
class inherit some members from the base class subject to protection level restrictions, and may extend/override implementation of member functions in the base class.
class Person
{
protected:
string name_;
string address_;
};
class Student : public Person
{
string school_;
};
Different derived classes may inplement member functions from the base class differently. The base class uses virtual
keyword to indicate a member function that may be specialized by derived classes.
class Base
{
public:
virtual void Method1();
virtual void Method2();
void Method3();
};
class Derived : public Base
{
void Method1(); // specializes Method1()
// uses default implementation of Method2()
// can NOT specialize Method3()
};
The base class has to
either provide a default implementation for that function or declare it pure virtual
. If a class has one or more pure virtual function, it is called an abstract class
or interface
. An abstract class cannot be instantiated.
class Base
{
public:
virtual void Method1() = 0;
};
class Derived : public Base
{
// this derived is also an abstract
};
When we delete a derived class we should execute both the derived class destructor and the base class destructor. A virtual base class destructor
is needed to make sure the destructors are called properly when a derived class object is deleted through a pointer to a base class
.
If we delete a derived class object through a pointer to a base class when the base class destructor is non-virtual, the result is undefined
.
The types related by inheritance are known as polymorphic
. types. We can use polymorphic types interchangeably.
We can use a pointer
or a reference
to a base class object to point to an object of a derived class – this is known as the Liskov Substitution Principle
(LSP). This allows us to write code without needing to know the dynamic type of an object
BankAccount* acc1 = new Savings();
acc1->ApplyInterest(); // ApplyInterest() on the Savings object
BankAccount* acc2 = new Checking();
acc2->ApplyInterest(); // ApplyInterest() on the Checking object
We can write one function which applies to all account types.
void UpdateAccount(BankAccount* acc)
{
acc->ApplyBankingFees();
acc->ApplyInterest();
}
void UpdateAccount(BankAccount& acc)
{
acc.ApplyBankingFees();
acc.ApplyInterest();
}
The STL array class from offers a more efficient and reliable alternative for C-style arrays, where size is known and we do not have to pass size of array as separate parameter.
#include <array>
array <int> a1 = {1, 2, 3};
a1.front();
a1.back();
a1.size();
a1.at(1);
get<1>(a1);
Vectors are the stored contiguously same as dynamic arrays with the ability to resize itself automatically when an element is inserted or deleted. Vector size is double whenever half is reached.
#include <vector>
vector<int> v1;
v1.begin();
v1.end();
v1.size();
v1.push_back(); // pushes the elements into a vector from the back
v1.pop_back(); // removes the elements from a vector from the back.
v1.insert(i); // inserts new elements before the element at the specified position
v1.assign(i); // assigns new value to the vector elements by replacing old ones
v1.erase(i); // removes elements from a container from the specified position or range
Different from arrays and vectors, A list is a sequential container that allows non-contiguous memory allocation.
#include <list>
list<int> l1;
for (int i = 0; i < 10; i++) {
l1.front(); // returns the value of the first element
l1.back(); // returns the value of the last element
l1.push_front(i); // adds a new element ‘i’ at the beginning of the list
l1.push_back(i); // adds a new element ‘i’ at the back of the list
l1.pop_front(); // removes the first element and reduces list size by 1
l1.pop_back(); // removes the last element and reduces list size by 1
l1.begin(); // returns an iterator pointing to the first element of the list
l1.end(); // returns an iterator pointing to the last element of the list
}
The STL string class stores the characters as a sequence of bytes, allowing access to single byte character. Any string is terminated by a \0
, so the string foo
actually stores four characters.
The use sizeof()
to return the size of an array in bytes. Use .size()
member function to return the number of elements in a STL container.
#include <iostream>
#include <vector>
using namespace std;
int main() {
int a[5] {1, 2, 3, 4, 5};
cout << "The size of a: " << sizeof(a) << " bytes" << endl;
vector<int> b {1, 2, 3, 4, 5};
cout << "The size of b: " << sizeof(b) << " bytes" << endl;
cout << "The size of b: " << b.size() << " elements" << endl;
}
The size of a: 20 bytes
The size of b: 24 bytes
The size of b: 5 elements
Sets are an associative container where each element is unique. The value of the element cannot be modified once it is added to the set.
#include <set>
set<int> s1;
for (int i = 0; i < 10; i++) {
s1.begin();
s1.end();
s1.size();
s1.insert(i);
s1.erase(i);
s1.find(i);
}
A std::map
sorts its elements by the keys.
The STL provides implementations of some widely used algorithms.
int main()
{
vector<int> values{10, 1, 22, 12, 2, 7};
//sort takes a range
sort(values.begin(), values.end());
}
int main()
{
vector<int> values{10, 1, 22, 12, 2, 7};
//binary_search takes a range and a value
bool found = binary_search(values.begin(), values.end(), 12);
}
int main()
{
vector<int> values1{ 10, 1, 22, 12, 2, 7 };
//destination
vector <int> values2;
copy(values1.begin(), values1.end(), //input range
back_inserter(values2)); //output iterator
}
int main()
{
vector<int> values{ 10, 1, 22, 12, 2, 7 };
replace(values.begin(), values.end(), //range
1, //old value
111); //new value
}
int main()
{
vector<int> v2{ 5, 4, 3, 2, 1 };
vector<int> v2{ 1, 2, 3, 4, 5 };
int r1 = accumulate(v1.begin(), v1.end(), 0); //range
int r2 = inner_product(v1.begin(), v1.end(),
v2.begin(), 0);
}
A unique pointer
takes unique ownership in its pointed object
. The unique pointer delete the object they managed either when the unique pointer is destroyed or when the object’s value changes.
#include <memory>
std::unique_ptr<Option> sp(new Option());
// initates a smart pointer (or through reset: sp.resert(Option()).)
std::unique_ptr<Option> sp2(sp);
// error: does not allow two reference (sp, sp2) to the same object (new Option());
std::unique_ptr<Option> sp2(std::move(sp));
// now sp is destroyed and sp2 takes ownership of the Option object
sp2->getPrice();
// smart pointer can be used as regular pointer
The shared pointer
counts the reference to its pointed object and can store and pass a reference beyond the scope of a function. In OOP, the share pointer is used to store a pointer as a member variable and can be used to reference value outside the scope of the class.
std::share_ptr<Option> sp2;
{
std::share_ptr<Option> sp(new Option());
sp2=sp;
}
sp2->getPrice();
// the Option object is not deleted after local scope ends
Creating a vector of shared_ptr:
#include <vector>
std::vector<std::shared_ptr<Option>> option_list;
for (int i=0; i< 10; i++) {
option_list.push_back(std::shared_ptr<Option>(new Option(i)));
}
A weak_ptr
works the same as shared pointer
, but will not increment the reference count.
std::weak_ptr<Option> sp2;
{
std::share_ptr<Option> sp(new Option());
sp2=sp;
}
sp2->getPrice(); // error! the Option object does not exist beyond scope.
A thread
is a small sequence of programmed instruction and is usually a component of a process
. Multi-threading
can exist within one process, executing concurrently
and share resources such as memory, while processes do not share their resources.
The std::thread
class in c++ supports multi-threading, and can be initiated to represent a single thread. We need to pass a callable object (function pointer, function, or lambda) to the constructor of the std::thread class. We use the std::thread.join()
method to wait for the copmletion of a thread.
Here we initiate two threads. Both threads share memory and attempt to modify the balance
variable at the same time which lead to concurrency issue.
#include <iostream>
#include <thread>
using namespace std;
int main() {
int balance = 0;
// t1 starts
thread t1([&balance] {for (int i=0; i<1000000; i++) {balance++;}});
// t2 starts
thread t2([&balance] {for (int i=0; i<1000000; i++) {balance--;}});
t1.join(); // the main() waits here until t1 completes
t2.join(); // the main() waits here until t2 completes
cout << balance << endl;
cout << "END OF CODE" << endl;
}
153258
END OF CODE
We introduce the an mutex
, or mutual exclusive, object, which contains a unique id for the resources allocated to the program. A thread can lock
the resource by a std::mutex.lock()
method, which prevent other thread from sharing the resource until the mutex becomes unlocked.
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int main() {
int balance = 0;
mutex m;
// t1 starts
thread t1([&balance, &m] {for (int i=0; i<1000000; i++) {
m.lock();
balance++;
m.unlock();
}});
// t2 starts
thread t2([&balance, &m] {for (int i=0; i<1000000; i++) {
m.lock();
balance--;
m.unlock();
}});
t1.join(); // the main() waits here until t1 completes
t2.join(); // the main() waits here until t2 completes
cout << balance << endl;
cout << "END OF CODE" << endl;
}
0
END OF CODE
A condition variable
is an object that can block the calling thread until notified to resume. It uses a unique_lock
(over a mutex
) to lock the thread when one of its wait
functions is called.
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
using namespace std;
mutex m;
condition_variable cv;
vector<int> v;
bool ready = false;
bool processed = false;
void make_vector() {
unique_lock<std::mutex> lk(m); // own the mutex
cv.wait(lk, []{return ready;}); // wait until main() sends data
for (int k = 0; k < 10; ++k) {
v.push_back(k);
}
processed = true;
lk.unlock(); // manual unlocking is done before notifying
cv.notify_one();
// unblocks one of the threads currently waiting for this condition
// if no threads are waiting, the function does nothing
// if more than one threads are waiting, it is unspecified which will be selected
}
int main() {
thread t(make_vector);
ready = false;
processed = false;
{
cout << "main() signals ready for processing\n";
ready = true;
}
cv.notify_one();
{
unique_lock<std::mutex> lk(m); // own the mutex
cv.wait(lk, []{return processed;}); // wait for cv.notify_one
cout << "back to main(), vector is processed\n";
}
for (auto i : v)
{
cout << i << " ";
}
t.join();
}
main() signals ready for processing
back to main(), vector is processed
0 1 2 3 4 5 6 7 8 9