Osnovna svojstva predloška funkcije

template <typename T>
void f(T   x) { ... }

T je parametar, a x je argument.

Dedukcija parametra u predlošku funkcije

Prevodilac deducira parametar predloška funkcije na osnovu tipa argumenta. Imamo 4 slučaja.

template <typename T>
void f(T &  x) { x++; }

Stoga imamo:

int x=1;
const double y = 2.0;

f(x); //  T = int, x je tipa int&
f(y); //  T = const double, y je tipa const double& (greška, ne možemo mijenjati y)
f(1.0);  // greška, rvalue
template <typename T>
void g(const T &  x) {
//  ...
}

onda vrijedi:

int x=1;
const double y = 2.0;
g(x);    // T = int, x je tipa const int &
g(y);   // T = double, y je tipa const double &
g(1.0); // T = double, 1.0 je tipa const double &
Napomena
Ako je argument funkcijskog predloška pokazivač na parametar predloška, onda su pravila esencijalno ista.

Univerzalna referenca (forwarding reference)

template <typename T>
void h(T &&  x) { x++; }

Za ovaj slučaj vrijede posebna pravila:

Na primjer:

int x=1;
const double y = 2.0;

h(x);   // h(int & x)
h(y);   // h(const double & x) -- greška
h(1.0); // h(double && x)
template <typename T>
void k(T x) { x++; }
int x=1;
const double y = 2.0;
const double & z = y;

k(x);  // T = int , x je tipa int
k(y);  // T = double, y je tipa double
k(z);  // T = double, z je tipa double

Univerzalna i desna referenca — razlikovanje

template <typename T>
void ff(T &&  x)   // univerzalna referenca
{
    // ...
}

void ff1(double &&  x) // desna referenca
{
    // ...
}

Drugi kontekst u kojem treba razlikovati desnu i univerzalnu referencu je u primjeni ključne riječi auto.

double && dr = 1.0;  // desna referenca
auto  && ur = dr;    // univerzalna referenca

Kako djeluje std::move ?

std::move funkcija vrši bezuvjetnu transformaciju (cast) u desnu vrijednost. Implementacija:

template<typename T> // namespace std
typename remove_reference<T>::type&& move(T&& param)
{
   using ReturnType = typename remove_reference<T>::type&&;
   return static_cast<ReturnType>(param);
}

Pogledajmo kako std::move djeluje u dva tipična slučaja:

int u = 1;
int v = std::move(u);
int w = std::move(5);
int&& move(int& param)
{
   return static_cast<int&&>(param);
}
int&& move(int&& param)
{
   return static_cast<int&&>(param);
}

Prosljeđivanje tipova (forwarding)

Ponekad želimo proslijediti parametar predloška nekoj pozvanoj funkciji nepromijenjen. Pri tome moramo sačuvati na svakom argumentu konstantnost i svojstvo lijeve odnosno desne vrijednosti. Na primjer, predložak funkcije

template <typename F, typename T1, typename T2>
void call(F f, T1 t1, T2 t2){
    f(t1, t2);
}

treba proslijediti tipove svojih parametara funkciji f koja je definirana ovdje:

void f(int x, int & y){
    cout <<"in:  x = " << x <<", y= " << ++y <<"\n";
}

Problem se vidi na ovom primjeru:

int main()
{
    int x=2, y=3;
    call(f, x, y);
    cout << "out: y = " << y << endl;
    return 0;
}

Ispis:

in:  x = 2, y= 4
out: y = 3

call deducira paramatre T1 i T2 kao int i stoga se prijenos parametra u call dešava po vrijednosti.

Da bismo informaciju o tipu argumenata proslijedili predlošku call on mora deklarirati parametre T1 i T2 kao univerzalne reference. Univerzalna referenca čuva svojstvo lijeve/desne vrijednosti i konstantnost. Ispravnija verzija funkcije call je:

template <typename F, typename T1, typename T2>
void call(F f,T1 && t1, T2  && t2){
    f(t1, t2);
}

Sada će t1 imati tip int &&, a t2 int & i varijabla y će biti izmijenjena. Ispis programa:

in:  x = 2, y= 4
out: y = 4

