19.advanced Topics II
19.advanced Topics II
Programming
19. Advanced Topics II
Federico Busato
2023-11-14
Table of Context
1 Undefined Behavior
Undefined Behavior Common Cases
Detecting Undefined Behavior
2 Error Handing
C++ Exceptions
Defining Custom Exceptions
noexcept Keyword
Memory Allocation Issues
Alternative Error Handling Approaches
1/64
Table of Context
3 C++ Idioms
Rule of Zero/Three/Five
Singleton
PIMPL
CRTP
Template Virtual Functions
2/64
Table of Context
4 Smart pointers
std::unique ptr
std::shared ptr
std::weak ptr
5 Concurrency
Thread Methods
Mutex
Atomic
Task-based parallelism
3/64
Undefined Behavior
Undefined Behavior Overview
• What Every C Programmer Should Know About Undefined Behavior, Chris Lattner
• What are all the common undefined behaviours that a C++ programmer should know 4/64
about?
Undefined Behavior Common Cases 1/5
• 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
5/64
Undefined Behavior Common Cases 2/5
• Memory access-related
• NULL pointer deferencing
• Out-of-bound access: the code could crash or not depending on the
platform/compiler
• Type definition
long x = 1ul << 32u; // different behavior depending on the OS
6/64
• Intrinsic functions
Undefined Behavior Common Cases 3/5
• 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];
7/64
Undefined Behavior Common Cases 4/5
• Signed overflow
for (int i = 0; i < N; i++)
if N is INT MAX , the last iteration is undefined behavior. The compiler can assume that
the loop is finite and enable important optimizations, as opposite to unsigned (wrap
around) 8/64
Undefined Behavior Common Cases 5/5
• Dangling reference
iint n = 1;
const int& r = std::max(n-1, n+1); // dagling
// GCC 13 experimental -Wdangling-reference (enabled by -Wall)
9/64
Detecting Undefined Behavior
There are several ways to detect undefined behavior at compile-time and at run-time:
• Using GCC/Clang undefined behavior sanitizer (run-time check)
• Static analysis tools
• Use constexpr expressions as undefined behavior is not allowed
10/64
Exploring Undefined Behavior Using Constexpr
Error Handing
Recoverable Error Handing
Recoverable Conditions that are not under the control of the program. They indicates
“exceptional” run-time conditions. e.g. file not found, bad allocation, wrong user
input, etc.
The common ways for handling recoverable errors are:
Exceptions Robust but slower and requires more resources
Error values Fast but difficult to handle in complex programs
• 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
• Code readability: Using exception can involve more code than the functionality
itself
• Code bloat: Exceptions could increase executable size by 5-15% (or more*)
13/64
*Binary size and exceptions
C++ Exceptions - Disadvantages 2/2
14/64
C++ Exception Basics
int main() {
try {
f();
} catch (int x) {
cout << x; // print "3"
}
}
15/64
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"
}
} 16/64
Exception Capture
NOTE: C++, differently 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"
}
} 17/64
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
}
}
18/64
Defining Custom Exceptions
int main() {
try {
throw MyException();
} catch (const std::exception& e) {
cout << e.what(); // print "C++ Exception"
}
}
19/64
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
21/64
Memory Allocation Issues 1/4
22/64
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";
}
23/64
Memory Allocation Issues 3/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
}; 25/64
Alternative Error Handling Approaches 1/2
26/64
Alternative Error Handling Approaches 2/2
27/64
C++ Idioms
Rule of Zero
Utilize the value semantics of existing types to avoid having to implement custom
copy and move operations
Note: many classes (such as std classes) manage resources themselves and should not
implement copy/move constructor and assignment operator
class X {
public:
X(...); // constructor
// NO need to define copy/move semantic
private:
std::vector<int> v; // instead raw allocation
std::unique_ptr<int> p; // instead raw allocation
}; // see smart pointer
28/64
Rule of Three
Some resources cannot or should not be copied. In this case, they should be declared
as deleted
X(const X&) = delete
X& operator=(const X&) = delete
29/64
Rule of Five
30/64
Singleton
Singleton is a software design pattern that restricts the instantiation of a class to one
and only one object (a common application is for logging)
class Singleton {
public:
static Singleton& get_instance() { // note "static"
static Singleton instance { ..init.. } ;
return instance; // destroyed at the end of the program
} // initiliazed at first use
void f() {}
private:
T _data;
header.hpp
class A {
public:
A();
∼A();
void f();
private:
class Impl; // forward declaration
Impl* ptr; // opaque pointer
};
NOTE: The class does not expose internal data members or methods
32/64
PIMPL - Implementation
33/64
PIMPL - Advantages, Disadvantages
Advantages:
• ABI stability
• Hide private data members and methods
• Reduce compile type and dependencies
Disadvantages:
34/64
PIMPL - Implementation Alternatives
• Put everything into Impl , and write the public class itself as only the public
interface, each implemented as a simple forwarding function:
Good
CoutWriter x;
CerrWriter y;
f(x);
f(y);
38/64
CRTP C++ Examples
Template Virtual Function 1/3
Virtual functions cannot have template arguments, but they can be emulated by
using the following pattern
class Base {
public:
template<typename T>
void method(T t) {
v_method(t); // call the actual implementation
}
protected:
virtual void v_method(int t) = 0; // v_method is valid only
virtual void v_method(double t) = 0; // for "int" and "double"
};
39/64
Template Virtual Function 2/3
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.
Smart pointers prevent most situations of memory leaks by making the memory
deallocation automatic
42/64
C++ Smart Pointers
Smart Pointers Benefits
43/64
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)
• 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
# 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
std::unique_ptr<A, decltype(DeleteLambda)>
x(new A(), DeleteLambda);
} // print "delete"
47/64
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 48/64
} // sh_ptr2: one references
std::shared ptr - Shared Pointer 2/3
• use count() returns the number of objects referring to the same managed
object
Utility method: std::make shared() creates a shared pointer that manages a new
object
49/64
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_ptr = nullptr;
cout << w_ptr.expired(); // print 'true'
51/64
std::weak ptr - Weak Pointer 2/3
• use count() returns the number of objects referring to the same managed
object
• expired() checks whether the referenced object was already deleted (true,
false)
52/64
std::weak ptr - Weak Pointer 3/3
# include <memory>
sh_ptr1 = nullptr;
cout << w_ptr.expired(); // print false
sh_ptr2 = nullptr;
cout << w_ptr.expired(); // print true
53/64
Concurrency
Overview
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:
# 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) );
thread_vect.clear();
for (int i = 0; i < 10; i++) { // thread + lambda expression
thread_vect.push_back(
std::thread( [](){ std::cout << "thread\n"; } );
} 55/64
}
Thread Methods 1/2
Library methods:
• std::this thread::get id() returns the thread id
• detach() permits the thread to execute independently from the thread handle
56/64
Thread Methods 2/2
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!!
C++11 provide 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
std::atomic (C++11) template class defines 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
} 62/64
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 first time its
result is requested (lazy evaluation)
63/64
Task-based parallelism 2/2