Skip to content

Zbiór informacji o C++

Oficjalna dokumentacja i zalecenia: ISO CPP GUIDELINES

Dane i struktury

Proste typy danych

Proste typy zmiennych:

  • int - liczba całkowita
  • float - liczba zmiennoprzecinkowa
  • double - liczba zmiennoprzecinkowa podwójnej precyzji
  • char - pojedynczy znak
  • bool - wartość logiczna
  • void - brak wartości

Kontenery📦

W bibliotekach standardowych C++ mamy następujące typy kontenerów:

  • Sekwencyjne
  • vector - jednowymiarowa tablica
  • string - jednowymiarowa tablica
  • list - lista dwukierunkowa
  • deque - kolejka o dwóch końcach
  • Asocjacyjne
  • set - usuwa elementy równoważne
  • map - tablica asocjacyjna (słownik)
  • multiset - nie usuwa el. równoważnych
  • multimap - klucz może występować wielokrotnie
  • Haszujące (unordered_)
  • unordered_set
  • unordered_map
  • unordered_multiset
  • unordered_multimap

Można też je podzielić względem tego na czym bazują:

  • Tablice: vector, string, array
  • Węzły: list, set, map, multiset, multimap, unordered_set, unordered_map, unordered_multiset, unordered_multimap

Kontenery biblioteki standardowej operują na kopiach elementów. W przypadku, gdy chcemy operować na oryginalnych elementach, należy użyć wskaźników lub referencji.

Vector

Podstawowym kontenerem w C++ jest std::vector, który jest odpowiednikiem tablicy dynamicznej w C.

#include <vector>
std::vector <int> zero_vector(5); //wektor zainicjalizowany 5 zerami

//wektor zainicjalizowany podanymi wartościami
std::vector<int> numbers = {1, 2, 3, 4, 5};

numbers[0] = 10; //zmiana wartości pierwszego elementu

Metody dostępu (mogą być używane także do zmieniania wartości):

  • at(index) - zwraca element na danym indeksie (w razie problemów rzuca wyjątek std::out_of_range)
  • operator [] - zwraca element na danym indeksie (nie sprawdza czy indeks jest poprawny)
  • front() - zwraca pierwszy element
  • back() - zwraca ostatni element

Metody modyfikujące:

  • push_back(elem) - dodaje element na koniec
  • pop_back() - usuwa element (nie zwraca go).
  • size() - zwraca ilość elementów
  • clear() - usuwa wszystkie elementy

List

std::list jest to lista dwukierunkowa, która pozwala na szybkie dodawanie i usuwanie elementów z początku i końca listy. Nie posiada operatora [], więc dostęp do elementów odbywa się za pomocą iteratorów.

#include <list>

std::list<int> numbers = {1, 2, 3, 4, 5};

Metody dostępu:

  • front()
  • back()

Metody modyfikujące:

  • push_front(elem), push_back(elem)
  • pop_front(), pop_back()
  • insert(iterator, elem)
  • erase(iterator)

std::set

Jest to kontener zawierający kolekcję niepowtarzalnych elementów.

Eliminuje on elementy równoważne a jest równoważne b, jeżeli !(a < b) && !(b < a)

#include <set>

std::set<int> numbers = {1, 2, 3, 4, 5};

set<int> s; //zbiór liczb całkowitych
s.insert(1); //dodaje elementy do kolekcji
s.insert(1); //ta operacja jest pusta (usuwa elementy równoważne)
s.insert(2);
assert( s.size() == 2); //bada liczbę elementów
assert( s.count(1) == 1 ); //zlicza liczbę wystąpień elementu

set jest zaimplementowany jako drzewo czerwono-czarne, co pozwala na szybkie dodawanie i usuwanie elementów. Z tego powodu potrzebuje on operatorów < i == dla swoich elementów.

#include <set>

typedef std::pair<int,int> para;
//Aby zdefiniować set z parami, musimy zdefiniować operator < dla pary
bool operator<(const para& a, const para& b) {
    return a.first < b.first || (a.first == b.first && a.second < b.second);
}

