## constexpr

In [ ]:
constexpr int factorial(int n) {
return n <= 1 ? 1 : (n * factorial(n - 1));
}

# loops and local variables since c++14
constexpr int loop(int i) {
if(i <= 1) return i;

int sum = 0;
for(int j = 0; j < i; ++j) {
sum += j;
}

return sum;
}

int main() {
constexpr auto s = factorial(5);
int array[s];
std::cout << s << "\n";

constexpr auto sum = loop(10);
std::cout << sum << "\n";
}


## declare a temporary variable in the if and switch statements

In [ ]:
int main() {
std::vector<int> vec = {1, 2, 3, 4};

if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) {
*itr = 4;
}

# after c++17, can be simplefied by using auto
if (auto itr = std::find(vec.begin(), vec.end(), 2); itr != vec.end()) {
std::cout << *itr << "\n";
*itr = 4;
}
}


## Structured binding

In [ ]:
std::tuple<int, double, std::string> f() {
return std::make_tuple(1, 2.3, "Hello");
}

int main() {
auto [x, y, z] = f();
std::cout << x << ", " << y << ", " << z << "\n";
}


## Type inference

In [ ]:
template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y;
}

# c++11
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y) {
return x + y;
}

# c++14
template<typename T, typename U>
auto add3(T x, U y) {
return x + y;
}

int main () {
# before C++11
std::vector<int> vec = {1, 2, 3, 4};
std::vector<int>::const_iterator it = vec.cbegin();

# using auto
auto it2 = vec.cbegin();

auto i = 5;              // i as int
auto arr = new auto(10); // arr as int *

# decltype

auto x = 1;
auto y = 2;
decltype(x + y) z;

if (std::is_same<decltype(x), int>::value)
std::cout << "type x == int" << std::endl;
if (std::is_same<decltype(x), float>::value)
std::cout << "type x == float" << std::endl;
if (std::is_same<decltype(x), decltype(z)>::value)
std::cout << "type z == type x" << std::endl;

# tail type inference

auto q = add3<double, int>(1.0, 2);
std::cout << "q: " << q << "\n";
}


## Control flow

In [ ]:
template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) {
return t + 1;
} else {
return t + 0.001;
}
}

int main() {
# if constexpr
std::cout << print_type_info(5) << "\n";
std::cout << print_type_info(3.14) << "\n";

# ranged based for loop
std::vector<int> vec = {1, 2, 3, 4};

if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end())
*itr = 4;

for (auto element : vec)
std::cout << element << "\n"; # read only
for (auto &element : vec) {
element += 1;               # writeable
}
}


## Templates

In [ ]:
# default parameters of the template:
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}

template<typename... Ts> class Magic;

# define one parameter
template<typename Require, typename... Args> class Magic2;

template<typename... Ts>
void magic_func(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}

# Recursive template function
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}

template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << "\n";
printf1(args...);
}

# Variable parameter template expansion
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << "\n";
if constexpr (sizeof...(t) > 0) printf2(t...);
}

# Initialize list expansion
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << "\n";
(void) std::initializer_list<T>{ ([&args] {
std::cout << args << "\n";
}(), value)...};
}

# Fold expression
template<typename ... T>
auto sum(T ... t) {
return (t + ...);
}

# Non-type template parameter deduction
template <typename T, int BufSize>
class buffer_t {
public:
T& alloc();
void free(T& item);
private:
T data[BufSize];
};

template <auto value> void foo() {
std::cout << value << "\n";
return;
}

int main () {
# default parameters of the template:
# normal
auto s2 = add<double, double>(2.1, 2.1);

std::cout << s << "  " << s2 << "\n";

magic_func();      # 0
magic_func(1);     # 1
magic_func(1, "Hello"); # 2

# Recursive template function
printf1(1, 2, "Good", 1.1);

# Variable parameter template expansion
printf2(1, 2, "Better", 1.1);

# Initialize list expansion
printf3(1, 2, "Best", 1.1);

# Fold expression
std::cout << "Sum: " << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << "\n";

# Non-type template parameter deduction
buffer_t<int, 100> buf; # 100 as template parameter

foo<10>();
foo<'D'>();
}


