assert makro

assert je preprocesorska naredba (makro) koja služi za ispitivanje da li je neki uvjet zadovoljen ili nije. Koristi se kao funkcija koja uzima jedan argument tipa bool ili int te ne čini ništa ako se argument izračunava na true ili broj različit od nule, a zaustavlja izvršavanje programa ako se izračunava na false ili nulu.

Na primjer,

#include <vector>
#include <cassert>  // Nužno za assert makro!

using namespace std;

int main()
{
  vector<int> a(3);
  a.push_back(1.0);

  assert(a.size() == 4);

  return 0;
}

Ovdje se ispituje da li je a.size() jednako 4. Ako jeste program se nastavlja, a ako nije bit će zaustavljen s porukom o grešci. Na primjer, ako u assert uvjetu zamijenimo 4 sa 5 dobit ćemo sljedeću poruku o grešci:

main: /.../Assert/main.cpp:13: int main(): Assertion `a.size() == 5' failed.
Aborted

Vidimo da nam assert daje uvjet koji nije ispunjen i liniju koda u kojoj se nalazi assert koji je zaustavio program.

Assert makro je sredstvo za provjeru ispravnosti koda i koristi se u fazi razvoja koda. Koristimo ga provjeru ispravnosti različitih invarijanti u kodu. Kada je kod završen sve assert naredbe mogu se jednostavno eliminirati iz programa. To se postiže time što preprocesor definira assert makro u ovisnosti o simbolu NDEBUG: ako simbol NDEBUG nije definiran tada cassert makro radi na opisan način; ako je NDEBUG definiran onda se assert naredba pretvara u trivijalnu naredbu koju prevodilac eliminira iz koda. Da bismo eliminirali assert naredbe NDEBUG treba definirati prije točke uključivanja <cassert> zaglavlja. Na primjer,

#include <vector>
#define NDEBUG 1
#include <cassert>  // Nužno za assert makro!

using namespace std;

int main()
{
  vector<int> a(3);
  a.push_back(1.0);

  assert(a.size() == 5);

  return 0;
}

sada assert naredba ne zaustavlja program.

Ubacivanje #define NDEBUG linije nije najpraktičniji način za eliminaciju assert naredbi pogotovo ako program ima puno datoteka koje koriste assert. Bolja opcija je da se simbol NDEBUG definira na komandnoj liniji pri kompilaciji pomoću -D opcije:

g++ -Wall -O3 -DNDEBUG main.cpp

Opcija -DNDEBUG učinit će simbol NDEBUG definiran.

Ako koristimo CMake tada treba program kompilirati u Release načinu i Cmake će ubaciti -DNDEBUG opciju prevodiocu za nas. Na primjer, u build direktoriju naredbe

cmake -DCMAKE_BUILD_TYPE=Release ..
make VERBOSE=1

će kompilirati program u Release načinu. Opcija VERBOSE=1 nam omogućava da vidimo koje su opcije poslane prevodiocu. Dobivamo:

 [50%] Building CXX object CMakeFiles/main.dir/main.cpp.o
/usr/bin/c++  -O3 -DNDEBUG -o CMakeFiles/main.dir/main.cpp.o -c /.../Assert/main.cpp

assert je nužno koristiti u što većoj mjeri za provjeru ispravnosti funkcioniranja programa. Pri tome treba testirati uvjete koji vode na logičke greške unutar programa, a ne greške pri izvršavanju koje dolaze od pogrešnog unosa podataka, pogrešnog korištenja programa, iscrpljivanja resursa i slično. Te greške treba tretirati korištenjem izuzetaka.

Pomoćna klasa std::pair

Par (std::pair) je pomoćni tip deklariran je u zaglavlju <utility>.

Tip std::pair sadrži dva podatka (dvije varijable) općenito različitih tipova koji čine (uređeni) par.

#include <utility> // Za pair
#include <string>

std::pair<std::string, int> ime_i_broj("XCmp", 12345000);
std::cout << "Ime    = " << ime_i_broj.first;
std::cout << ", broj = " << ime_i_broj.second << std::endl;
  • Konstruktor uzima dva argumenta kojima inicijalizira par.

  • Konstruktor bez argumenata vrši incijalizaciju nulama, odnosno pozivom konstruktora bez argumenata na elementima para.

  • Elementi se dohvaćaju kao first i second (javne varijable u klasi std::pair<T1,T2>).

  • Parove je moguće kopirati, pridruživati i uspoređivati ako su kompatiblinih tipova.

Radi uniformnosti pristupa elementi se mogu dohvatiti istom sintaksom kao i kod tuple spremnika:

auto x = std::get<0>(ime_i_broj);  // isto što i x = ime_i_broj.first
auto y = std::get<1>(ime_i_broj);  // isto što i y = ime_i_broj.second

Funcija std::make_pair kreira par iz zadane dvije vrijednosti i pri tome deducira tipove elemenata para iz inicijalizacijskih vrijednosti.

auto par = std::make_pair("ggg", 1.2);
  // par je tipa std::pair<const char*, double>

Tipovi podataka koje std::pair drži mogu biti i reference te konstantne reference. Na primjer,

int i = 0;
std::pair<int&,int&> par3{i,i};
par3.first++;
par3.second++;
assert(i == 2);

Funcija std::make_pair će prepoznati tip podataka kao referencu (konstantnu referencu) ako podatak zamotamo u std::ref() (odnosno std::cref()) koji su definirani u zaglavlju <functional>. Na primjer,

#include <functional>
...
// deducira std::pair<int const &, int const &>
auto par4 = std::make_pair(std::cref(i), std::cref(i));
par4.first--; // greška, referenca je konstantna

Konstrukcija po dijelovima

Konačno, postoji i konstruktor koji elemente para ne kopira iz inicijalizacijskih vrijednosti već ih konstruira pozivajući odgovarajućih konstruktora. Argumenti tog konstruktora para se interpretiraju kao argumenti konstruktora prvog, odnosno drugog člana para. Argumenti ovog konstruktora imaju drukčije značenja od argumenata ostalih konstruktora pa stoga on dobiva dodatni argument std::piecewise_construct koji služi samo da ga razlikuje od ostalih konstruktora.

Tabela 1. Konstruktor

std::pair<T1,T2> p(std::piecewise_construct, t1,t2)

Konstrukcija para u kojem se prvi član para konstruira pozivom njegovog konstruktora s argumentima u t1, a drugi par se konstruira pozivom njegovog konstruktora s argumentima u t2. Varijable t1 i t2 su tipa tuple.

U varijabli t1 se nalaze argumenti konstruktora za tip T1, a u varijabli t2 se nalaze argumenti konstruktora za tip T2. Tip tuple koji se ovdje koristi je generalizacija para i može čuvati proizvoljan broj vrijednosti različitih tipova. Na primjer,

#include <tuple>

struct X{
    X(int i, char c, float f) : mi(i), mc(c), mf(f) {}
    int   mi;
    char  mc;
    float mf;
};
...

std::tuple<int,char,float> a(1,'a',2.0f), b(2,'b',3.0f); // argumenti konstruktora za tip X
std::pair<X,X> par5(std::piecewise_construct, a,b);

std::cout << par5.first.mc << std::endl; // 'a'

Pomoćna klasa std::tuple

Tip std::tuple je generaliracija tipa std::pair i predstavlja uređenu n-torku. On može držati proizvoljan ali fiksan broj vrijednosti različitih tipova koji se zadaju kao parametri predloška. Na primjer,

#include <tuple>

std::tuple<double,int,std::string> t1; // inicijalizacijama "nulama"

Objekt t1 čuva tri vrijednosti redom tipa double, int i std::string; konstruktor bez argumenata vrši vrijednosnu inicijalizaciju (inicijalizacija nulama).

U sljedećem primjeru std::tuple drži dvije vrijednosti inicijalizirane u konstruktoru:

#include <complex>
using namespace std::complex_literals;

std::tuple<double, std::complex<double>> t2(1.0, 1.0i);

Vrijednosti se mogu dohvatiti pomoću funkcije std::get koja uzima indeks pozicije komponente:

std::get<1>(t2) = 3.0+4i;

Funkcija std::make_tuple kreira tuple deducirajući tipove iz argumenata kojima se inicijaliziraju.

auto t3 = std::make_tuple(1,2.0,'v');  // kreira std::tuple<int, double, char>

Kao i kod tipa std::pair pojedini tipovi u std::tuple mogu biti reference. Funkcija std::make_tuple će deducirati referencu na tip (konstantnu referencu na tip) ako argument zamotamo u std::ref (u std::cref) iz zaglavlja <functional>.

std::optional<>

Parametrizirana klasa std::optional<T> (zaglavlje <optional>) može sadržavati vrijednost tipa T ali i ne mora, odnosno može biti prazna. Koristi se za signaliziranje greške kada tražena vrijednost nije dostupna.

Metode članice:

  • has_value() provjerava da li objekt sadrži vrijednost. Postoji i operator konverzije u bool s istom ulogom;

  • value() vraća sadržanu vrijednosti ili izbacuje std::bad_optional_access izuzetak. Istu ulogu ima operator dereferenciranja;

  • value_or(n) vraća vrijednost ili (ako je nema) n.

Dodijeljeni konstruktor konstruira prazan objekt.

// Funkcija vraća int ali može i ne uspjeti.
optional<int> read_int(){
    int n;
    if(cin >> n)
        return n;
    else{
        cin.clear(); // resetiraj stream -- poredak je važan
        cin.ignore(numeric_limits<int>::max(), '\n');
        return {};
    }

}

optional<int> add(optional<int> n, optional<int> m){
    if(n and m)
        return *n + *m;
    else
        return {};
}


int main()
{
    auto n = read_int();
    if(n.has_value())
        cout << "Pročitao " << *n << endl;
    else
        cout << "Neuspješno čitanje.\n";

    auto m = read_int();
    if(m)
        cout << "Pročitao " << m.value() << endl;
    else
        cout << "Neuspješno čitanje.\n";


    if(auto z = add(n,m); z)
        cout << "n+m = " << z.value()  << endl;
    else
        cout << "Greška. " << z.value_or(123) << "\n"; // value_or(n) vrati vrijednost ili n.

    return 0;
}

std::any

U zaglavlju <any> jedefiirana klasa any koja može sadržavati vrijednost bilo kojeg tipa. Pored toga any objekt može biti prazan.

Metode članice:

  • has_value() provjerava da li any objekt sadrži vrijednost;

  • type() vraća typeid sadržane vrijednosti.

Globalna funkcija any_cast<T>(x) vraća kopiju u x sadržanog objekta kao objekt tipa T. Ako sadržani objekt nije tipa T baca std::bad_any_cast izuzetak. Ako rebamo referencu koristimo any_cast<T&>(x).

Primjer:

void print(any const & a){
   if(!a.has_value())
       cout << "Ništa.\n";
   else if(a.type() == typeid(int)){
       cout << "Tip je int, vrijednost = "
            << any_cast<int>(a) << endl;
   }
   else if(a.type() == typeid(string))
       cout << "Tip je string, vrijednost = "
            << any_cast<string const&>(a) << endl;
   else
       cout << "Nepoznati tip.\n";

   return;
}

std::variant<>

Unija u programskom jeziku C (i C++) je struktura koja u istoj memorijskoj lokaciji može držati više varijabli različitih tipova. Služi optimizaciji memorije. Odgovornost je programera da dohvati tip varijable koji je spremljen u uniju.

std::variant<> (zaglavlje <variant>) je moderna verzija unije. Tipovi koje može pohraniti su zadani kao parametri predloška i svi dijele istu memorijsku lokaciju. Objekt tipa std::variant<> uvijek sadrži samo jednu vrijednost i ne može biti prazan.

Metode i globalne funkcije:

  • metoda index() vraća indeks tipa kojeg variant sadrži. Na primjer, ako variant<A,B,C>::index() vrati nulu onda sadrži tip A, ako vrati 1 onda sadrži tip B te ako vrati 2, onda sadrži tip C.

  • globalna metoda get<n>(x) dohvaća element s indeksom n varijanta x (ili izbacuje izuzetak).

  • globalna metoda get_if<T>() uzima adresu variant objekta i vraća pokazivač tipa T na njegov sadržaj. Ukoliko sadržaj nije tipa T vraća nullptr.

  • globalna metoda holds_alternative<T>(x) vraća true ako x sadrži objekt tipa T i false inače.

Primjer.

 list< variant<int, double> > lV{ 1, 2.0, 3, 4.0};
     for(auto const & x : lV){
         switch(x.index()){
           case 0: cout << " Tip = int   : val = " << get<0>(x) << endl;  break;
           case 1: cout << " Tip = double: val = " << get<1>(x) << endl;  break;
        }
    }

Isti primjer pomoću get_if<>:

list< variant<int, double> > lV{ 1, 2.0, 3, 4.0};

for(auto const & x : lV){
        if(auto p=get_if<int>(&x); p)
            cout << " Tip = int   : val = " << *p << endl;
        else if(auto q=get_if<double>(&x); q)
            cout << " Tip = double: val = " << *q << endl;
}

Primjer sa holds_alternative<T>():

template <typename T>
bool is_type(variant<int,double> const &a){
  return holds_alternative<T>(a);
}
// ..
// ne može direktno holds_alternative jer on deducira i tip varianta
cout << "Imamo "
     << count_if(lV.begin(), lV.end(), is_type<int>)
     << " objekata tipa int.\n";

Primjer pomoću globalne funkcije std::visit():

// Funkcijski objekt koji može proći kroz sve tipove u variantu
struct Visitor{
  void operator()(int const & x)
   { cout << "Visitor int: val = " << x << endl; }
  void operator()(double const & x)
   {  cout << "Visitor double: val = " << x << endl; }
};
// ....

for(auto const & x : lV)
     visit(Visitor{}, x);  // primijeni visitora na variant

Generiranje slučajnih brojeva

Biblioteka slučajnih brojeva je dana u zaglavlju <random> i uključuje:

  • Generatore (pseudo) slučajnih brojeva s uniformnom distribucijom ("Random number engines");

  • Različite distribucije — uniformna, normalna, Poissonova itd. Svaka distribucija uzima generator slučajnih brojeva i generira brojeve sa zadanom statističkom distribucijom.

I generatori slučajnih brojeva (SB) i sve distribucije djeluju kao funkcijski objekti. Pozivom generatora SB (bez argumenata) generira se sljedeći SB. Isto tako, pozivom distribucije (sa generatorom SB kao argumentom) dobiva se novi SB po toj distribuciji. Na generatoru je nužno pozvati metodu seed() s nekim promjenjivim brojem (na primjer trenutnim vremenom) kako se ne bi pri svakom pozivanju programa generirao isti slučajni niz.

Primjer

Primjer korištenja dodijeljenog generatora:

#include <iostream>
#include <vector>
#include <random> // Uključi biblioteku slučajnih brojeva
#include <ctime>  // Za trenutno vrijeme time(0)
// ...

std::default_random_engine r_engine; // Generator slučajnih brojeva
r_engine.seed( std::time(nullptr) ); // seed = trenutno vrijeme,osigurava da
                                     // ne generiramo uvijek isti niz brojeva

Iskoristimo normalnu distribuciju sa očekivanjem 0 i standardnom devijacijom 2:

// Normalna distribucija sa očekivanjem 0 i standardnom devijacijom 2.
std::normal_distribution<> nd(0.0, 2.0);
// Svaka distribucija funkcionira kao funkcija koja uzima generator SB
// kao argument i vraća sljedeću SB dane distribucije.
std::vector<double> rand(20);
for(auto & x : rand) x = nd(r_engine);

std::cout << "N(0,2) distribucija.\n";
for(auto & x : rand) std::cout << x << "\n";

Zadaci

Zadatak 1. Generirati vektor od 20 cijelih brojeva po binomnoj distribuciji (npr. t=26, p=0.4). Na generiranom vektoru napraviti ove operacije i ispisati rezultat.

  • Sortirati ga pomoću algoritma sort.

  • Sortirati prvih 5 elemenata niza pomoću partial_sort.

  • Particionirati niz ako da peti element bude na svom mjestu, ispred njega manji elementi, a iza njega veći elementi. Koristiti nth_element.

  • Particionirati elemente tako da svi elementi manji od 10 budu ispred elemenata većih ili jednakih 10. Prvi dio izdvojiti u novi vektor i ispisati ga. Koristiti algoritam partition i lambda izraz.

Zadatak 2. Učitajte iz datoteke stringovi.txt niz stringova u vektor pomoću copy algoritma. Zatim instancirajte set pokazivača na stringove i ubacite u njega adrese učitanih stringova. Ukoliko bismo koristili spremnik set<string *> uređaj bi se odnosio na adrese stringova, a ne na same stringove. Stoga treba setu dati dodatni template parametar koji predstavlja tip binarnog operatora uspoređivanja koji će uspoređivati stringove umjesto pokazivača na stringove. Dakle, treba koristiti set<string *, Cmp> gdje je Cmp klasa koja implementira binarni operator uspoređivanja. Nakon toga ispišite sadržaj set-a na izlazni stream koristeći algoritam transform te ostream_iterator i lambda funkciju koja će dereferencirati pokazivač na string (to je transformacija koja se radi na svakom elementu). Stringovi moraju biti ispisani u leksikografskom poretku.

Zadatak 3. Za normalnu distribuciju N(mean,sigma) ispišite histogram - izračunati relativne frekvencije nalaženja broja u sljedećim intervalima : [-3,-2),[-2,-1),[-1,0),[0,1),[1,2),[2,3). Srednja vrijednost mean i standardna devijacija sigma neka su argumenti kondne linije. Povećajte broj generiranih SB, mijenjajte parametre distribucije. Napravite ispis oblika:

[-3,-2) : xx
[-2,-1) : xxxxxxxxxxxxx
[-1,-0) : xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[0,1)   : xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[1,2)   : xxxxxxxxxxxx
[2,3)   : xx

gdje svaki x predstavlja 1 % relativne frekvencije za dani interval.

Za račun relativnih frekvencija koristite algoritam std::count_if s predikatom zadanim lambda izrazom.