std::set<para> pary = {para(1, 2), para(2, 3)};

Ciągi znaków std::string

std::string jest to kontener przechowujący ciągi znaków.

#include <string>

std::string s = "Hello, World!";

Stringi nie są typowymi kontenerami. Mają one wiele metod do mnipulaji nimi.

Metody do konwersji:

  • std::string::c_str() - zwraca wskaźnik do tablicy znaków (kiedy potrzebujemy kompatybilności z C)
  • atoi(str), atof(str), atol(str) - konwertują string na liczbę (nie są to metody klasy string, ale funkcje globalne)
  • std::to_string(num) - konwertuje liczbę na string

Metody do edycji:

  • std::string::erase(start, length) - usuwa podciąg
  • std::string::replace(start, length, str) - zamienia podciąg na inny
  • std::string::append(str) - dodaje string na koniec (analogiczny do operatora +)

Inne:

  • std::string::compare(str) - porównuje dwa stringi (zwraca 0, jeżeli są równe lub liczbę ujemną/dodatnią, jeżeli pierwszy jest mniejszy/większy od drugiego)
std::string s1 = "abc";
std::string s2 = "def";
assert(s1.compare(s2) < 0);

Do porównań można też użyć operatorów ==, !=, <, >, <=, >=

  • std::string::substr(start, length) - zwraca podciąg stringa
  • std::string::find(str) - zwraca pozycję, na której zaczyna się podciąg (lub std::string::npos, jeżeli nie znaleziono)
std::string s = "Hello, World!";
assert(s.find("World") == 7);

Klasy

Prosta przykładowa klasa w C++

using namespace std;

// sama deklaracja klasy w pliku nagłówkowym .hpp
class User{
    public:
        // prosty konstruktor z domyślną wartością dla nazwiska
        User(string name, string surname = "Kowal");
    protected:
        int id = -1; //ustawiona domyślna wartość jako -1
        int get_proper_id();
    private:
        string name;
        string surname;
        //Destruktor wołany podczas usuwania klasy
        ~User();
};

// definicje konkretnych metod umieszcza się w pliku źródłowym .cpp
User::User()
 :name(name),surname(surname) // lista inicjalizacyjna
{
  id = get_proper_id();
}

int User::get_proper_id()
{
  /////kod
}

Kontrola dostępu do składników

W ramach klas możemy ograniczyć dostęp do jej składników, dzięki czemu możemy dokładnie określić, które elementy powinny stanowić publicznie dostępne elementy API dla danej klasy, a które nie.

Wyróżniamy tutaj 3 rodzaje dostępu (etykiet dla elementów):

  • private - składniki oznaczone jako private są dostępne tylko dla funkcjo składowych dla danej klasy (no i i dla klas zaprzyjaźnionych)
  • protected - tak jak private + dodatkowo jest dostępny dla klas dziedziczących
  • public - dostępny bez ograniczeń

Na ogół w ramach definicji klasy najpierw umieszcza się elementy publiczne (jako najbardziej interesujące dla używających nasz obiekt), potem protected i na samym końcu private.

Funkcje i klasy zaprzyjaźnione

link

Czasami w C++ może pojawić się potrzeba dostępu do prywatnych lub chronionych pól wybranej klasy poza nią samą. Wtedy przydaje nam się koncept klas zaprzyjaźnionych (friend classes and functions). Warto tutaj pamiętać o tym, że na ogół takie operacje to zły pomysł.

class Customer
{
    // klasa, która może być wygodnie printowana, albo czytana za pomocą iostreama
    friend std::ostream &operator<<( std::ostream &, const Customer & );
    friend std::istream &operator>>( std::istream &, Customer & );

private:
    std::string name;
    std::string surname
public:
    Customer(std::string name, std::string surname):name(name),surname(surname){};
};

int main()
{
  Customer c = Customer("Jan","K");
  std::cout<<c;
}

Funkcje wirtualne