## Object-oriented

In [ ]:
struct Base {
virtual void foo(int);

virtual void foo2() final;
};

struct SubClass: Base {
virtual void foo(int) override; # legal
#virtual void foo(float) override; # illegal, no virtual function in super class
#void foo2(); # illegal, foo has final

public:
SubClass() = default; # explicit let compiler use default constructor
SubClass& operator=(const SubClass&) = delete; # explicit declare refuse constructor
};

int main () {
# Strongly typed enumerations
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};

if (new_enum::value3 == new_enum::value4) { # true
std::cout << "new_enum::value3 == new_enum::value4" << "\n";
}
}


## Lambda expression

In [ ]:
# C++14 campture rvalues
void lambda_expression_capture() {
auto important = std::make_unique<int>(1);
auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
return x+y+v1+(*v2);
};
}

# Generic Lambda
# Lambda expressions can use auto parameters
void lambda_generic() {
auto generic = [](auto x, auto y) {
return x+y;
};

std::cout << generic(1, 2) << std::endl;
std::cout << generic(1.1, 2.2) << std::endl;
}

int main () {
int value = 1;
auto copy_value = [&value] {
return value;
};

value = 100;
auto stored_value = copy_value();
std::cout << "stored_value = " << stored_value << std::endl;
}


## Function Object Wrapper

In [ ]:
# std::function
# C++11 std::function is a generic, polymorphic function wrapper whose instances can
##store, copy, and call any target entity that can be called. It is also an existing callable to C++.

int foo(int para) {
return para;
}

# std::bind and std::placeholder
# And std::bind is used to bind the parameters of the function call. It solves the requirement
# that we may not always be able to get all the parameters of a function at one time. Through this function,
# we can Part of the call parameters are bound to the function in advance to become a new object, and then
# complete the call after the parameters are complete

int foo2(int a, int b, int c) {
return a + b + c;
}

int main () {
# std::function wraps a function that take int paremeter and returns int value
std::function<int(int)> func = foo;

int important = 10;
std::function<int(int)> func2 = [&](int value) -> int {
return 1+value+important;
};
std::cout << func(10) << std::endl;
std::cout << func2(10) << std::endl;

# std::bind and std::placeholder

# bind parameter 1, 2 on function foo, and use std::placeholders::_1 as placeholder
# for the first parameter.
auto bindFoo = std::bind(foo2, std::placeholders::_1, 1,2);
# when call bindFoo, we only need one param left
std::cout << bindFoo(1) << "\n";
}


## rvalue Reference

In [ ]:
# pvalue, pure rvalue, purely rvalue, either purely literal, such as 10, true; either the result of
# the evaluation is equivalent to a literal or anonymous temporary object, for example 1+2. Temporary
# variables returned by non-references, temporary variables generated by operation expressions, original
# literals, and Lambda expressions are all pure rvalue values.

# xvalue, expiring value is the concept proposed by C++11 to introduce rvalue references (so in traditional C++,
# pure rvalue and rvalue are the same concept), that is, A value that is destroyed but can be moved.

# To get a xvalue, you need to use the declaration of the rvalue reference: T &&, where T is the type.
# The statement of the rvalue reference extends the lifecycle of this temporary value, and as long as the
# variable is alive, the xvalue will continue to survive.

# C++11 provides the std::move method to unconditionally convert lvalue parameters to rvalues. With it we can
# easily get a rvalue temporary object

void reference(int& v) {
std::cout << "lvalue reference" << std::endl;
}

void reference(int&& v) {
std::cout << "rvalue reference" << std::endl;
}

template <typename T>
void pass(T&& v) {
std::cout << "          normal param passing: ";
reference(v);
std::cout << "       std::move param passing: ";
reference(std::move(v));
std::cout << "    std::forward param passing: ";
reference(std::forward<T>(v));
std::cout << "static_cast<T&&> param passing: ";
reference(static_cast<T&&>(v));
}