Problem je time samo napola riješen, jer ako pokušamo iskoristiti funkciju:

void f(int  && x, int & y){
   cout <<"in:  x = " << x <<", y= " << ++y <<"\n";
}

naredba

call(f,2, y); // greška pri kompilaciji

daje grešku pri kompilaciji.

Razlog: Argument t1 ima tip int && no on je svejedno lijeva vrijednost i ne može inicijalizirati desnu referencu.

Da bismo kod ispravili moramo koristiti predložak std::forward<T> iz <utility> zaglavlja.

Ispravna implementacija bi bila:

template <typename F, typename T1, typename T2>
void call(F f,T1 && t1, T2 && t2){
     f(std::forward<T1>(t1), std::forward<T2>(t2));
}

U naredbi

call(f,2, y);

imamo:

Zaključak:

Napomena: std::forward ne može deducirati tip iz parametra već se tip mora zadati eksplicitno.

Povratna vrijednost

Povratna vrijednost se ne deducira.

template <typename RT, typename T1, typename T2>
RT max(T1 a, T2 b)
{
  return (a<b) ? b : a;
}

// ...
cout << ::max(3,5.0)  << endl;  // GREŠKA
cout << ::max<double>(3,5.0)  << endl; // O.K:

Dedukcija je moguća pomoću auto deklaracije funkcije (C++11):

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype((a<b) ? b : a)
{
  return (a<b) ? b : a;
}

Da povratni tip ne bi bio deduciran kao referenca trebamo iskoristiti std::decay<> (definiran u zaglavlju <type_traits>) koji primijenjuje standardne konverzije na tipu koje se dešavaju pri prijenosu parametra po vrijednosti: eliminacija reference, cv kvalifikatora, polje se konvertira u pokazivač i slično.

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype((a<b) ? b : a)>::type
{
  return (a<b) ? b : a;
}

Nužno je iskoristiti typename jer je type tip.

U C++-14 je dovoljno pisati

template <typename T1, typename T2>
auto max(T1 a, T2 b)
{
  return (a<b) ? b : a;
}

i povratni tip će biti deduciran kao i u prethodnom slučaju jer imamo povrat po vrijednosti.

Konačno, moguće je povratnom tipu dati dodijeljenu vrijednost:

template <typename T1, typename T2,
          typename RT = typename std::decay<decltype(true ? T1() : T2())>::type>
RT max(T1 a, T2 b)
{
  return (a<b) ? b : a;
}

U C++-14 možemo typename std::decay<decltype(true ? T1() : T2())>::type zamijeniti sa std::decay_t<decltype(true ? T1() : T2()).

Predložak s proizvoljnim brojem parametara (variadic template)

C++11 dozvoljava predloške klasa i funkcija koje uzimaju proizvoljan broj parametara predloška. Kada se radi o predlošku funkcije ona uzima i proizvoljan broj argumenata.

Primjer: print() funkcija koja ispisuje proizvoljan broj varijabli proizvoljnih tipova:

// predložak koji služi zaustavljanju rekurzije
template <typename T>
void print(std::ostream & out, T t){
    std::cout << t << "\n";
}

// predložak koji uzima jedan ili više paramatara predloška
template <typename T, typename... Args>
void print(std::ostream & out, T t, Args... ostatak){
     std::cout << t << ", ";
     print(out, ostatak...);  // rekurzivni poziv
}
// varijable različitih tipova
double x = 0.17, y = 3.14;
std::string a="alpha", b="beta";
std::complex<double> z1(1,1),z2(0,-1);

// funkcija može biti pozvana s bilo kojim brojem argumenata različitih tipova
print(std::cout, x, y, a, b, z1, z2, "--");

Ovaj se poziv funkciji print() pretvara u niz poziva:

print(std::cout, x, y, a, b, z1, z2,"--");
print(std::cout, y, a, b, z1, z2,"--");
print(std::cout, a, b, z1, z2,"--");
print(std::cout, b, z1, z2,"--");
print(std::cout, z1, z2,"--");
print(std::cout, z2,"--");
print(std::cout,"--"); // običan predložak

Ekspanzija paketa

Način ekspanzije možemo kontrolirati. Na primjer,