są oznaczane za pomocą słowa kluczowego virtual. Bez słowa virtual kompilator nie wiedziałby, że funkcja jest zależna od tego jaką klasę mamy pod wskaźniekiem (po prostu wywoływałby metodę odpowiadającą temu na jaką klasę mamy obecnie zmapowany nasz wskaźnik).

class Base {
   public:
    virtual void print() {
        cout << "Base Function" << endl;
    }
};

class Derived : public Base {
   public:
    void print() {
        cout << "Derived Function" << endl;
    }
};

int main() {
    Derived derived1;

    // pointer of Base type that points to derived1
    Base* base1 = &derived1;

    // calls member function of Derived class
    base1->print();

    return 0;
}

Podczas pracy z funkcjami wirtualnymi dobrą praktyką jest korzystanie ze specyfikatora override.
Dzięki jego użyciu w klasie potomnej będziemy mieć pewność, że ta funkcja w klasie bazowej jest wirtualna.

struct A
{
    virtual void foo();
    void bar();
    virtual ~A();
};

// member functions definitions of struct A:
void A::foo() { std::cout << "A::foo();\n"; }
A::~A() { std::cout << "A::~A();\n"; }

struct B : A
{
//  void foo() const override; // Error: B::foo does not override A::foo
                               // (signature mismatch)
    void foo() override; // OK: B::foo overrides A::foo
//  void bar() override; // Error: A::bar is not virtual
    ~B() override; // OK: `override` can also be applied to virtual
                   // special member functions, e.g. destructors
};

Kiedy chcemy jakiś abstrakcyjny interfejs możemy wymusić na klasach potomnych implementację zaznaczając brak implementacji w interfejsie:

virtual int getValue() = 0;

Funkcje const

Kiedy chcemy zaznaczyć, że metoda w naszej klasie nie zmodyfikuje jej wykorzystuje się słowo kluczowe const. Warto tutaj uważać, aby nie pomylić takich deklaracji z deklaracjami zwracającymi wartości typu const.

class User{
  /// kod

  // ta metoda nie może zmodyfikować żadnych danych w klasie
  std::string getName() const;
  //tutaj kompilator pozwoli nam zmodyfikować dane w instancji klasy
  const std::string getSurname();
}

Funkcje

Lambdy

Składnia lambdy:

  • [] - Tu podajemy listę przechwytywania
  • [x] - przechwytuje obiekt x (tylko odczyt)
  • [&x] - przechwytuje obiekt x (odczyt i zapis)
  • [=] - dowolny obiekt ze scope'a do odczytu
  • [&] - dowolny obiekt ze scope'a do odczytu i zapisu
  • () - argumenty, jakie ma przyjmować wyrażenie lambda. (Opcjonalne)
  • atrybuty wyrażenia lambda, z możliwych atrybutów w tym momencie najistotniejszy jest mutable, który sprawia że zmienne przechwycone przez wartość mogą być modyfikowane wewnątrz ciała wyrażenia. (Opcjonalne)
  • -> T - typ zwracany (Opcjonalne)
  • {} - ciało wyrażenia
//Najprostsza możliwa lambda
[] { }();

[]( int a )->float
{
    if( a < 0 )
         return 0;

    return a * 0.5f;
}

Przekazywanie argumentów do funkcji

W C++ istnieją różne sposoby na przekazywanie argumentów do funkcji.

  • poprzez kopię - w domyślnym wypadku do naszej funkcji przekazywana jest kopia naszego obiektu. O ile to nie jest problem przy liczbach to przy większych obiektach to to może być już problem.
  • poprzez wskaźnik - jest to opcja zalecana bardziej przy kodzie napisanym w czystym C, czy też w wypadku, gdy chcemy sobie zastrzec możliwość przekazania pustego wskaźnika.
  • poprzez referencję - jest to sposób zbliżony do wskaźnika, do funkcji przekazujemy referencję do naszego obiektu.
class Klasa
{
private:
    /* data */
public:
    Klasa(/* args */)
    {
        std::cout << "Wołanie konstruktora\n";
    }

    Klasa(const Klasa& other)
    {
        std::cout << "Wołanie konstruktora kopiującego\n";
    }
};