int main () {
std::cout << "rvalue pass:" << std::endl;
pass(1);

std::cout << "lvalue pass:" << std::endl;
int l = 1;
pass(l);
}


## containers

In [ ]:
int main () {
# std::array

std::array<int, 4> arr = {1, 2, 3, 4};

arr.empty(); # check if container is empty
arr.size();  # return the size of the container

# iterator support
for (auto &i : arr) {
# ...
}

# use lambda expression for sort
std::sort(arr.begin(), arr.end(), [](int a, int b) {
return b < a;
});

# array size must be constexpr
constexpr int len = 4;
std::array<int, len> arr2 = {1, 2, 3, 4};

# When we started using std::array, it was inevitable that we would encounter a C-style compatible
# interface. There are three ways to do this:

int* p = &arr;
int* pp = arr.data();

# std::tuple (std::pair can save only two parameter)

# std::make_tuple: construct tuple
# std::get: Get the value of a position in the tuple
# std::tie: tuple unpacking

auto student = std::make_tuple(3.8, 'A', "John");

std::cout << "ID: 0, "
<< "GPA: "   << std::get<0>(student) << ", "
<< "Grade: " << std::get<1>(student) << ", "
<< "Name: "  << std::get<2>(student) << '\n';

double gpa;
std::string name;

# unpack tuples
std::tie(gpa, grade, name) = std::make_tuple(2.8, 'B', "Balazs");
std::cout   << "ID: 1, "
<< "GPA: "   << gpa << ", "
<< "Name: "  << name << '\n';

# Merge and Iteration,  Another common requirement is to merge two tuples, which can be done with
# std::tuple_cat:
std::tuple<std::string, double, double, int> t("123", 4.5, 6.7, 8);
auto new_tuple = std::tuple_cat(std::make_tuple(2.8, 'B', "Balazs"), std::move(t));
}


## Smart pointers

In [ ]:
# The basic idea is to count the number of dynamically allocated objects. Whenever you add a reference to
# the same object, the reference count of the referenced object is incremented once. Each time a reference is
# deleted, the reference count is decremented by one. When the reference count of an object is reduced to zero,
# the pointed heap memory is automatically deleted.

# std::shared_ptr
void foo(std::shared_ptr<int> i) {
(*i)++;
}

# unique_ptr
struct Foo {
Foo()      { std::cout << "Foo::Foo" << std::endl;  }
~Foo()     { std::cout << "Foo::~Foo" << std::endl; }
void foo() { std::cout << "Foo::foo" << std::endl;  }
};

void f(const Foo &) {
std::cout << "f(const Foo&)" << std::endl;
}