template <typename T, template ... Args>
void f(T& t, const Args&... args){
// ...
}

će u slučaju

int i; double j; string s;
f(i, j, s);

ekspandirati predložak u

void f(int &, const double&, const string&);

S druge strane (univerzalna referenca),

template <typename T, template ... Args>
void f(T& t, Args&&... args){
// ...
}

pri pozivu

f(i, j, s);

bit će ekspandiran u

void f(int &, double&, string&);

Prije ekspanzije paketa možemo primijeniti funkciju na paket s efektom da će funkcija biti primijenjena na svaki član paketa. Na primjer, ako je g() funkcija koju možemo primijeniti na svaki član paketa args :

template <typename T, template ... Args>
void f(T& t, Args&&... args){
    // ...
    f(g(args)...);
}

onda pri pozivu

int i; double j; string s;
f(i, j, s);

kod tijela predloška funkcije ekspandira u

f(g(j),g(s));

Prosljeđivanje paketa funkcijskih argumenta

Predložak treba često proslijediti svoje argumente funkciji bez gubljenja informacije o tipovima. U tom slučaju treba:

Primjer. Možemo zamisliti ovakvu implementaciju vektora iz standardne biblioteke:

template <typename T>
class vector{
public:
    // ...
    template <typename... Args>
    void emplace_back(Args&&... args);
    // ...
};

Funkcija emplace_back() bi mogla imati ovakvu implementaciju:

template <typename T>
template <typename... Args>
void vector<T>::emplace_back(Args&&... args)  // (1) univerzalna referenca
{
       //std::allocator<T> alloc; -- u konstruktoru
       // Memorija je alocirana pomoću alokatora alloc,
       // a p je pokazivač na prvo slobodno mjesto

       // (2) primijeni forward na svaki argument
       alloc.construct(p, std::forward<Args>(args)...);
       // ...
}

Specijalizacija predloška

Predložak možemo specijalizirati radi, na primjer, efikasnosti implementacije. Predložak:

// Funkcija koja zamijenjuje a i b. Objekti su dani kroz svoje pokazivače.
template <typename T>
void exchange(T* a, T* b)
{
    T tmp(*a);     // Privremeni objekt
    *a = *b;       // OP
    *b = tmp;      // OP
} // destruktor

Nakon što je predložak funkcije definiran možemo definirati specijalizaciju:

template <>    // SPECIJALIZACIJA!
void exchange(Vect<double>* a, Vect<double>* b)
{
    for(Vect<double>::index k = 0; k < a->n(); ++k )
    {
        double  tmp = (*a)[k];
        (*a)[k] = (*b)[k];
        (*b)[k] = tmp;
    }
}

Specijalizacija predloška - pravila

template <typename T>  // Novi predložak koji preopterećuje prethodni.
void exchange(Vect<T>* a, Vect<T>* b)
{
    for(typename Vect<T>::index k = 0; k < a->n(); ++k )
    {
        T  tmp = (*a)[k];
        (*a)[k] = (*b)[k];
        (*b)[k] = tmp;
    }
}

Dodijeljeni (default) template parametar

Prema standardu iz 2011. parametar predloška funkcije može imati dodijeljenu vrijednost. Sintaksa se vidi na sljedećem primjeru koji koristi operator std::less<T> u tu svrhu.

#include <iostream>
#include <algorithm>

template <typename T, typename F=std::less<T> >
int compare(T const & v1, T const &v2, F f=F())
{
  if( f(v1,v2) ) return -1;
  if( f(v2,v1) ) return  1;
  return 0;
}
 // Objekt za uspoređivanje char-ova. Invertira obično uspoređivanje.
class Char_cmp{
  public:
    bool operator()(char const& a, char const & b){
      return a > b;
    }
};

int main()
{
  int i = compare(1,2);  // 1 < 2 daje -1
  i = compare(2.0,1.0);  // 2.0 > 1.0 daje 1
  i = compare('a','b');  // 'a' < 'b' daje -1
  Char_cmp char_cmp;  // objekt za uspoređivanje
  i = compare('a','b', char_cmp);  // 'b' > 'a'  daje 1

  return 0;
}