void funkcja_zwykla(Klasa k) {}
void funkcja_pointer(Klasa *k) {}
void funkcja_referencja(Klasa &k) {}

int main()
{
    Klasa k = Klasa();
    std::cout << "funkcja_zwykla:\n";
    funkcja_zwykla(k);
    std::cout << "funkcja_pointer:\n";
    funkcja_pointer(&k);
    std::cout << "funkcja_referencja:\n";
    funkcja_referencja(k);
}

program wypisze:

Wołanie konstruktora
funkcja_zwykla:
Wołanie konstruktora kopiującego
funkcja_pointer:
funkcja_referencja:

W wypadku przekazywania poprzez referencję lub wskaźnik należy pamiętać o tym, że zmiany obiektu, które miały miejsce wewnątrz funkcji będą nadal widoczne z zewnątrz, ponieważ operujemy tam na tej samej instancji obiektu. Aby uniknąć takich problemów warto przekazywać te argumenty jako const, albo zastanowić się, czy jednak kopia nie będzie lepsza.

L-Value, R-Value i std::move

L-Value - jest to obiekt, który ma swoje miejsce w pamięci, czyli możemy go zmieniać, przypisywać do niego wartości, pozyskać jego położenie itp. Jest on odniesieniem do konkretnego miejsca w pamięci. Jest to wyrażenie zwbędące referencją na obiekt.

R-Value - jest to obiekt, który nie ma swojego miejsca w pamięci, czyli nie możemy go zmieniać, przypisywać do niego wartości itp. W niektórych wypadkach można powiedzieć, że to on jest wartością. Nie możemy do niego przypisać jakiejś chcianej przez nas wartości.

int a = 5; //a jest L-Value, a 5 jest R-Value

int &foo(){
  static int i=5;
  return i;
}

foo()// L-Value ponieważ zwraca referencję na i

struct St{
  int x;
}

St s; // s jest L-Value
s.x = 5; // s.x jest L-Value

Bar(); // R-Value ponieważ nie zwraca referencji, lecz jest nowym obiektem

Jednym z przykładów zastosowania tej wiedzy jest przypisywanie wartości do obiektów.

int i=32;
int j=99;
int *p = &i;
7 = i; //błąd kompilacji, 7 jest R-Value

*p = j; //poprawne, *p jest L-Value

((i>21) ? i : j) = 42; //dozwolone, ponieważ wyrażenie po lewej zwraca jedno z dwóch l-value

Jednak najczęstszym wykorzystaniem tych pojęć jest std::move. Jest to funkcja, która pozwala na przeniesienie obiektu z jednego miejsca do drugiego. Jest to bardzo przydatne w przypadku, gdy chcemy przenieść obiekt, a nie kopiować go, poniweaż kopiowanie obiektów może być kosztowne. link1