int main () {
# std::shared_ptr

# std::shared_ptr is a smart pointer that records how many shared_ptr points to an object, eliminating the
# display call delete, which automatically deletes the object when the reference count becomes zero.
# But not enough, because using std::shared_ptr still needs to be called with new, which makes the code a
# certain degree of asymmetry.
# std::make_shared can be used to eliminate the explicit use of new, so std::make_shared will allocate the
# objects in the generated parameters. And return the std::shared_ptr pointer of this object type.

{
auto pointer = std::make_shared<int>(10);
foo(pointer);
std::cout << *pointer << std::endl; // 11
# The shared_ptr will be destructed before leaving the scope
}

# std::shared_ptr can get the raw pointer through the get() method and reduce the reference count by reset().
# And see the reference count of an object by use_count()

{

auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; # reference count+1
auto pointer3 = pointer; # reference count+1
int *p = pointer.get(); # no increase of reference count
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; # 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; # 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; # 3

pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; # 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; # 0, pointer2 has reset
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; # 2
pointer3.reset();
std::cout << "reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; # 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; # 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; # 0, pointer3 has reset

}

# std::unique_ptr

# std::unique_ptr is an exclusive smart pointer that prohibits other smart pointers from sharing the same
# object, thus keeping the code safe:

std::unique_ptr<int> pointer = std::make_unique<int>(10); # make_unique was introduced in C++14
# std::unique_ptr<int> pointer2 = pointer; // illegal

# Since it is monopolized, in other words, it cannot be copied. However, we can use std::move to transfer
# it to other unique_ptr

{
std::unique_ptr<Foo> p1(std::make_unique<Foo>());

# p1 is not empty, prints
if (p1)
p1->foo();

{
std::unique_ptr<Foo> p2(std::move(p1));

# p2 is not empty, prints
f(*p2);

# p2 is not empty, prints
if(p2)
p2->foo();

# p1 is empty, no prints
if(p1)
p1->foo();

p1 = std::move(p2);

# p2 is empty, no prints
if(p2)
p2->foo();
std::cout << "p2 was destroied" << std::endl;
}

# p1 is not empty, prints
if (p1)
p1->foo();

# Foo instance will be destroied when leaving the scope
}

# std::weak_ptr

# it there is a cycle reference shared pointers cannot be released so we can solve this by using weak Pointers
# The solution to this problem is to use the weak reference pointer std::weak_ptr, which is a weak reference
# (compared to std::shared_ptr is a strong reference). A weak reference does not cause an increase in the
# reference count

# std::weak_ptr has no * operator and -> operator, so it can't operate on resources. Its only function is to
# check if std::shared_ptr exists, its expired() The method can return true when the resource is not released,
# otherwise it returns false.
}


## Regular Expression

In [ ]:
int main () {
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
# In C++, \ will be used as an escape character in the string. In order for \. to be passed as a
# regular expression, it is necessary to perform second escaping of \, thus we have \\.
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname: fnames)
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;

# Another common form is to pass in the three arguments std::string/std::smatch/std::regex. The essence
# of std::smatch is actually std::match_results. In the standard library, std::smatch is defined as
# std::match_results<std::string::const_iterator>, which means match_results of a substring iterator type.
# Use std::smatch to easily get the matching results

std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
# the first element of std::smatch matches the entire string
# the second element of std::smatch matches the first expression with brackets
if (base_match.size() == 2) {
std::string base = base_match.str();
std::cout << "sub-match: " << base_match.str() << std::endl;
std::cout << fname << " sub-match: " << base << std::endl;
}
}
}
}


## Parallelism and Concurrency

In [ ]:
int v = 1;

void critical_section(int change_v) {
static std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);

# execute contention works
v = change_v;

# mtx will be released after leaving the scope
}

void critical_section2(int change_v) {
static std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
# do contention operations
v = change_v;
std::cout << v << std::endl;
# release the lock
lock.unlock();

# during this period,
# others are allowed to acquire v

# start another group of contention operations
# lock again
lock.lock();
v += 1;
std::cout << v << std::endl;
}

std::atomic<int> count = {0};

std::atomic<int> counter = {0};

struct A {
float x;
int y;
long long z;
};

int main () {
# Basic of Parallelism

# std::thread is used to create an execution thread instance, so it is the basis for all concurrent
# programming. It needs to include the <thread> header file when using it. It provides a number of basic
# thread operations, such as get_id() to get the thread ID of the thread being created, use join() to join

std::cout << "hello world." << std::endl;
});
t.join();

# Mutex and Critical Section

# std::mutex is the most basic mutex class in C++11, and you can create a mutex by instantiating std::mutex.
# It can be locked by its member function lock(), and unlock() can be unlocked. But in the process of actually
# writing the code, it is best not to directly call the member function, Because calling member functions,
# you need to call unlock() at the exit of each critical section, and of course, exceptions. At this time,
# C++11 also provides a template class std::lock_gurad for the RAII syntax for the mutex.

# RAII guarantees the exceptional security of the code while losing the simplicity of the code.

t1.join();
t2.join();

# v is global variable now
std::cout << v << std::endl;

