Modern C++
Programming
19. Advanced Topics II
Federico Busato
2024-03-29
Table of Contents
1 Undeﬁned Behavior
Illegal Behavior
Platform Speciﬁc Behavior
Unspeciﬁed Behavior
Detecting Undeﬁned Behavior
1/56
Table of Contents
2 Error Handing
Recoverable Error Handing
Return Code
C++ Exceptions
Deﬁning Custom Exceptions
noexcept Keyword
Memory Allocation Issues
Return Code and Exception Summary
std::expected
Alternative Error Handling Approaches
2/56
Table of Contents
3 Smart pointers
std::unique ptr
std::shared ptr
std::weak ptr
4 Concurrency
Thread Methods
Mutex
Atomic
Task-based parallelism
3/56
Undeﬁned Behavior
Undeﬁned Behavior Overview
Undeﬁned behavior means that the semantic of certain operations is
• Unspeciﬁed behavior: outside the language/library speciﬁcation, two or more choices
• Illegal: the compiler presumes that such operations never happen, e.g. integer overﬂow
• Implementation-deﬁned behavior: depends on the compiler and/or platform (not portable)
Motivations behind undeﬁned behavior:
• Compiler optimizations, e.g. signed overﬂow or NULL pointer dereferencing
• Simplify compile checks
• Unfeasible/expensive to check
• What Every C Programmer Should Know About Undefined Behavior, Chris Lattner
• What are all the common undefined behaviors that a C++ programmer should know
about?
• Enumerating Core Undefined Behavior
4/56
Illegal Behavior 1/3
• const cast applied to a const variables
const int var = 3;
const_cast<int&>(var) = 4;
... // use var
• Memory alignment
char* ptr = new char[512];
auto ptr2 = reinterpret_cast<uint64_t*>(ptr + 1);
ptr2[3]; // ptr2 is not aligned to 8 bytes (sizeof(uint64_t))
• Memory initialization
int var; // undefined value
auto var2 = new int; // undefined value
• Memory access-related: Out-of-bound access: the code could crash or not
depending on the platform/compiler
5/56
Illegal Behavior 2/3
• Strict aliasing
float x = 3;
auto y = reinterpret_cast<unsigned&>(x);
// x, y break the strict aliasing rule
• Lifetime issues
int* f() {
int tmp[10];
return tmp;
}
int* ptr = f();
ptr[0];
• One Deﬁnition Rule violation
- Diﬀerent deﬁnitions of inline functions in distinct translation units
6/56
Illegal Behavior 3/3
• Missing return statement
int f(float x) {
int y = x * 2;
}
• Dangling reference
iint n = 1;
const int& r = std::max(n-1, n+1); // dagling
// GCC 13 experimental -Wdangling-reference (enabled by -Wall)
• Illegal arithmetic and conversion operations
- Division by zero 0 / 0 , fp value / 0.0
- Floating-point to integer conversion
7/56
Platform Speciﬁc Behavior
• Memory access-related: NULL pointer dereferencing: the 0x0 address is valid
in some platforms
• Endianness
union U {
unsigned x;
char y;
};
• Type deﬁnition
long x = 1ul << 32u; // different behavior depending on the OS
• Intrinsic functions
8/56
Unspeciﬁed Behavior 1/2
Legal operations but the C++ standard does not document the result → diﬀerent
compilers/platforms can show diﬀerent behavior
• Signed shift -2 ≪ x (before C++20), large-than-type shift 3u ≪ 32 , etc.
• Floating-point narrowing conversion between ﬂoating-point types
double → float
• Arithmetic operation ordering f(i++, i++)
• Function evaluation ordering
auto x = f() + g(); // C++ doesn't ensure that f() is evaluated before g()
9/56
Unspeciﬁed Behavior 2/2
• Signed overﬂow
for (int i = 0; i <= N; i++)
if N is INT MAX , the last iteration is undeﬁned behavior. The compiler can assume that
the loop is ﬁnite and enable important optimizations, as opposite to unsigned (wrap
around)
• Trivial inﬁnite loops, until C++26
int main() {
while (true) // -> std::this_thread::yield(); in C++26
;
}
void unreachable() { cout << "Hello world!" << endl; }
the code print Hello world! with some clang versions
P2809R3: Trivial infinite loops are not Undefined Behavior
10/56
Detecting Undeﬁned Behavior
There are several ways to detect or prevent undeﬁned behavior at compile-time and at
run-time:
• Modify the compiler behavior, see Debugging and Testing: Hardening Techniques
• Using undeﬁned behavior sanitizer, see Debugging and Testing: Sanitizer
• Static analysis tools
• constexpr expressions doesn’t allow undeﬁned behavior
constexpr int x1 = 2147483647 + 1; // compile error
constexpr int x2 = (1 << 32); // compile error
constexpr int x3 = (1 << -1); // compile error
constexpr int x4 = 3 / 0; // compile error
constexpr int x5 = *((int*) nullptr) // compile error
constexpr int x6 = 6
constexpr float x7 = reinterpret_cast<float&>(x6); // compile error
Exploring Undefined Behavior Using Constexpr
11/56
Error Handing
Recoverable Error Handing
Recoverable Conditions that are not under the control of the program. They indicate
“exceptional” run-time conditions. e.g. ﬁle not found, bad allocation, wrong user
input, etc.
A recoverable should be considered unrecoverable if it is extremely rare and diﬃcult to
handle, e.g. bad allocation due to out-of-memory error
The common ways for handling recoverable errors are:
Exceptions Robust but slower and requires more resources
Return code Fast but diﬃcult to handle in complex programs
12/56
Error Handing References
• Modern C++ best practices for exceptions and error handling
• Back to Basics: Exceptions - CppCon2020
• ISO C++ FAQ: Exceptions and Error Handling
• Zero-overhead deterministic exceptions: Throwing values, P0709
• C++ exceptions are becoming more and more problematic, P2544
• std::expected
13/56
Return Code
Historically, C programs handled errors with return codes, even for unrecoverable errors
enum Status { IllegalValue, Success };
Status f(int* ptr) { return (ptr == nullptr) ? IllegalValue : Success; }
Why such behavior? Debugging → need to understand what / where / why the
program failed
A better approach in C++ involves
std::source location() C++20 and
std::stacktrace() C++23
ABI related issues:
• Removing an enumerator value is an API breaking change
• Adding a new enumerator value associated to a return type is also problematic as it
causes ABI breaking change
14/56
C++ Exceptions - Advantages
C++ Exceptions provide a well-deﬁned mechanism to detect errors passing the
information up the call stack
• Exceptions cannot be ignored. Unhandled exceptions stop program execution
(call std::terminate() )
• Intermediate functions are not forced to handle them. They don’t have to
coordinate with other layers and, for this reason, they provide good composability
• Throwing an exception acts like a return statement destroying all objects in the
current scope
• An exception enables a clean separation between the code that detects the error
and the code that handles the error
• Exceptions work well with object-oriented semantic (constructor)
15/56
C++ Exceptions - Disadvantages 1/2
• Code readability: Using exception can involve more code than the functionality
itself
• Code comprehension: Exception control ﬂow is invisible and it is not explicit in
the function signature
• Performance: Extreme performance overhead in the failure case (violate the
zero-overhead principle)
• Dynamic behavior: throw requires dynamic allocation and catch requires
RTTI. It is not suited for real-time, safety-critical, or embedded systems
• Code bloat: Exceptions could increase executable size by 5-15% (or more*)
*Binary size and exceptions
16/56
C++ Exceptions - Disadvantages 2/2
17/56
C++ Exception Basics
C++ provides three keywords for exception handling:
throw Throws an exception
try Code block containing potential throwing expressions
catch Code block for handling the exception
void f() { throw 3; }
int main() {
try {
f();
} catch (int x) {
cout << x; // print "3"
}
}
18/56
std Exceptions
throw can throw everything such as integers, pointers, objects, etc. The standard
way consists in using the std library exceptions <stdexcept>
# include <stdexcept>
void f(bool b) {
if (b)
throw std::runtime_error("runtime error");
throw std::logic_error("logic error");
}
int main() {
try {
f(false);
} catch (const std::runtime_error& e) {
cout << e.what();
} catch (const std::exception& e) {
cout << e.what(); // print: "logic error"
}
}
19/56
Exception Capture
NOTE: C++, diﬀerently from other programming languages, does not require explicit
dynamic allocation with the keyword new for throwing an exception. The compiler
implicitly generates the appropriate code to construct and clean up the exception
object. Dynamically allocated objects require a delete call
The right way to capture an exception is by const -reference. Capturing by-value is
also possible but, it involves useless copy for non-trivial exception objects
catch(...) can be used to capture any thrown exception
int main() {
try {
throw "runtime error"; // throw const char*
} catch (...) {
cout << "exception"; // print "exception"
}
}
20/56
Exception Propagation
Exceptions are automatically propagated along the call stack. The user can also
control how they are propagated
int main() {
try {
...
} catch (const std::runtime_error& e) {
throw e; // propagate a copy of the exception
} catch (const std::exception& e) {
throw; // propagate the exception
}
}
21/56
Deﬁning Custom Exceptions
# include <exception> // to not confuse with <stdexcept>
struct MyException : public std::exception {
const char* what() const noexcept override { // could be also "constexpr"
return "C++ Exception";
}
};
int main() {
try {
throw MyException();
} catch (const std::exception& e) {
cout << e.what(); // print "C++ Exception"
}
}
22/56
noexcept Keyword
C++03 allows listing the exceptions that a function might directly or indirectly throw,
e.g. void f() throw(int, const char*) {
C++11 deprecates throw and introduces the noexcept keyword
void f1(); // may throw
void f2() noexcept; // does not throw
void f3() noexcept(true); // does not throw
void f4() noexcept(false); // may throw
template<bool X>
void f5() noexcept(X); // may throw if X is false
If a noexcept function throw an exception, the runtime calls std::terminate()
noexcept should be used when throwing an exception is impossible or unacceptable.
It is also useful when the function contains code outside user control, e.g. std
functions/objects
23/56
Function-try-block
Exception handlers can be deﬁned around the body of a function
void f() try {
... // do something
} catch (const std::runtime_error& e) {
cout << e.what();
} catch (...) { // other exception
...
}
24/56
Memory Allocation Issues 1/4
The new operator automatically throws an exception ( std::bad alloc ) if it cannot
allocate the memory
delete never throws an exception (unrecoverable error)
int main() {
int* ptr = nullptr;
try {
ptr = new int[1000];
}
catch (const std::bad_alloc& e) {
cout << "bad allocation: " << e.what();
}
delete[] ptr;
}
25/56
Memory Allocation Issues 2/4
C++ also provides an overload of the new operator with non-throwing memory
allocation
# include <new> // std::nothrow
int main() {
int* ptr = new (std::nothrow) int[1000];
if (ptr == nullptr)
cout << "bad allocation";
}
26/56
Memory Allocation Issues 3/4
Throwing exceptions in constructors is ﬁne while it is not allowed in destructors
struct A {
A() { new int[10]; }
∼A() { throw -2; }
};
int main() {
try {
A a; // could throw "bad_alloc"
// "a" is out-of-scope -> throw 2
} catch (...) {
// two exceptions at the same time
}
}
Destructors should be marked noexcept
27/56
Memory Allocation Issues 4/4
struct A {
int* ptr1, *ptr2;
A() {
ptr1 = new int[10];
ptr2 = new int[10]; // if bad_alloc here, ptr1 is lost
}
};
struct A {
std::unique_ptr<int> ptr1, ptr2;
A() {
ptr1 = std::make_unique<int[]>(10);
ptr2 = std::make_unique<int[]>(10); // if bad_alloc here,
} // ptr1 is deallocated
};
28/56
Return Code and Exception Summary
Exception Return Code
Pros
• Cannot be ignored
• Work well with object-oriented semantic
• Information: Exceptions can be arbitrarily rich
• Clean code: Conceptually, clean separation
between the code that detects errors and the
co d e that handles the error, but. . . *
• Non-Intrusive wrt. API: Proper communication
channel
• Visibility: prototype of the called function
• No performance overhead
• No code bloat
• Easy to debug
Cons
• Visibility: Not visible without further analysis of
the code or documentation
• Clean code: *... handling exception can generate
more code than the functionality itself
• Dynamic behavior: memory and RTTI
• Extreme performance overhead in the failure case
• Code bloat
• Non-trivial to debug
• Easy to ignore, [[deprecated]] can help
• Cannot be used with object-oriented semantic
• Information: Historically, a simple integer.
Nowadays, richer error code
• Clean code: At least, an if statement after
each function call
• Non-Intrusive wrt. API: Monopolization of
the return channel
29/56
std::expected 1/2
C++23 introduces std::expected to get the best properties of return codes and
exceptions
The class template expected<T, E> contains either:
• A value of type T , the expected value type; or
• A value of type E , an error type used when an unexpected outcome occured
enum class Error { Invalid };
std::expected<int, Error> f(int v) {
if (v > 0)
return 3;
return std::unexpected(Error::Invalid);
}
30/56
std::expected 2/2
The user chooses how to handle the error depending on the context
auto ret = f(n);
// Return code handling
if (!ret)
// error handling
int v = *ret + 3; // execute without checking
// Exception handling
ret.value(); // throw an exception if there is a problem
// Monadic operations
auto lambda = [](int x) { return (x > 3) ? 4 : std::unexpected(Error::Invalid); };
ret.and_then(lambda) // pass the value to another function
.tranform([](int x) { return x + 4; };) // transform the previous value
.transform_error([](auto error_code){ /*error handling*/ };
•
31/56
Alternative Error Handling Approaches 1/2
• Global state, e.g. errno
- Easily forget to check for failures
- Error propagation using if statements and early return is manual
- No compiler optimizations due to global state
• Simple error code, e.g. int , enum , etc.
- Easily forget to check for failures (workaround [[nodiscard]] )
- Error propagation using if statements and early return is manual
- Potential error propagation through diﬀerent contexts and losing initial error
information
- Constructor errors cannot be handled
32/56
Alternative Error Handling Approaches 2/2
• std::error code , standardized error code
- Easily forget to check for failures (workaround [[nodiscard]] )
- Error propagation using if statements and early return is manual
- Code bloating for adding new enumerators (see Your own error code)
- Constructor errors cannot be handled
• Supporting libraries, e.g. Boost Outcome, STX, etc.
- Require external dependencies
- Constructor errors cannot be handled in a direct way
- Extra logic for managing return values
33/56
Smart pointers
Smart Pointers
Smart pointer is a pointer-like type with some additional functionality, e.g. automatic
memory deallocation (when the pointer is no longer in use, the memory it points to is
deallocated), reference counting, etc.
C++11 provides three smart pointer types:
• std::unique ptr
• std::shared ptr
• std::weak ptr
Smart pointers prevent most situations of memory leaks by making the memory
deallocation automatic
C++ Smart Pointers
34/56
Smart Pointers Beneﬁts
• If a smart pointer goes out-of-scope, the appropriate method to release resources
is called automatically. The memory is not left dangling
• Smart pointers will automatically be set to nullptr if not initialized or when
memory has been released
•
std::shared ptr provides automatic reference count
• If a special delete function needs to be called, it will be speciﬁed in the pointer
type and declaration, and will automatically be called on delete
35/56
std::unique ptr - Unique Pointer 1/4
std::unique ptr is used to manage any dynamically allocated object that is not
shared by multiple objects
# include <iostream>
# include <memory>
struct A {
A() { std::cout << "Constructor\n"; } // called when A()
∼A() { std::cout << "Destructor\n"; } // called when u_ptr1,
}; // u_ptr2 are out-of-scope
int main() {
auto raw_ptr = new A();
std::unique_ptr<A> u_ptr1(new A());
std::unique_ptr<A> u_ptr2(raw_ptr);
// std::unique_ptr<A> u_ptr3(raw_ptr); // no compile error, but wrong!! (not unique)
// u_ptr1 = raw_ptr; // compile error (not unique)
// u_ptr1 = u_ptr2; // compile error (not unique)
u_ptr1 = std::move(u_ptr2); // delete u_ptr1;
} // u_ptr1 = u_ptr2;
// u_ptr2 = nullptr
36/56
std::unique ptr - Unique Pointer 2/4
std::unique ptr methods
• get() returns the underlying pointer
• operator* operator-> dereferences pointer to the managed object
• operator[] provides indexed access to the stored array (if it supports random
access iterator)
• release() returns a pointer to the managed object and releases the ownership
• reset(ptr) replaces the managed object with ptr
Utility method: std::make unique<T>() creates a unique pointer to a class T
that manages a new object
37/56
std::unique ptr - Unique Pointer 3/4
# include <iostream>
# include <memory>
struct A {
int value;
};
int main() {
std::unique_ptr<A> u_ptr1(new A());
u_ptr1->value; // dereferencing
(*u_ptr1).value; // dereferencing
auto u_ptr2 = std::make_unique<A>(); // create a new unique pointer
u_ptr1.reset(new A()); // reset
auto raw_ptr = u_ptr1.release(); // release
delete[] raw_ptr;
std::unique_ptr<A[]> u_ptr3(new A[10]);
auto& obj = u_ptr3[3]; // access
}
38/56
std::unique ptr - Unique Pointer 4/4
Implement a custom deleter
# include <iostream>
# include <memory>
struct A {
int value;
};
int main() {
auto DeleteLambda = [](A* x) {
std::cout << "delete" << std::endl;
delete x;
};
std::unique_ptr<A, decltype(DeleteLambda)>
x(new A(), DeleteLambda);
} // print "delete"
39/56
std::shared ptr - Shared Pointer 1/3
std::shared ptr is the pointer type to be used for memory that can be owned by
multiple resources at one time
std::shared ptr maintains a reference count of pointer objects. Data managed by
std::shared ptr is only freed when there are no remaining objects pointing to the data
# include <iostream>
# include <memory>
struct A {
int value;
};
int main() {
std::shared_ptr<A> sh_ptr1(new A());
std::shared_ptr<A> sh_ptr2(sh_ptr1);
std::shared_ptr<A> sh_ptr3(new A());
sh_ptr3 = nullptr; // allowed, the underlying pointer is deallocated
// sh_ptr3 : zero references
sh_ptr2 = sh_ptr1; // allowed. sh_ptr1, sh_ptr2: two references
sh_ptr2 = std::move(sh_ptr1); // allowed // sh_ptr1: zero references
} // sh_ptr2: one references
40/56
std::shared ptr - Shared Pointer 2/3
std::shared ptr methods
• get() returns the underlying pointer
• operator* operator-> dereferences pointer to the managed object
• use count() returns the number of objects referring to the same managed
object
• reset(ptr) replaces the managed object with ptr
Utility method: std::make shared() creates a shared pointer that manages a new
object
41/56
std::shared ptr - Shared Pointer 3/3
# include <iostream>
# include <memory>
struct A {
int value;
};
int main() {
std::shared_ptr<A> sh_ptr1(new A());
auto sh_ptr2 = std::make_shared<A>(); // std::make_shared
std::cout << sh_ptr1.use_count(); // print 1
sh_ptr1 = sh_ptr2; // copy
// std::shared_ptr<A> sh_ptr2(sh_ptr1); // copy (constructor)
std::cout << sh_ptr1.use_count(); // print 2
std::cout << sh_ptr2.use_count(); // print 2
auto raw_ptr = sh_ptr1.get(); // get
sh_ptr1.reset(new A()); // reset
(*sh_ptr1).value = 3; // dereferencing
sh_ptr1->value = 2; // dereferencing
}
42/56
std::weak ptr - Weak Pointer 1/3
A std::weak ptr is simply a std::shared ptr that is allowed to dangle (pointer
not deallocated)
# include <memory>
std::shared_ptr<int> sh_ptr(new int);
std::weak_ptr<int> w_ptr = sh_ptr;
sh_ptr = nullptr;
cout << w_ptr.expired(); // print 'true'
43/56
std::weak ptr - Weak Pointer 2/3
It must be converted to std::shared ptr in order to access the referenced object
std::weak ptr methods
• use count() returns the number of objects referring to the same managed
object
• reset(ptr) replaces the managed object with ptr
• expired() checks whether the referenced object was already deleted (true,
false)
• lock() creates a std::shared ptr that manages the referenced object
44/56
std::weak ptr - Weak Pointer 3/3
# include <memory>
auto sh_ptr1 = std::make_shared<int>();
cout << sh_ptr1.use_count(); // print 1
std::weak_ptr<int> w_ptr = sh_ptr1;
cout << w_ptr.use_count(); // print 1
auto sh_ptr2 = w_ptr.lock();
cout << w_ptr.use_count(); // print 2 (sh_ptr1 + sh_ptr2)
sh_ptr1 = nullptr;
cout << w_ptr.expired(); // print false
sh_ptr2 = nullptr;
cout << w_ptr.expired(); // print true
45/56
Concurrency
Overview
C++11 introduces the Concurrency library to simplify managing OS threads
# include <iostream>
# include <thread>
void f() {
std::cout << "first thread" << std::endl;
}
int main(){
std::thread th(f);
th.join(); // stop the main thread until "th" complete
}
How to compile:
$g++ -std=c++11 main.cpp -pthread
46/56
Example
# include <iostream>
# include <thread>
# include <vector>
void f(int id) {
std::cout << "thread " << id << std::endl;
}
int main() {
std::vector<std::thread> thread_vect; // thread vector
for (int i = 0; i < 10; i++)
thread_vect.push_back( std::thread(&f, i) );
for (auto& th : thread_vect)
th.join();
thread_vect.clear();
for (int i = 0; i < 10; i++) { // thread + lambda expression
thread_vect.push_back(
std::thread( [](){ std::cout << "thread\n"; } );
}
}
47/56
Thread Methods 1/2
Library methods:
• std::this thread::get id() returns the thread id
• std::thread::sleep for( sleep duration )
Blocks the execution of the current thread for at least the speciﬁed sleep duration
• std::thread::hardware concurrency() returns the number of concurrent threads
supported by the implementation
Thread object methods:
• get id() returns the thread id
• join() waits for a thread to ﬁnish its execution
• detach() permits the thread to execute independently of the thread handle
48/56
Thread Methods 2/2
# include <chrono> // the following program should (not deterministic)
# include <iostream> // produces the output:
# include <thread> // child thread exit
// main thread exit
int main() {
using namespace std::chrono_literals;
std::cout << std::this_thread::get_id();
std::cout << std::thread::hardware_concurrency(); // e.g. print 6
auto lambda = []() {
std::this_thread::sleep_for(1s); // t2
std::cout << "child thread exit\n";
};
std::thread child(lambda);
child.detach(); // without detach(), child must join() the
// main thread (run-time error otherwise)
std::this_thread::sleep_for(2s); // t1
std::cout << "main thread exit\n";
}
// if t1 < t2 the should program prints:
// main thread exit
49/56
Parameters Passing
Parameters passing by-value or by-pointer to a thread function works in the same way
of a standard function. Pass-by-reference requires a special wrapper ( std::ref ,
std::cref ) to avoid wrong behaviors
# include <iostream>
# include <thread>
void f(int& a, const int& b) {
a = 7;
const_cast<int&>(b) = 8;
}
int main() {
int a = 1, b = 2;
std::thread th1(f, a, b); // wrong!!!
std::cout << a << ", " << b << std::endl; // print 1, 2!!
std::thread th2(f, std::ref(a), std::cref(b)); // correct
std::cout << a << ", " << b << std::endl; // print 7, 8!!
th1.join(); th2.join();
}
50/56
Mutex (The Problem) 1/3
The following code produces (in general) a value < 1000:
# include <chrono>
# include <iostream>
# include <thread>
# include <vector>
void f(int& value) {
for (int i = 0; i < 10; i++) {
value++;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
int value = 0;
std::vector<std::thread> th_vect;
for (int i = 0; i < 100; i++)
th_vect.push_back( std::thread(f, std::ref(value)) );
for (auto& it : th_vect)
it.join();
std::cout << value;
}
51/56
Mutex 2/3
C++11 provides the mutex class as synchronization primitive to protect shared data
from being simultaneously accessed by multiple threads
mutex methods:
• lock() locks the mutex, blocks if the mutex is not available
• try lock() tries to lock the mutex, returns if the mutex is not available
• unlock() unlocks the mutex
More advanced mutex can be found here: en.cppreference.com/w/cpp/thread
C++ includes three mutex wrappers to provide safe copyable/movable objects:
• lock guard (C++11) implements a strictly scope-based mutex ownership
wrapper
• unique lock (C++11) implements movable mutex ownership wrapper
• shared lock (C++14) implements movable shared mutex ownership wrapper
52/56
Mutex 3/3
# include <thread> // iostream, vector, chrono
void f(int& value, std::mutex& m) {
for (int i = 0; i < 10; i++) {
m.lock();
value++; // other threads must wait
m.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
std::mutex m;
int value = 0;
std::vector<std::thread> th_vect;
for (int i = 0; i < 100; i++)
th_vect.push_back( std::thread(f, std::ref(value), std::ref(m)) );
for (auto& it : th_vect)
it.join();
std::cout << value;
}
53/56
Atomic
std::atomic (C++11) class template deﬁnes an atomic type that are implemented
with lock-free operations (much faster than locks)
# include <atomic> // chrono, iostream, thread, vector
void f(std::atomic<int>& value) {
for (int i = 0; i < 10; i++) {
value++;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
std::atomic<int> value(0);
std::vector<std::thread> th_vect;
for (int i = 0; i < 100; i++)
th_vect.push_back( std::thread(f, std::ref(value)) );
for (auto& it : th_vect)
it.join();
std::cout << value; // print 1000
}
54/56
Task-based parallelism 1/2
The future library provides facilities to obtain values that are returned and to catch
exceptions that are thrown by asynchronous tasks
Asynchronous call: std::future async(function, args...)
runs a function asynchronously (potentially in a new thread)
and returns a std::future object that will hold the result
std::future methods:
• T get() returns the result
• wait() waits for the result to become available
async() can be called with two launch policies for a task executed:
• std::launch::async a new thread is launched to execute the task asynchronously
• std::launch::deferred the task is executed on the calling thread the ﬁrst time its
result is requested (lazy evaluation)
55/56
Task-based parallelism 2/2
# include <future> // numeric, algorithm, vector, iostream
template <typename RandomIt>
int parallel_sum(RandomIt beg, RandomIt end) {
auto len = end - beg;
if (len < 1000) // base case
return std::accumulate(beg, end, 0);
RandomIt mid = beg + len / 2;
auto handle = std::async(std::launch::async, // right side
parallel_sum<RandomIt>, mid, end);
int sum = parallel_sum(beg, mid); // left side
return sum + handle.get(); // left + right
}
int main() {
std::vector<int> v(10000, 1); // init all to 1
std::cout << "The sum is " << parallel_sum(v.begin(), v.end());
}
56/56