revision of basic concepts
The Rule of Three/Five/Zero
- if you need one of the following, you need all
- Copy constructor
- Destructor
- Copy assignment operator
- Move constructor
- Move assignment operator
- this is also inlcuded in the core guidelines
#include <string>
#include <iostream>
// functions that need to be called at ction and dtion
// for some business reason
void registerForPrices(const std::string& ykString){
std::cout << "registering " << ykString << std::endl;
}
void deregisterForPrices(const std::string& ykString){
std::cout << "deregistering " << ykString << std::endl;
}
// cmdyfuture.h
class CmdyFuture
{
public:
CmdyFuture(const std::string& ykString);
CmdyFuture(const CmdyFuture& original);
CmdyFuture& operator=(CmdyFuture copy);
~CmdyFuture();
private:
std::string d_ykString;
friend void swap(CmdyFuture& a, CmdyFuture& b);
};
// cmdyfuture.cpp
CmdyFuture::CmdyFuture(const std::string& ykString)
: d_ykString(ykString)
{
registerForPrices(d_ykString);
}
CmdyFuture::~CmdyFuture()
{
deregisterForPrices(d_ykString);
}
CmdyFuture::CmdyFuture(const CmdyFuture& original)
: d_ykString(original.d_ykString)
{
registerForPrices(d_ykString);
}
CmdyFuture& CmdyFuture::operator=(CmdyFuture copy)
{
swap(*this, copy); // swap the old ykString
return *this;
// the dtor will be called here for old ykString
}
void swap(CmdyFuture& a, CmdyFuture& b)
{
std::swap(a.d_ykString, b.d_ykString);
}
int main(){
// usage
CmdyFuture c("EQUITY");
c = CmdyFuture("COMDTY"); // this is why you need rule of three
return 0;
}
RAII
Resource Acquisition Is Initialization
- Essentially in C++ we should acquire all resource in Ctrs (start of lifecycle) and we release/freeup all resources in Dtrs (end of lifecycle)
- is the simplest, most systematic way of preventing leaks (that’s why there is no garbage collection in C++)
- encapsulate each resource into a class, where
- the constructor acquires the resource and establishes all class invariants or throws an exception if that cannot be done,
- the destructor releases the resource and never throws exceptions;
- always use the resource via an instance of a RAII-class that either
- has automatic storage duration or temporary lifetime itself, or
- has lifetime that is bounded by the lifetime of an automatic or temporary object
- the benefit is that we don’t have to think about allocation of memory anymore and compenets are reusable
- e.g. if we use
vector
of weird stucts, it’s gonna deallocate everything when we stop using it - e.g. if we have custom classes that do weird allocations, we can still use them inside other objects (such as
vector
)
- e.g. if we use
Inheritance and Polymorphism
-
inheritance: it’s all about having one common interface / API and trigger different behaviours dynamically
-
overloading is polymorphism, same name for different functions (you look at the arguments to understand which to call)
note that you need virtual functions which use Vtable and that makes runtime a bit slower !!always have a virtual destructor!! there is an alternative for static polymorphism in C++11 is
variant
#include <string>
#include <iostream>
#include <memory>
#include <ctime>
#include <cstdlib>
/*
Person
/ \
Grandpa Youngster
/ \
Millenial CmdyFuture
*/
// Person
class Person
{
public:
Person(const std::string& name)
: d_name(name)
{}
virtual ~Person(){} // !!! always use that
virtual std::string greeting() const = 0;
std::string d_name;
};
class Grandpa : public Person
{
public:
Grandpa(const std::string& name, int pension)
: Person(name), d_pension(pension)
{
}
std::string greeting() const override {
return "Well, hello sir! Did you read the newspaper today?";
}
int d_pension;
};
class Youngster : public Person
{
public:
Youngster(const std::string& name, int pocketMoney)
: Person(name), // calls the base class ctor
d_pocketMoney(pocketMoney)
{}
int d_pocketMoney;
};
class Millenial : public Youngster
{
public:
Millenial(const std::string& name, int pocketMoney)
: Youngster(name, pocketMoney) // calls the base class ctor
{}
std::string greeting() const override {
return "What's up man? What's your insta?";
}
};
class GenZ : public Youngster
{
public:
GenZ(const std::string& name, int pocketMoney)
: Youngster(name, pocketMoney) // calls the base class ctor
{}
std::string greeting() const override {
return "Don't talk to me, I'm watching TikToks now!!";
}
};
std::unique_ptr<Person> getRandomPerson(){
int r = (rand() % 3); // either 0, 1, 2
if (r == 0){
return std::make_unique<Grandpa>("Uncle George", 5000);
}
else if (r == 1){
return std::make_unique<Millenial>("MC GG", 120);
}
else { // if (r == 0
return std::make_unique<GenZ>("Ginfluencer", 5);
}
}
int main(){
srand(time(0)); // Initialize random number generator.
for (int i = 0; i < 10; i++){
std::unique_ptr<Person> u_p = getRandomPerson();
std::cout << u_p->greeting() << std::endl;
}
return 0;
}
- Inheritance lets us inherit attributes and methods from another class. Polymorphism uses those methods to perform different tasks. This allows us to perform a single action in different ways.
- Polymorphism is like duck-typing, you get same behaviour
always put virtual Destructor in classes with at least one virtual functions !!! (otherwise you delete the dynamic/derived-class-type with the dtor of the static/base-class type and it leads to undefined behaviour, resource leaks, …)
algorithms, iterators and functional programming coding style
the main purpose of iterators is to decouple conainters and algorithms (it’s a generalisation of position for algorithms)
- iterators: implementations differ per compiler: some use pointers, some use indices,
- they are useful abstraction and common API for making the algorithms generic (
begin
,next
, etc.., same API) const_iterator
is used for read only (we CAN mutate the iterator, but CANNOT mutate the object that the iterator is pointing to)note that C++ does not provide good structures for
trees
- Stepanov: the creator of the iterators
- there are family’s of iterators and algorithms make requirements on these iterators
- they make some implicit requirements for the underlying containers
conatiners and iterators they provide:
STL CONTAINER | ITERATOR SUPPORTED |
---|---|
Vector | Random-Access |
List | Bidirectional |
Dequeue | Random-Access |
Map | Bidirectional |
Set | Bidirectional |
Stack | N/A |
Examples:
for_each
(cppreference): the algorithms only needs the forward move (hence requiresLegacyForwardIterator
or a “stronger” iterator)sort
(cppreference): the algorithm only needs the forward move (hence requiresLegacyRandomAccessIterator
)- table of iterators to examine and note: https://en.cppreference.com/w/cpp/iterator
some resources for mastering algorithms:
- https://hackingcpp.com/cpp/cheat_sheets.html
- https://github.com/gibsjose/cpp-cheat-sheet/blob/master/Data%20Structures%20and%20Algorithms.md
using algorithms helps writing more functional style code
in C++20 we have constraints and concepts
tips and fun facts
tools
use spaces to have a out-of-the-box environment, no weird setups etc.
always use cppreference for official documentation (this is the correct resource)
favourite tool to quickly test code is compiler explorer
sanitizers
- use sanitizers (checks for buggy code)
- Address Sanitizer (ASan): use-after-free, double-free, buffer (stack, heap and global buffer) overflows and underflows, along with other memory errors (link)
- Memory Sanitizer (MSan): uninitialized memory reads
- Undefined Behavior Sanitizer (UBSan): use of null pointers, division by zero, signed integer overflow and other undefined behavior
always use static analysis tools and sanitizers
C++11/14/17 mentionalbe features
- use keyword
auto
- smart pointers, move semantics
- use
override
in derived classes for functions that exist also in base class - use
delete
to stop the usage of ctors, dtors (before we used to make them private) = 0
for pure virtual functions that have no implementationoptional
is good way to handle Nullable- use
explicit
in ctors to avoid implicit converstion being made - use
[[fallthrough]]
,[[noreturn]], [[maybe_unused]]
attributes to make code readable (or a comment) (it is good to distinguish if there is a bug or an actual thing the developer intended to do) link here
compiler errors
- always add this flags when compiling
-Wall -Wextra -Wpedantic
(these warning are useualy errors) - good warnings
- in
CMakeLists.txt
the following warnigns should always exist:configure_compiler_options(-Wall -Wextra -Wshadow -Wnon-virtual-dtor -pedantic)
Thoughts on performance:
- in order to have fast code, you need data locality (things that are close to each other in memory are quick to access as well, because you already brought them from RAM)
- even if you need to read one byte, you will get a cache line (at least 64 bytes)
examples:
- linked-lists vs vectors, benchmarking vector
- using
make_shared
,make_unique
for smart pointers,std::shared_ptr<Dog> sptr (new Dog("azor", 3)); // this makes allocation two times std::shared_ptr<Dog> sptr = make_shared<Dog>("azor", 3); // this makes allocation one time auto sptr = make_shared<Dog>("azor", 3); // even better
Guidelines for pointers:
- always prefer stack instead of heap (automatic and fast allocation of resources)
- always use smart pointers (don’t use
new/delete
), - first option is
unique_ptr
(shared_ptr
only make sense in multithreaded applications, it’s often used in prod code to not think about lifetime of things, but in general it’s harder to reason about) - Raw pointers should be used as observing pointers (could be thought as references with NULL state) (sometimes are mandatory because of old code)
If you only care about the object, pass it by reference (forget about the pointer)
Many people use
shared_ptr
to handle custody automatically and not worry about lifetimes (inefficient since you have things at the heap and it’s harder to think about owneship)
good resources to learn C++
- herb sutter blog, best way to learn in the long run, has really good examples and it’s free
- scott meyers has good sections about containers and algorithms
- core guidelines