Inicijalizacija u C++-u

Varijable možemo inicijalizirati različitom sintaksom:

int i;       // neinicijalizirana varijabla
int j = 6;   // inicijalizacija sa 6
int k(6);    // inicijalizacija sa 6

Polja možemo inicijalizirati inicijalizacijskom listom:

int ivec[] = {1,2,3};

Inicijalizacijsku listu koristimo i s dinamički alociranim poljima:

double * pvec = new double[5]{1.1,2.2,3.3,4.4,5.5};
 // ..
delete [] pvec;

Višedimenzionalna polja

Višedimenzionalna polja inicijaliziramo listom vrijednosti na prirodan način.

int va[2][3] = {      // inicijalizacija višedimenzionalnog polja
                      {1,2,3}, // prvi redak
                      {4,5,6}  // drugi redak
                     };

int vb[2][3] = {1,2,3,4,5,6}; // ovo daje posve istu inicijalizaciju

int vc[3][4] = {{1},{2},{3}}; // drugi elementi su inicijalizirani
                              // prevodiocem; u svakom je retku prvi
                              // element inicijaliziran

int vd[3][4] = { 1, 2, 3 }; // drugi elementi su inicijalizirani
                            // prevodiocem; elementi prvog retka
                           // su inicijalizirani

Uniformna inicijalizacija

C++11 uvodi novu sintaksu inicijalizacije - uniformnu inicijalizaciju. Ona treba zamijeniti ostale oblike inicijalizacije.

int l{6};    // l inicijalizira sa 6

Time imamo četiri načina kojim možemo inicijalizirati varijablu:

int i = 1;   // incijalizacija kopiranjem
int j(1);    // direktna inicijalizacija
int k{1};    // uniformna inicijalizacija
int l = {1}; // uniformna inicijalizacija (ne koristiti!)

Svi su ti oblici ekvivalentni osim što:

Napomena Uniformna inicijalizacija ne dozvoljava konverzije u kojima dolazi do gubitka preciznost, dok druge vrste inicijalizacija dozvoljavaju.

Inicijalizacija listom i STL spremnici

STL spremnici dozvoljavaju inicijalizaciju listom kada to ima smisla (C++11):

std::list<int> d{1,2,3,4,5};  // 5 elemenata

std::deque<std::string> e{"aa","bbc"};

std::set<std::string> set{"dune","deal.ii","libmesh"};

std::map<std::string, std::string> map{
 {"Newton", "Isac"}, {"Euler", "I."}, {"Gauss", "F."}
                                       };

Nepoznavanje sintakse lako dovodi do pogrešaka:

std::vector<int> a(3);        // 3 elementa
std::vector<int> b(3,1);      // 3 elementa inicijalizirana s 1
std::vector<int> c{3};        // Inicijalizacija jednim elementom
                              // jednakim 3
std::vector<int> c{3,1};      // 2 elementa:  3 i 1

std::initializer_list<T>

Ako želimo da naša funkcija može uzeti inicijalizacijsku listu kao parametar trebamo iskoristiti klasu std::initializer_list<T> koja je definirana u zaglavlju <initializer_list>.

#include <initializer_list>

Klasa std::initializer_list<T> predstavlja listu argumenata, te nudi samo tri metode:

Tabela 1. Metode
Metoda Značenje

begin()

konstantan iterator na prvi element liste

end()

end iterator

size()

broj elemenata u listi

Klasa sadrži jedino konstantne vrijednosti (tipa T).

std::initializer_list<T> - primjer

// #include ...

// funkcija koja prima proizvoljno nmogo parametara tipa int
void f(std::initializer_list<int> il) {
  // using u C++11 zamjenjuje typedef
  using Iterator = std::initializer_list<int>::iterator;

  for(Iterator it = il.begin(); it != il.end(); ++it)
        std::cout << *it << " " ;
  std::cout << std::endl;
}

Funkciju f() zovemo na sljedeći način:

f({1,2,3,4,5});
f({34,56});

Ako tip koji funkcija vraća dozvoljava inicijalizaciju inicijalizacijskom listom, onda u return naredbi možemo koristiti inicijalizacijsku listu:

std::vector<double> g()
{
  return {0.1,0.2};
}

Automatska dedukcija tipa: auto