template <class T> swap(T& a, T& b)
{
    T tmp(a);   // teraz mamy 2 kopie a
    a = b;      // teraz mamy 2 kopie b
    b = tmp;    // teraz mamy 2 kopie tmp (czyli a)
}
// aby zrobić to bez kopii możemy użyć std::move
template <class T> swap(T& a, T& b)
{
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

Z jego pomocą mówimy też kompilatorowi, że nie zamierzamy już korzystać z danego obiektu, po przekazaniu.

std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = std::move(v1); //przenosimy v1 do v2
// v1.clear(); //błąd kompilacji, v1 jest już przeniesione i nie powinniśmy z niego korzystać

std::move łączy się z operatorem &&, który jest oznaczeniem R-Value Reference.

class Klasa
{
public:
    Obj o;
    Klasa() {}
    // prosty konstruktor przenoszący
    Klasa(Klasa&& other): o(std::move(other.o)) {}
};

Mechanizmy języka

Zarządzanie pamięcią

new i delete

Zarządzanie pamięcią w C++ jest podobne do zarządzania w C. Jednak jest oparta o słowa kluczowe new i delete. (NIGDY nie mieszajmy tych dwóch sposobów)

artykuł

new służy do tworzenia nowych obiektów i alokowania pamięci dla nich, natomiast delete służy do zwalniania zarezerwowanej wcześniej pamięci.

Przykład użycia słowa kluczowego new:

int *p = new int; // alokuje pamięć dla zmiennej typu int
*p = 5; // przypisuje wartość 5 do zmiennej
delete p; // zwalnia pamięć zarezerwowaną dla zmiennej p

Możliwe jest także użycie słowa kluczowego new do tworzenia tablic dynamicznych:

int *tab = new int[10]; // alokuje pamięć dla tablicy 10-elementowej
tab[0] = 5; // przypisuje wartość 5 do pierwszego elementu tablicy
delete[] tab; // zwalnia pamięć zarezerwowaną dla tablicy tab

Sprytne wskaźniki - Smart Pointers

W związku z wieloma problemami występującymi przy zarządzaniu pamięcią w C++, takimi jak wycieki pamięci, powstały tzw. smart pointery. Są to obiekty, które pomagają zautomatyzować zarządzanie pamięcią.

  • std::unique_ptr - jest to smart pointer, który przechowuje wskaźnik do obiektu i zwalnia pamięć po wyjściu poza zakres. Nie można go kopiować, ale można przenieść.
std::unique_ptr<int> p(new int);
*p = 5;
  • obiekty unique_ptr mają wielkość zwykłego wskaźnika
  • zastąpił on auto_ptr z C++98
  • automatycznie usuwają wskazywany obiekt w destruktorze
  • mogą być przechowywane w kontenerach standardowych
  • nie można kopiować, ale można przenieść
  std::unique_ptr<Foo> f() {
    return std::unique_ptr<Foo>(new Foo(42));
  }
  std::unique_ptr<Foo> q = f(); //konstruktor kopiujący z r-value
  // v.push_back(p); //błąd kompilacji - nie ma konstruktora kopiującego
  v.push_back( std::move(p) ); //v staje się wł. wskazywanego obiektu
  //teraz p wskazuje na null
  • std::shared_ptr - jest to smart pointer, który przechowuje wskaźnik do obiektu i zwalnia pamięć po wyjściu poza zakres, jeżeli nie ma innych shared_ptr wskazujących na ten obiekt. Można go kopiować.

  • obiekty shared_ptr mają dodatkowo licznik referencji

  • konstruktor ustwia licznik na 1
  • konstruktor kopiujący zwiększa licznik referencji
  • destruktor zmniejsza licznik
#include <memory>
class Foo { /* ... */ };
{
  std::shared_ptr<Foo> p1(new Foo(1) );
  {
    std::shared_ptr<Foo> p2(p1);
    //licznik odniesien == 2
    /* ... */
  } //destruktor p2, licznik = 1
} //destruktor p1 usuwa obiekt
  • std::weak_ptr - jest to smart pointer, który przechowuje wskaźnik do obiektu, ale nie zwiększa licznika referencji. Można go kopiować.

  • używany do uniknięcia cyklicznych referencji

  • nie zwiększa licznika referencji
  • nie ma operatora -> ani *
  • można go zamienić na shared_ptr za pomocą lock()
std::shared_ptr<int> p(new int(42));
std::weak_ptr<int> q(p);
if (std::shared_ptr<int> r = q.lock()) {
  // r jest teraz shared_ptr
}

Przy okazji korzystania ze sprytnych wskańników warto wspomnić o istnieniu funkcji std::make_shared i std::make_unique, które pozwalają na tworzenie obiektów bezpośrednio w smart pointerach, bez używaniu operatora new.

#include <boost/make_shared.hpp>
std::shared_ptr<Foo> pf = make_shared<Foo>(); //konstruktor domyślny
std::shared_ptr<Foo> pf2 = make_shared<Foo>(arg1,...,argN);

Pętle

Typy pętli:

  • Podstawowa pętla for
for (int i = 0; i < 5; ++i)
  • pętla while
int j = 0;
while (j < 10) {
    // rób coś
    ++j;
}
  • pętla do-while
int k = 0;
do {
    // Wykonuj przynajmniej raz, potem sprawdzaj warunek
    ++k;
} while (k < 3);
  • range-based for - jest to zalecana metoda dla zbiorów iterowalnych
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
    // Iteracja po elementach kontenera
}

