int i; // neinicijalizirana varijabla
int j = 6; // inicijalizacija sa 6
int k(6); // inicijalizacija sa 6
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 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
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:
|
Uniformna inicijalizacija ne dozvoljava konverzije u kojima dolazi do gubitka preciznost, dok druge vrste inicijalizacija dozvoljavaju. |
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:
Metoda | Značenje |
---|---|
|
konstantan iterator na prvi element liste |
|
end iterator |
|
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};
}
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
auto x = expr; // slučaj 1
const auto x = expr; // slučaj 1
auto
u konstrukciji tipa ignorira referencu. Ako je referenca bila konstantna,
auto
ignorira i const.
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;
auto & x = expr; // slučaj 2
const auto & x = expr; // slučaj 2
const
neće biti ignoriran.
Jednako tako možemo dodati const
u auto
deklaraciju.
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;
auto && x = expr; // slučaj 3
int i = 1;
auto && k = i; // int & k = i;
auto && k = 3; // int && k = 3;
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>
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.
|
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
|
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;
decltype(*pa)
vraća referencu.
decltype(*pa + 0)
nije referenca.
decltype( (a) )
vraća referencu (varijabla uvijek može stajati na lijevoj strani
operatora pridruživanja).
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;
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;
}
decltype
pravila dedukcijeNova 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 &
.
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.
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] << ",";
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 << ",";
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 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:
return
naredbu, onda se povratni tip deducira iz return
izraza.
return
naredbe, onda je povratni tip void
.
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 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; }
);
|
Ako lambda ne uzima argumente možemo ispustiti i oble zagrade. Jedino uglate zagrade moraju uvijek biti prisutne. |
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":
[]
prazne uglate zagrade znače da lambda ne hvata varijable iz svog okruženja.
[x,y,&z]
hvata tri varijable, x
i y
po vrijednosti i z
po referenci.
[&]
hvata sve varijable iz svog okruženja po referenci.
[=]
hvata sve varijable iz svog okruženja po vrijednosti.
[&,x,y]
hvata sve varijable iz svog okruženja po referenci osim x
i y
koje hvata po vrijednosti.
[=,x,y]
hvata sve varijable iz svog okruženja po vrijednosti osim x
i y
koje hvata po 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
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.
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; }
);
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
|
Imena navedna u enumeraciji eksportirana su u okružujući doseg. |
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;
}
enum
implicitno se konverira u (implementacijski zavisan) cjelobrojni tip.
static_cast
).
enum class
Standard C++11 uvodi novi tip enumeracije koji se definira sa enum class
.
Na primjer,
enum class Boje { plava, crvena, zelena };
enum class
slijedi uobičajena pravila dosega. Izvan enumeracije enumeratori
se mogu dohvatiti samo kvalificiranim imenom enumeracije (Boje::plava
, …)
enum class
ne dozvoljava implicitnu konverziju u cjelobrojni tip niti obratnu konverziju.
U oba slučaja moramo koristiti static_cast<>
.
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;
enum
zovemo enumeracijom vanjskog dosega (eng. unscoped enumeration)
enum class
zovemo enumeracijom unutarnjeg dosega (eng. scoped enumeration).
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 };
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
funkcijeFunkcija 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čenjaconstexpr
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.
|
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.