Kada želimo izračunatu vrijednost nekog izraza spremiti u varijablu trebamo odrediti tip te varijable. Taj posao možemo prepustiti prevodiocu jer on ionako zna tip izraza; dovoljno je varijablu deklarirati s auto:

int x = 1;
double y = 1.0;

auto z = x + y;  // prevodilac će učiniti z tipa double

Taj je mehanizam najkorisniji s predlošcima, gdje stvarni tip ne znamo.

template <typename T1, typename T2>
void f(T1 t1, T2 t2)
{
        auto t = t1 + t2;
        // ...
}

Postoji potpuna analogija između dedukcije tipa pri pozivu predloška funkcije i auto dedukcije tipa. Auto dedukcija se može javiti u različitim oblicima ovisno o dekoracijama koje prate auto:

auto x = expr;         // slučaj 1
const auto x = expr;   // slučaj 1
auto & x = expr;       // slučaj 2
const auto & x = expr; // slučaj 2
auto && x = expr;      // slučaj 3

Automatska dedukcija tipa: Slučaj 1

auto x = expr;         // slučaj 1
const auto x = expr;   // slučaj 1
int i = 1;
const int & k = i;

auto k1 = k;  // daje:  int k1 = k;

const se ne ignorira kod pokazivača na konstantne objekte:

auto pk = &k; // const int * pk = &k;
auto pi = &i; // int * pi = &i;

Automatska dedukcija tipa: Slučaj 2

auto & x = expr;       // slučaj 2
const auto & x = expr; // slučaj 2
int i = 1;
const int & k = i;

auto & k2 = k;     // const int & k2 = k;
const auto ii = i; // const int ii = i;

Konzistentnih auto deklaracija možemo imati više u jednoj liniji, jednako kao i standardnih:

auto v = 0.0, *pv = &v;

Automatska dedukcija tipa: Slučaj 3

auto && x = expr;      // slučaj 3
int i = 1;

auto  && k = i; //  int & k = i;
auto  && k = 3; // int && k = 3;

auto i univerzalna inicijalizacija

Ako varijablu inicijaliziramo izrazom u vitičastim zagradama i koristimo auto za dedukciju tipa, auto će deducirati std::initializer_list. Stoga auto ne treba koristiti u takvoj situaciji.

auto v1{10};     // v1 je tipa int
auto v2={10};    // v2 je tipa int ili std::initializer_list<int>
                 // kod starijih prevodioca
auto v3={10,11}; // v3 je tipa std::initializer_list<int>

Automatska dedukcija tipa: decltype

auto omogućava određivanje tipa varijable na osnovu tipa izraza samo pri inicijalizaciji varijable. U drugim slučajevima koristimo decltype.

decltype(f()) sum = 0.0; // sum ima tip koji vraća f()

decltype određuje tip varijable sum na osnovu povratnog tipa funkcije f(). Pri tome se sama funkcija ne izračunava.

Napomena Kada se decltype primijenjuje na varijablu on vraća tip varijable bez ignoriranja konstantnosti i reference.
const double xx = 0.0;
const double & yy = xx;

decltype(xx) uu = 1.0; // const double uu = 1.0;
decltype(yy) vv;    //  const double & vv; greška
                    //  neinicijalizirana referenca

decltype i izrazi

Napomena Kada se decltype primijenjuje na izraz dobit će se lijeva referenca ako izraz ima lijevu vrijednost. Ako izraz ima desnu vrijednost dobiva se tip bez reference.
double a = 1.0, *pa = &a, &ra = a;
decltype(ra) newa = a; // double & newa = a;
decltype(*pa) nnewa;   // double & nnewa; greška
                       // neinicijalizirana referenca
decltype(*pa + 0) ab;  // double ab;
decltype( (a) ) ac;    // greška - double & ac;

Kako prikazati deducirani tip?

Postoji više načina. Jedan je iskoristiti prevodilac:

template <typename T>
class TD;      // Predložak klasa nije definiran što će inicirati
               // grešku pri prevođenju