# Because C++ guarantees that all stack objects will be destroyed at the end of the declaration period, such
# code is also extremely safe. Whether critical_section() returns normally or if an exception is thrown in the
# middle, a stack rollback is thrown, and unlock() is automatically called.

# And std::unique_lock is more flexible than std::lock_guard, std::unique_lock is more flexible. Objects of
# std::unique_lock manage the locking and unlocking operations on the mutex object with exclusive ownership
# (no other unique_lock objects owning the ownership of a mutex object). So in concurrent programming, it is
# recommended to use std::unique_lock.

# std::lock_guard cannot explicitly call lock and unlock, and std::unique_lock can be called anywhere after
# the declaration. It can reduce the scope of the lock and provide higher concurrency.

# If you use the condition variable std::condition_variable::wait you must use std::unique_lock as a parameter.

t3.join();
t4.join();

# Future

# std::future, which provides a way to access the results of asynchronous operations.
# Imagine if our main thread A wants to open a new thread B to perform some of our expected tasks and return
# me a result. At this time, thread A may be busy with other things, and have no time to take into account
# the results of B. So we naturally hope to get the result of thread B at a certain time.

# Before the introduction of std::future in C++11, the usual practice is: Create a thread A, start task B in
# thread A, send an event when it is ready, and save the result in a global variable. The main function thread
# A is doing other things. When the result is needed, a thread is called to wait for the function to get the
# result of the execution.

# The std::future provided by C++11 simplifies this process and can be used to get the results of asynchronous
# tasks. Naturally, we can easily imagine it as a simple means of thread synchronization, namely the barrier.

# pack a lambda expression that returns 7 into a std::packaged_task
# get the future of task
std::cout << "waiting...";
result.wait(); // block until future has arrived
# output result
std::cout << "done!" << std:: endl << "future result is " << result.get() << std::endl;

# Condition Variable

# The condition variable std::condition_variable was born to solve the deadlock and was introduced when the
# mutex operation was not enough. For example, a thread may need to wait for a condition to be true
# to continue execution. A dead wait loop can cause all other threads to fail to enter the critical section
# so that when the condition is true, a deadlock occurs. Therefore, the condition_variable instance is created
# primarily to wake up the waiting thread and avoid deadlocks. notd_one() of std::condition_variable is used
# to wake up a thread; notify_all() is to notify all threads.

std::queue<int> produced_nums;
std::mutex mtx;
std::condition_variable cv;
bool notified = false;  # notification sign

auto producer = [&]() {
for (int i = 0; ; i++) {
std::unique_lock<std::mutex> lock(mtx);
std::cout << "producing " << i << std::endl;
produced_nums.push(i);
notified = true;
cv.notify_all();
}
};

auto consumer = [&]() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
while (!notified) {  # avoid spurious wakeup
cv.wait(lock);
}

# temporal unlock to allow producer produces more rather than
# let consumer hold the lock until its consumed.
lock.unlock();
lock.lock();
if (!produced_nums.empty()) {
std::cout << "consuming " << produced_nums.front() << std::endl;
produced_nums.pop();
}
notified = false;
}
};

for (int i = 0; i < 2; ++i) {
}

p.join();

for (int i = 0; i < 2; ++i) {
cs[i].join();
}

# Atomic Operation and Memory Model

# std::mutex can solve the problem of concurrent read and write, but the mutex is an operating system level
# function. This is because the implementation of a mutex usually contains two basic principles:

# Provide automatic state transition between threads, that is, "lock" state
# Ensure that the memory of the manipulated variable is isolated from the critical section during the mutex
# operation
# This is a very strong set of synchronization conditions, in other words, when it is finally compiled into a
# CPU instruction, it will behave as a lot of instructions (we will look at how to implement a simple mutex
# later). This seems too harsh for a variable that requires only atomic operations (no intermediate state).