W wypadku bardziej złożonych obiektów można użyć też referencji

for(auto&& element: lista)

może być też używany do rozpakowywania link

for (auto&& [first, second] : mymap)

Wyjątki

Wyjątki służą do niesekwencyjnego przekazania sterowania, kiedy pojawi się jakiś nieoczywisty problem. W C++ wyjątki są rzucane za pomocą słowa kluczowego throw, a łapane za pomocą bloku try-catch.

Są rzucane w rzadkich sytuacjach, kiedy nie da się kontynuować programu. Należy unikać rzucania wyjątków w miejscach, gdzie można to zastąpić zwracaniem wartości. (ich koszt obliczeniowy jest dużo większy)

try {
    throw std::runtime_error("Error");
} catch (std::runtime_error& e) {
    std::cout << e.what() << std::endl;
}

Zasady używania:

  • Wyjątki powinny dziedziczyć po klasie std::exception.
  • Nie należy rzucać wyjątków w destruktorach. Bo podczas wyjątku niszczymy stare klasy i wtedy po raz drugi wywołałby się nasz destruktor i poraz drugi pojawiłby się wyjątek.
  • Wyjątek rzucać przez wartość.
throw Exception //zgłasza wyjątek przez wartość
throw new Exception;//zajmuje pamięć na stercie
  • Wyjątek przechwytywać przez referencję
catch (const Exception& e) //przechwytuje przez referencję
//catch(Exception e) //tworzy lokalną kopię

Templatki

Pozwalają kompilatorowi na łatwą autogenerację kodu.

Templatka metody

template <class myType>
myType GetMax (myType a, myType b) {
 return (a>b?a:b);
}

Templatka klasy

template <class T>
class mypair {
    T values [2];
  public:
    mypair (T first, T second)
    {
      values[0]=first; values[1]=second;
    }
};

Specjalizacje templatek - pozwalają na łatwe doprecyzowanie implementacji dla pewnych ścićle określonych typów.

template <class T> class mycontainer { ... };
template <> class mycontainer <char> { ... };

Typy castów (operatorów rzutowania)

Używając tych operatorów na ogół powinno się operować na wskaźnikach (albo referencjach)

  • static_cast<naCoChcemyZrzutowac>(wyrazenie) - wykorzystywany do konwersji danych w zmiennych różnych typów (np pomiędzy typami reprezentującymi liczby), jest on wykonywany w trakcie kompilacji.
  • dynamic_cast<naCoChcemyZrzutowac>(wyrazenie) - służy do przekształcania typów klas pomiędzy klasami które po sobie dziedziczą. W wypadku niepowodzenia zwraca null Wyróżniamy:
  • Downcasting - kastujemy klasę bazową na potomną
  • Upcasting - gdy chcemy uzyskać instancję klasy bazowej
  • reinterpret_cast<naCoChcemyZrzutowac>(wyrazenie) - działa podobnie do dynamic casta, ale nie zwraca nulla, zaleca się używanie tylko kiedy dobrze wiesz co robisz.
  • const_cast<naCoChcemyZrzutowac>(wyrazenie) - pozwala zmienić stałą na zmienną i na odwrót na ogół jego używanie nie jest zalecane
     const double liczbaPI = 3.14;
     const double *wskDoStalej = &liczbaPI;

     double *wskaznik = const_cast<double *>(wskDoStalej); //przypisujemy dane ze stałej do zwykłego wskaźnika

     cout << *wskaznik << endl; //wypisze 3.14
     *wskaznik = 43;
     // *wskDoStalej = 43; ERROR
     cout << *wskaznik << endl; //wypisze 3.14

Wydajność

Wątki

Jest wiele sposobów na wątki, ale najprostszym do użycia jest std::thread

#include <thread>

void foo() {
    // funkcja, którą chcemy uruchomić w nowym wątku
}

