template <typename T>
void f(T x) { ... }
template <typename T>
void f(T x) { ... }
T
je parametar, a x
je argument.
Prevodilac deducira parametar predloška funkcije na osnovu tipa argumenta. Imamo 4 slučaja.
template <typename T>
void f(T & x) { x++; }
T
je identificiran kao tip bez reference (referencu smo stavili izvan T
).
T
će biti identificiran kao const
.
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:
T
je identificiran kao tip bez reference i bez const
(referencu const
smo stavili izvan T
).
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. |
template <typename T>
void h(T && x) { x++; }
Za ovaj slučaj vrijede posebna pravila:
x
desna vrijednost tipa X
, onda će biti deducirano T=X
(kao što i očekujemo).
x
lijeva vrijednost tipa X
, onda će biti deducirano T=X&
(što je iznimka u odnosu na obična pravila).
X& &
-→ X&
X& &&
-→ X&
X&& &
-→ X&
X&& &&
-→ X&&
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++; }
x
konstantan objekt, konstantnost se zanemaruje pri dedukciji tipa jer nije bitna
kod prenošenja argumenta po vrijednosti.
x
referenca na objekt, referenca se zanemaruje pri dedukciji tipa T
.
T
.
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
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
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 v = std::move(u);
, u
je lijeva vrijednost:
param
je lijeva referenca te je stoga T = int &
.
remove_reference<int &>::type = int
param
daje: T&& = int & && = int &
.
int&& move(int& param)
{
return static_cast<int&&>(param);
}
int v = std::move(5);
, 5
je desna vrijednost:
param
je desna vrijednost te je stoga T = int
.
remove_reference<int>::type = int
param
se ne vrši: T&& = int &&
.
int&& move(int&& param)
{
return static_cast<int&&>(param);
}
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.
std::forward<T>()
traži eksplicitan parametar predloška T
i vrši static_cast
u tip T&&
te na taj način
implementira uvjetni cast u desnu referencu.
T
oblika T = U&
tada je T&& = U&
T
oblika T = U&&
tada je T&& = U&&
T
oblika T = U
tada je T&& = U&&
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:
t1
se inicijaizira s 2
i time se T1
deducira kao int
.
Time dobivamo: std::forward<int> = int &&
(i argument ostaje desna vrijednost).
t2
se inicijaizira s y
i time se T2
deducira kao int &
.
Time dobivamo: std::forward<int &> = int & && = int &
(nema castanja).
Zaključak:
std::forward<T>
na svakom argumentu.
Napomena: std::forward
ne može deducirati tip iz parametra već se tip mora
zadati eksplicitno.
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())
.
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
}
typename...
,
a deklaracija proizvoljnog broja argumenata funkcije sa sintaksom Args...
print()
su zadana eksplicitno sa svojim tipovima, dok svi drugi ulaze u ostatak
.
Args
);
ostatak
).
print(out, ostatak...)
.
Nakon ekspanzije funkcija print
ne vidi dva argumenta već
čitav niz argumenata, ovisno o tome koliko ih je u paketu.
// 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
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));
Predložak treba često proslijediti svoje argumente funkciji bez gubljenja informacije o tipovima. U tom slučaju treba:
std::forward
.
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)...);
// ...
}
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;
}
}
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;
}
}
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;
}