# The research on synchronization conditions has a very long history, and we will not go into details here.
# Readers should understand that under the modern CPU architecture, atomic operations at the CPU instruction
# level are provided. Therefore, in the C + + 11 multi-threaded shared variable reading and writing, the
# introduction of the std::atomic template, so that we instantiate an atomic type, will be a Atomic type read
# and write operations are minimized from a set of instructions to a single CPU instruction. E.g:

# And provides basic numeric member functions for atomic types of integers or floating-point numbers, for
# example, Including fetch_add, fetch_sub, etc., and the corresponding +, - version is provided by overload.

{
# count is a global variable
});
count += 1;     # identical to fetch_add
});
t1.join();
t2.join();
std::cout << count << std::endl;
}

std::atomic<A> a;
std::cout << std::boolalpha << a.is_lock_free() << std::endl;

# Memory Orders

# In order to achieve the ultimate performance and achieve consistency of various strength requirements,
# C++11 defines six different memory sequences for atomic operations. The option std::memory_order expresses
# four synchronization models between multiple threads

# Relaxed model:
# Under this model, atomic operations within a single thread are executed sequentially, and instruction
# reordering is not allowed, but the order of atomic operations between different threads is arbitrary. The
# type is specified by std::memory_order_relaxed

{
for (int i = 0; i < 100; ++i) {
vt.emplace_back([](){
});
}

for (auto& t : vt) {
t.join();
}
std::cout << "current counter:" << counter << std::endl;
}

# Release/consumption model:
# In this model, we begin to limit the order of operations between processes. If a thread needs to modify a
# value, but another thread will have a dependency on that operation of the value, that is, the latter depends.
# former. Specifically, thread A has completed three writes to x, and thread B relies only on the third x write
# operation, regardless of the first two write behaviors of x, then A When active x.release() (ie using
# std::memory_order_release), the option std::memory_order_consume ensures that B observes A when calling
# x.load() Three writes to x.

{
std::atomic<int*> ptr;
int v;
int* p = new int(42);
v = 1024;
ptr.store(p, std::memory_order_release);
});
int* p;

std::cout << "p: " << *p << std::endl;
std::cout << "v: " << v << std::endl;
});
producer.join();
consumer.join();
}

# Release/Acquire model:
# Under this model, we can further tighten the order of atomic operations between different threads,
# specifying the timing between releasing std::memory_order_release and getting std::memory_order_acquire.
# All write operations before the release operation are visible to any other thread, ie, happens-before.

# As you can see, std::memory_order_release ensures that the write behavior after it does not occur before
# the release operation, is a backward barrier, and std::memory_order_acquire ensures the previous write
# behavior after it, no It will happen after the get operation, it is a forward barrier. For the option
# std::memory_order_acq_rel, it combines the characteristics of the two, and only determines a memory
# barrier, so that the current thread reads and writes to the memory. Will not be rearranged before and after
# this operation.

{
std::vector<int> v;
std::atomic<int> flag = {0};
v.push_back(42);
flag.store(1, std::memory_order_release);
});
int expected = 1; # must before compare_exchange_strong
while(!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
expected = 1; # must after compare_exchange_strong
}
# flag has changed to 2
});

std::cout << v.at(0) << std::endl; # must be 42
});
release.join();
acqrel.join();
acquire.join();
}

# In this case we used compare_exchange_strong, which is the Compare-and-swap primitive, which has a weaker
# version, compare_exchange_weak, which allows a failure to be returned even if the exchange is successful.
# The reason is due to a false failure on some platforms, specifically, when the CPU performs a context
# of compare_exchange_strong may be slightly worse than compare_exchange_weak, but in most cases,
# compare_exchange_strong should be limited.

# Sequential Consistent Model:
# Under this model, atomic operations satisfy sequence consistency, which in turn can cause performance loss.
# It can be specified explicitly by std::memory_order_seq_cst
# This example is essentially the same as the first loose model example. Just change the memory order of the
# atomic operation to memory_order_seq_cst

{
for (int i = 0; i < 100; ++i) {
vt.emplace_back([](){