int main() {
    std::thread t(foo); // tworzymy nowy wątek, który uruchomi funkcję foo
    //robimy coś w głównym wątku
    t.join(); // czekamy na zakończenie wątku
}

W wypadku klas wygląda to następująco:

#include <thread>

class Klasa
{
public:
    void foo()
    {
        // funkcja, którą chcemy uruchomić w nowym wątku
    }
};

int main()
{
    Klasa k;
    //std::thread t(&Klasa::moja_metoda,&instancja_klasy, argument1, argument2, argument3);
    std::thread t(&Klasa::foo, &k); // tworzymy nowy wątek, który uruchomi funkcję foo
    //robimy coś w głównym wątku
    t.join(); // czekamy na zakończenie wątku
}

Wielowątkowość z użyciem std::par

W C++11 i C++17 pojawiły się nowe sposoby przetwarzania wielowątkowego mogącego stanowić swego rodzaju alternatywę dla OpenMP.

W tym podejściu wykorzystujemy standardowe kontenery oraz algorytmy znajdujące się w bibliotece standardowej. Takie jak std::for_each, std::sort, czy std::reduce etc.

#include <vector>
#include <algorithm>
#include <iostream>

//Using functor
struct Suma
{
    void operator()(int n) { sum += n; }
    int sum{0};
};

int main()
{
    std::vector<int> nums{3, 4, 2, 8, 15, 267};

    auto print = [](const int& n) { std::cout << " " << n; };

    std::cout << "before:";
    std::for_each(nums.cbegin(), nums.cend(), print);
    std::cout << '\n';

    std::for_each(nums.begin(), nums.end(), [](int &n){ n++; });

    // calls Sum::operator() for each number
    Suma s = std::for_each(nums.begin(), nums.end(), Suma());

    std::cout << "after: ";
    std::for_each(nums.cbegin(), nums.cend(), print);
    std::cout << '\n';
    std::cout << "suma: " << s.sum << '\n';
}

Wrac z C++17 do algorytmów pojawiły się polityki wykonania (execution policies) które pozwalają na wykorzystanie wielowątkowości w algorytmach.

Wyróżnia się:

  • std::execution::seq - wykonanie sekwencyjne. Domyślna polityka. Zabrania zrównoleglania
  • std::execution::par - umożliwia wykonanie równoległe
  • std::execution::par_unseq - umożliwia wykonanie równoległe i wektorowe //TODO wyjaśnić dokładniej
std::sort(std::execution::par, c.begin(), c.end());

Przy wykonaniu równoległym należy pamiętać, że zmienne muszą znajdować się na stercie (heap). W przeciwnym wypadku mogą wystąpić problemy z dostępem do pamięci.

std::array<int, 1024> a = ...;
std::sort(std::execution::par, a.begin(), a.end()); // Error, elementy snadjują się na stosie

std::vector<int> v = ...;
std::sort(std::execution::par, v.begin(), v.end()); // OK, wektor alokuje na stercie

Przy takim podjeściu warto pamiętać o ochronie pamięci. W tym celu można użyć std::mutex lub std::atomic.

std::atomic<int> *suma = new std::atomic<int>[nbin];

Dzięki uniwersalnemu kodowi możliwa jest także kompilacja kodu rówloległego w taki sposób, aby mógł wykorzystać zasoby chociażby kart graficznych. link dla nvidii CUDA i AMD z ROCm

Inne Słowa kluczowe

explicit TODO

Nowe standardy

C++17

Artykuł

C++20

Pełna lista z omówieniem

Z fajnych rzeczy są kontrakty //TODO C++ contracts https://www.modernescpp.com/index.php/c-core-guidelines-a-detour-to-contracts (similar to java JML, or Dafny for C#)

Oraz moduły https://en.cppreference.com/w/cpp/language/modules.html

Do zrobienia

TODO: ogólnie o klasach i dziedziczeniu, explicit C- czyli co musi być kompilowane jako czyste C, typy smart pointerów.

//TODO VOLATILE https://en.cppreference.com/w/cpp/language/cv