int main(){
  const int i = 1024;
  auto & j = i;

  TD<decltype(j)> td; // prevodilac će pokazati tip decltype(i)

Ispis greške prevodioca:

auto-decltype.cpp: In function int main():
auto-decltype.cpp:10:19: error: aggregate TD<const int&> td
has incomplete type and cannot be defined
   TD td;

Nova sintaksa funkcije

C++2011 dozvoljava sintaksu funkcije u kojoj povratni tip dolazi nakon funkcijskih argumenata (trailing return type):

auto f() -> int
{
   int tmp = 3;
   // ...
   return tmp;
}

Takva sintaksa je najkorisnija kod predložaka funkcije jer se povratni tip može deducirati pomoću decltype:

template <typename T1, typename T2>
auto g(T1 const & t1, T2 const & t2) -> decltype(t1*t2)
{
        return t1*t2;
}

Nova sintaksa i decltype pravila dedukcije

Nova sintaksa upotrebljava decltype pravila za dedukciju tipa. Na primjer, uz definiciju

template <typename C, typename I>
auto g(C & container, I index) -> decltype(container[index])
{
    return container[index];
}

sljedeći kod je ispravan:

std::vector<double> vec{1.0,2.0};
g(vec,1) = 3.14;

jer je deducirani povratni tip double &.

Nova sintaksa funkcije i C++14

Prema standardu C++14 prevodilac može u potpunosti deducirati povratni tip (ako u svim return naredbama nađe izraze istog tipa). Stoga možemo pisati:

template <typename C, typename I>
auto g(C & container, I index)
{
    return container[index];
}

Ali, sada se primijenjuju auto pravila za dedukciju tipa. To znači da u primjeru

std::vector<double> vec{1.0,2.0};
g(vec,1) = 3.14; // greška

deducirani tip postaje double jer auto zanemaruje referencu. C++14 nudi sintaksu

template <typename C, typename I>
decltype(auto) g(C & container, I index)
{
    return container[index];
}

kojom ponovo dobivamo decltype pravila za dedukciju povratnog tipa.

range-for petlja

Novi standard nudi pojednostavljenu for-naredbu sljedećeg oblika (tzv. range-for petlja):

for(deklaracija : izraz)
    naredba;

Ovdje izraz predstavlja niz elemenata — polje, višedimenzionalno polje, niz u vitičastim zagradama, string, vector ili bilo koji STL spremnik koji ima begin() i end() metode.

deklaracija mora biti takva deklaracija varijable da se element niza može konvertirati u tu varijablu. Na primjer,

char name[] = { 'a','b','c','d','e'};
for(char x : name) std::cout << x << ",";

U svakom prolazu petlje, x dobiva novu vrijednost iz name. Petlja je ekvivalentna sa

for(int i=0; i<5; ++i)  std::cout << name[i] << ",";

range-for i auto

Kod deklaracije varijable u range-for petlji prirodno je koristiti auto i pustiti prevodiocu da deducira tip elementa u spremniku:

for(auto x : name) std::cout << x << ",";

Ukoliko mijenjamo elemente spremnika treba varijablu deklarirati kao referencu:

for(auto& x : name)  x += 1;

Svi STL spremnici koji imaju begin i end metodu mogu se koristiti u range-for petlji, kao i eksplitno zadani nizovi:

std::vector<double> vec{1.1,2.2,3.3};
for(auto x : vec) std::cout << x << ",";
for(auto x : {"aaa","bbb","ccc"}) std::cout << x << ",";

range-for i polja

Kod višedimenzionalnih polja treba redak definirati kao referencu kako bi izbjegli automatsku konverziju niza u pokazivač na prvi element niza:

int table[][3] = { {1,2,3}, {4,5,6}, {7,8,9} };

for(auto& row : table){
   for(auto col : row) std::cout << col << ",";
       std::cout << std::endl;
}

Lambda izrazi

Lambda izrazi (lambda expressions) su posebni funkcijski objekti koji se mogu opisati kao bezimene inline funkcije. Sintaksa je slijedeća.

[lista varijabli iz okruženja](lista parametara) -> povratni tip
{ tijelo funkcije }

Primjer. Sljedeća lambda izračunava treću potenciju broja:

auto p3 = [](int i) -> int { return i*i*i; };
int j = p3(7);

Povratni tip (koji se piše novom sintaksom) ne moramo specificirati i tada se deducira. C++11 pravila kažu:

Prema standardu C++14 povratna vrijednost se uvijek deducira prema auto pravilima dedukcije. Na primjer:

auto r3 = [](double r) {return r*r*r; }; // povratni tip se deducira kao double
double r = r3(12.0);

Lambde i algoritmi

Lambde najčešće koristimo kao argumente algoritama:

std::sort(words.begin(), words.end(),
                [](const std::string & a, const std::string & b)
                           { return a.size() < b.size(); }
           );


std::for_each(words.begin(), words.end(),
               [](const std::string & a)
                 { std::cout << a << std::endl; }
               );
Napomena Ako lambda ne uzima argumente možemo ispustiti i oble zagrade. Jedino uglate zagrade moraju uvijek biti prisutne.

Hvatanje varijabli iz okruženja (capture)

Lambda može dohvatiti varijable iz svog lokalnog okruženja navodeći ih u uglatim zagradama. Varijable se pišu odvojene zarezom. Prijenos varijabli je po vrijednosti, odnosno po referenci ako se & stavi ispred imena varijable.

template <typename T>
void print(std::ostream & out, std::vector<T> const & container)
{
  std::for_each(container.begin(), container.end(),
      [&out](T const & t) { out << t << std::endl; } );
}

Lambda vrši "hvatanje" varijabli tamo gdje je deklarirana, a ne tamo gdje je pozvana, što znači da varijabla koju hvatamo mora biti definirana prije lambde. Sljedeći su oblici "hvatanja":

Hvatanje po vrijednosti i referenci

Kada varijablu iz okruženja uhvatimo po vrijednosti ona se kopira unutar lambda-izraza i njene daljnje promjene ne utječu na lambda izraz. Štoviše, kopija varijable unutar lambda izraza je konstantna i ne možemo ju mijenjati.

double pi = 3.14;
auto f = [pi]() { pi += 0.001; return pi*pi; };  // greška

Ukoliko imamo potrebu promijeniti varijablu uhvaćenu po vrijednosti trebamo lambdu deklarirati mutable:

double pi = 3.14;
auto f = [pi]() mutable { pi += 0.001; return pi*pi; };   // o.k.

Ako varijablu hvatamo po referenci, onda dobivamo referencu na varijablu i naša je odgovornost da uhvaćena varijabla ima životni vijek koji nije kraći od vijeka lambde. Varijabla uhvaćena po referenci nije konstantna i svaka njena promjena reflektira se na lambdu.

double pi = 3.14;
auto f = [&pi]() { pi += 0.001; return pi*pi; };

pi = 6;
std::cout << f() << std::endl; // ispisuje 36.012

Init capture

U standardu C++14 hvatanje varijabli iz okruženja je generalizirano i sada se može unutar uglatih zagrada koristiti sintaksa var = expr (ili &var = expr). Na primjer,

auto g = [pi = pi](double x) { return pi*x; };
std::cout << g(1.11) << std::endl;

U sintaksi var = expr, var je varijabla unutar dosega lambde, dok je expr izraz unutar okružujućeg dosega. To je razlog zbog kojega u prethodnom primjeru možemo imati isto ime. Ova sintaksa daje puno više mogućnosti, na primjer:

auto g = [pi = std::move(pi)](double x) { return pi*x; };

gdje premještamo varijablu pi u lambdu.

Generički lambda izrazi

U dosadašnjim primjerima su argumenti lambda izraza morali biti eksplicitno navedeni. Standard C++14 uvodi mogućnost da argumente deklariramo s auto. U tom slučaju se vrši standardna auto dedukcija parametara.

std::vector<int> vec{6,7,3,5,2,1,9,0,6};
std::sort(vec.begin(), vec.end(),
              [](auto x, auto y) { return x < y; }
         );

Enumeracija (vanjskog dosega) enum

Kada niz simboličkih imena želimo koristiti umjesto cjelobrojnih konstanti, onda koristimo enumeraciju.

enum Boja {
        plava, crvena, bijela, crna, siva
    };

Svaka enumeracija uvodi novi tip u program. Enumeratori (plava, crvena, …) su cjelobrojne konstante; prva dobiva vrijednost 0, druga 1, treća 2 itd.

Ako nam ne odgovaraju vrijednosti koje prevodilac pridružuje enumeratorima možemo im eksplicitno dati vrijednosti. Na primjer,

enum Boja {
        plava=12, crvena, bijela=10, crna, siva
    };

daje vrijednosti

plava = 12, crvena =  13, bijela = 10, crna = 11, siva = 12
Napomena Imena navedna u enumeraciji eksportirana su u okružujući doseg.

Primjer korištenja enumeracije

int main() {
    enum Boja     // Uvodi novi tip (Boja) u program.
    {
        plava, crvena, bijela, crna, siva
    };

    Boja  fasada = plava; // fasada je varijabla tipa Boja

    std::cout << "fasada = " << fasada
              << ", crvena =  " << crvena << std::endl;

    crvena = plava;  // Greška: plava, crvena,... su konstante
    int crvena;      // Greška: Ne možemo redefinirati ime crvena
    fasada = 1;      // Greška: int se ne može konvertirati u tip Boja
    return 0;
}

Enumeracija unutarnjeg dosega: enum class

Standard C++11 uvodi novi tip enumeracije koji se definira sa enum class. Na primjer,

enum class Boje { plava, crvena, zelena };
Boje x = Boje::plava;
switch(x) {
    case Boje::plava :
        cout << "plava = " << static_cast<int>(x);
        break;
    case Boje::crvena :
        cout << "crvena = " << static_cast<int>(x);
        break;
    case Boje::zelena :
        cout << "zelena = " << static_cast<int>(x);
        break;
}
cout << endl;

Neki detalji

Eksplicitno zadavanje implementacijskog tipa

U C++11 implementacijski se tip može eksplicitno zadati. Na primjer, u sljedećoj deklaraciji se implicitni integralni tip definira kao char:

enum Colors : char { blue, red, green };

Anonimne enumeracije

Enumeracija vanjskog dosega može biti anonimna. Ukoliko ime enumeracije ne koristimo, možemo ga naprosto ispustiti.

enum{ NoEquations = 2, NoVariables = 3};

Anonimne enumeracije nisu dozvoljene kod enumeracija unutarnjeg dosega.

constexpr

Konstantan izraz je izraz koji se može izračunati za vrijeme kompilacija i čija se vrijednost ne može promijeniti.

const int n = 1024;    // konstantan izraz
const double xx = n+1; // konstantan izraz
int m;
//
const int mm = m; // nije konstantan izraz

C++11 nudi ključnu riječ constexpr kojom možemo deklarirati varijable koje predstavljaju konstantne izraz

constexpr int mm = m;   // greška - nije konstantan izraz
constexpr double u = 5; // konstantan izraz

Tipovi koji se mogu koristiti u ‘constexpr` deklaracijama su reducirani na tzv. literalne tipove, odnosno tipove koji su dovoljno jednostavni da mogu primati vrijednosti dane eksplicitnim konstantama (1, 2, 'a’, 'b' itd). U literalne tipove spadaju svi skalarni tipovi, pokazivači, reference, polja literalnih tipova, agregacije itd.

constexpr funkcije

Funkcija može biti deklarirana constexpr s namjerom da se koristi u konstantnim izrazima (odnosno da se izračunava za vrijeme kompilacije).

Na primjer,

constexpr int pow2(int n){
    int k = 1;
    for(int i=0; i<n; ++i) k *= 2;
    return k;
}

Zatim ju možemo koristiti za izračun parametra predloška array:

array<double, pow2(4)> arr1;

Izraz pow2(4) bit će izračunat za vrijeme kompilacije i čitava inicijalizacija varijable arr1 je ispravna i identična deklaraciji

array<double, 16> arr1;

constexpr funkcije - ograničenja

constexpr funkcije imaju određena ograničenja u odnosu na obične funkcije. Njihovi argumenti i povratna vrijednost moraju biti literalni tipovi. Pored toga, budući da se tjelo funkcije izračunava za vrijeme kompilacije, postoje i neka ograničenja na sadržaj tijela: nisu dozvoljeni try-blokovi, goto naredbe itd.

Napomena constexpr funkcije su uvedene u standardu C++11 i u njemu postoje vrlo velika ograničenja na sadržaj tijela constexpr funkcije: tijelo se može sastojati samo od jedne return naredbe, no funkcija se može pozivati rekurzivno. Ta su ograničenja uklonjena u C++14 standardu. Koja ograničenja još uvijek postoje može se vidjeti na stranici cppreference.com.

Bitno je svojstvo constexpr funkcije da se može koristiti i kao obična funkcija. Ako constexpr funkciju pozovemo s argumentima koji nisu poznati za vrijeme kompilacije tijelo funkcije će biti izvršeno na normalan način, za vrijeme izvršavanja programa.

int nn;
cin >> nn;
double y = pow2(nn);

Na taj način ne moramo imati dvije funkcije iste funkcionalnosti — jednu običnu, a drugu za izračunavanje za vrijeme kompilacije.