Predložak (programiranje)

Predložak (en. template) mogućnost je objektno orijentiranih jezika da implementiraju koncepte generičkog programiranja, odnosno da rade s poopćenim programskim kodom, koji radi jednako za različite tipove podataka. Time se vrijeme razvoja složenih programa može značajno skratiti, a olakšava se i održavanje programa te ispravljanje eventualnih pogrešaka.

Predložak funkcije

uredi

Predložak funkcije definira funkciju čiji su parametri generičkog tipa. Prilikom prevođenja programa, kompilator će sam generirati definiciju konkretne funkcije na osnovu stvarnih tipova parametara navedenih u pozivu funkcije.

Svaki predložak funkcije ima listu formalnih parametara koja ne smije biti prazna. Svaki formalni parametar mora imati jedinstveni identifikator, koji predstavlja neki potencijalni ugrađeni ili korisnički tip koji će se navesti prilikom poziva funkcije.[1]

Kao primjer je dan predložak funkcije koja, uz pružena dva parametra, vraća veći parametar kao izlaz.

template <typename Tip> 

Tip max(Tip x, Tip y) {
    return x < y ? y : x;
}

Ova se funkcija može pozivati kao i svaka druga, a kompilator će automatski, postupkom specijalizacije, zamijeniti generički tip Tip (i sve druge generičke tipove, ako ih ima više) stvarnim tipom pruženog parametra,[2] pod uvjetom da tip ima definiran binarni operator usporedbe "<". Ako je riječ o korisničkom tipu ili tipu koji obično nema definiran operator "<", isti je potrebno preopteretiti odnosno definirati.

Primjerice, pri pozivu:

std::cout << max(3, 7);  // vraća rezultat 7
std::cout << max(3.2, 7.1);  // vraća rezultat 7.1
std::cout << max('h', 'r');  // vraća rezultat 'r'


U gornjem primjeru funkcija max() poziva se tri puta. Prvi put se kao parametri navode cjelobrojne konstante, drugi put se navode decimalne konstante, a treći put znakonve konstante. Za svaki od poziva stvara se po jedna verzija funkcije. Prevoditelj koristi tipove argumenata da bi odredio koji se predložak funkcije koristi. Zbog toga se i svi formalni argumenti predloška obavezno moraju pojaviti u listi parametara funkcije.

Nakon uparivanja formalnih i stvarnih argumenata prevoditelj će generirati verziju funkcije za dane tipove te će se tada provjeriti sintaksa samog predloška. Prilikom prevođenja samog predloška prevoditelj zapravo uopće ne analizira napisani kôd; on može biti i potpuno sintaktički neispravan. Tek kada se prilikom instanciranja funkcije doznaju tipovi parametara, prevoditelj ima sve podatke potrebne da instancira predložak.

U prvom slučaju se općeniti tip Tip zamjenjuje tipom int, u drugom slučaju s double, u trećem slučaju s char. Nakon supstitucije tipova, stvaraju se tri preopterećene verzije funkcije max(), čiji bi ekvivalent u izvornom kôdu bio:

int max(int x, int y) {
    return x < y ? y : x;
}

double max(double x, double y) {
    return x < y ? y : x;
}

char max(char x, char y) {
    return x < y ? y : x;
}

Pri pisanju predložaka, nužno je postići potpuno slaganje formalnih i stvarnih argumenata funkcije. Nasljeđivanje tipova je dopušteno, no implicitna pretvorba tipova podataka nije dopuštena.

int i = 7;
short int j = 9;
std::cout << max(i, j);  // POGREŠKA!

Iako jezik C++ implicitno promiče tip short int u int, formalni tipovi argumenata predloška (int, int) ne odgovaraju stvarnim tipovima (int, short int), pa će program vratiti pogrešku pri kompilaciji.

Predloške je moguće preopteretiti, kao i sve druge funkcije:

// zbrajanje dvaju objekata
template <typename Tip>
Tip zbroji(Tip a, Tip b) {...}

// zbrajanje triju objekata
template <typename Tip>
Tip zbroji(Tip a, Tip b, Tip c) {...}

Nije moguće preopteretiti funkcije tako da se razlikuju samo u povratnom tipu:

template <typename Tip>
int zbroji(Tip a, Tip b) {...}
// pogreška - predložak se razlikuje samo u povratnom tipu

Moguće je napisati potpunu ili djelomičnu specijalizaciju predloška funkcije (en. function template specialization), za slučaj kada automatski generirane funkcije nisu adekvatne. To se radi na način da se napiše obična funkcija koja pokriva specifičan slučaj. Primjerice, za gornji primjer predloška max, ako bismo pokušali izvesti max("def", "ghi"), autogenerirana funkcija neće ispravno usporediti dva znakovna niza (uvijek će vratiti prvi niz) jer se operator < ne interpretira kao usporedba nizova, nego kao usporedba pokazivača. Puni kod, uz specijalizaciju predloška izgledao bi ovako:

// predložak za općenite tipove
template <typename Tip> 
Tip max(Tip x, Tip y) {
    return x < y ? y : x;
}

// specijalizacija za niz znakova
char *max(char *a, char *b) {
    return strcmp(a, b) < 0 ? a : b;
}

Za sve pozive funkcije max() koristi se odgovarajuća instantacija predloška, osim u slučaju poziva kada se traži maksimum niza znakova. Tada se poziva specijalizacija:

// poziva specijalizaciju
char *rez = max("def", "ghi");

U posljednjoj verziji C++ jezika moguće je prilikom specijalizacije unutar znakova < i > navesti parametre za koje se definira specijalizacija. Na primjer: char *max<char *>(char *a, char *b);

Lista parametara predloška može biti i prazna, ako se oni mogu zaključiti iz liste argumenta funkcije: char *max<>(char *a, char *b);

Java od 2004. godine (verzija J2SE 5.0) koristi vlastitu implementaciju generičkog programiranja koja se naziva generics.

Iako Java i C# podržavaju generičko programiranje putem genericsa, to je tek ograničena kopija osnovnih funkcionalnosti C++ predložaka.[3]

Sintaksa generičke metode zahtjeva popis svih tipskih parametara unutar znakova < i > prije povratnog tipa metode.

Primjer generičke funkcije za ispis u Javi dan je niže:

// Primjer generičke metode
static <Tip> void ispis(Tip element)
{
    System.out.println(element.getClass().getName()
                        + " = " + element);
}

public static void main(String[] args)
{
    // pozivanje generičke metode sa Integer argumentom
    ispis(11);

    // pozivanje generičke metode sa String argumentom
    ispis("Tekst");

    // pozivanje generičke metode sa double argumentom
    ispis(1.0);
}

Preciznija tipizacija

uredi

S obzirom na to da Java ima sigurniju tipizaciju, ponekad je nužno ograničiti generički tip podatka na neki skup. U primjeru koda za pronalazak maks. vrijednosti niže, T je ograničen na sve tipove podataka koji su usporedivi, T extends Comparable<T> (tzv. upper bounded type).

public static <T extends Comparable<T>> T max(T prvi, T drugi) {
    return prvi.compareTo(drugi)>0 ? prvi : drugi;
}

public static void main(String[] args) {
    System.out.println("Integer Max: " + max(Integer.valueOf(32), Integer.valueOf(56)));
    System.out.println("Double Max: " + max(Double.valueOf(5.6), Double.valueOf(7.8)));
    System.out.println("String Max: " + max("Strawberry", "Mango"));
    System.out.println("Boolean Max: " + max(Boolean.TRUE, Boolean.FALSE));
    System.out.println("Byte Max: " + max(Byte.MIN_VALUE, Byte.MAX_VALUE));
}

Gornji se kod može proširiti na neograničeni broj elemenata nad kojima se traži maksimum:

Izvor:[4]
public static <T extends Comparable<T>> T max(T... elements) {
    T max = elements[0];
    for (T element : elements) {
        if (element.compareTo(max) > 0) {
            max = element;
        }
    }
    return max;
}

public static void main(String[] args) {
       System.out.println("Integer Max: " + max(Integer.valueOf(32), Integer.valueOf(56), Integer.valueOf(89), Integer.valueOf(3), Integer.valueOf(456), Integer.valueOf(78), Integer.valueOf(45)));
       System.out.println("Double Max: " + max(Double.valueOf(5.6), Double.valueOf(7.8), Double.valueOf(2.9), Double.valueOf(18.6), Double.valueOf(10.25), Double.valueOf(18.6001)));
       System.out.println("String Max: " + max("Strawberry", "Mango", "Apple", "Pomegranate", "Guava", "Blackberry", "Cherry", "Orange", "Date"));
       System.out.println("Boolean Max: " + max(Boolean.TRUE, Boolean.FALSE));
       System.out.println("Byte Max: " + max(Byte.MIN_VALUE, Byte.MAX_VALUE));
   }

Java dopušta i eksplicitnu deklaraciju nepoznatog tipa podatka koristeći simbol ? (wildcard). Tipizacija varijable sa <?> semantički je jednaka kao da piše <? extends Object>, što uključuje sve tipove podataka (tzv. unbound wildcard). Nemoguće je koristiti ? pri invokaciji generičke metode ili instanciranju generičke klase - tip podatka mora biti moguće zaključiti prije stvaranja objekta tipa zamjenjenog sa ?.

Java dopušta ograničavanje tipizacije i u drugom smjeru. Primjerice, ako želimo kreirati listu koja prihvaća sve elemente koji mogu, između ostaloga, sadržavati cijele brojeve, možemo napisati List<? super Integer>. Takva lista prihvatit će tipove Integer (cijeli broj), ali i Number (bilo koji brojčani tip), i također Object (najopćenitiji tip) jer svi oni mogu sadržavati tip Integer.[5] (tzv. lower bounded wilcard).

Sukladno principima tipizacije u Javi, donji kod je u redu:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  
// OK. List<? extends Integer> je pod-tip od List<? extends Number>

Ovakvo ograničavanje tipizacije na neki podskup korisno je samo za potrebe pisanja programam, kako bi se održala sigurnost tipizacije i spriječile greške koje bi izazvali neočekivani tipovi podataka. Pri kompilaciji Jave u bajtkod, uklanja se sav generički kod za održavanje tipizacije, ostavlja se samo ne-generički tip podatka (primjerice kod List<? extends Integer> postaje List<Integer>) i vrše se implicitne pretvorbe tipova, ako su potrebne.[5] (tzv. type erasure).

Predložak klase

uredi

Klase je također moguće generalizirati, i omogućiti članskim varijablama i metodama da prihvaćaju generičke tipove.

U donjem je primjeru kreirana klasa koja omogućava unos dvije vrijednosti bilo kojeg tipa u klasu imena MojPar. Klasa sadrži članske varijable a i b za spremanje unesenih vrijednosti, te metodu dohvatiMax za traženje veće od dvaju vrijednosti. Pri instanciranju objekta klase nužno je ručno definirati klasni tip.

#include <iostream>
using namespace std;

template <class Tip>
class MojPar {
    Tip a, b;
  public:
    MojPar (Tip prvi, Tip drugi)
      {a=prvi; b=drugi;}
    Tip dohvatiMax();
};

template <class Tip>
Tip MojPar<Tip>::dohvatiMax()
{
    Tip retval;
    retval = a>b? a : b;
    return retval;
}

int main () {
    // definiranje klasnog tipa int
    MojPar<int> par(100, 75);
    cout << par.dohvatiMax();
    return 0;
}

Predlošci klase također se mogu specijalizirati, no u specijalizaciji se ne navodi lista parametara predloška.

Klasa u primjeru niže sprema element generičkog tipa, i sadrži metodu za uvećanje vrijednosti za 1 putem operatora prefiksnog inkrementa (++element). Napisana je potpuna specijalizacija za tip char - umjesto povećanja vrijednosti slova za 1, malo slovo bit će pretvoreno u veliko slovo putem metode VelikoSlovo():

#include <iostream>
using namespace std;

// predložak klase:
template <class Tip>
class Spremnik {
    Tip element;
  public:
    Spremnik (Tip arg) {element=arg;}
    Tip Uvecaj() {return ++element;}
};

// specijalizacija predloška klase - prazna lista parametara <>
template <>
class Spremnik <char> {
    char element;
  public:
    Spremnik (char arg) {element=arg;}
    char VelikoSlovo()
    {
      if ((element>='a')&&(element<='z'))
      element+='A'-'a';
      return element;
    }
};

int main () {
  Spremnik<int> mojBroj(7);
  Spremnik<char> mojZnak('j');
  cout << mojBroj.Uvecaj() << endl;
  cout << mojZnak.VelikoSlovo() << endl;
  return 0;
}

Java u svom programskom kodu intenzivno koristi generičke tipove i predloške, naročito pri definiranju kolekcija (java.util.Collection) poput lista, redova, setova i sličnih struktura.

Primjer klase Spremnik koja sprema i dohvaća podatak o jednom objektu tipa Object:

public class Spremnik {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

S obzirom na to da je Object najopćenitiji tip koji sadrži sve druge ne-primitivne tipove u jeziku Java, kompilator ne može provjeriti na koji se način klasa koristi. Neki dio koda može unutra spremiti jedan cijeli broj, a kasnije pokušati pročitati listu brojeva (što će, naravno dovesti do pogreške i pada programa).

Stoga je poželjno ovu klasu napisati kao predložak:

public class Spremnik<Tip> {
    private Tip t;

    public void set(Tip t) { this.t = t; }
    public Tip get() { return t; }
}

Pri instanciranju objekta ove klase, nužno je definirati tip tog objekta, što sprječava pogrešku iz prošlog primjera:

Spremnik<Integer> broj = new Spremnik<Integer>();
Spremnik<String> tekst = new Spremnik<String>();

broj.set(5);            // OK
tekst.set("Krokodil");  // OK

broj.set("Krokodil");   // GREŠKA
tekst.set(80);          // GREŠKA

U kasnijim inačicama Jave (od SE7 nadalje), uveden je en. diamond operator – „dijamantni operator” koji omogućava kompilatoru da sam odredi tip podatka klase iz tipa varijable.[6] Uvođenjem dijamantnog operatora, klase se mogu deklarirati ovako:

Spremnik<Integer> broj = new Spremnik<>();    
    // kompilator zaključuje da je spremnik tipa Integer jer je 
    // varijabla "broj" tipa Spremnik<Integer>

Spremnik<String> tekst = new Spremnik<>();    // kompilator zaključuje da je spremnik tipa String

Na isti je način mofuće deklarirati i sučelja. U sljedećem je primjeru deklarirano sučelje Pair (par) s dva generička tipa, K i V (predstavljaju ključ i vrijednost). Klasa OrderedPair (uređeni par) implementira to sučelje.[6]

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

Predložak klase moguće je instancirati s bilo koja dva tipa podataka:

Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Osam", 8);
Pair<String, String>  p2 = new OrderedPair<String, String>("Bok", "svima");

// koristeći dijamantni operator
Pair<Integer, String>  p2 = new OrderedPair<>(31, "Trideset jedan");

Moguće je također instancirati predložak koristeći parametrizirani tip[6]:

OrderedPair<String, Spremnik<Integer>> p = new OrderedPair<>("tekst", new Spremnik<>(...));

Izvori

uredi
  1. Template parameters and template arguments. CPP Reference. Pristupljeno 11. kolovoza 2022.
  2. Templates. CPP Reference. Inačica izvorne stranice arhivirana 24. svibnja 2022. Pristupljeno 11. kolovoza 2022.
  3. Differences Between C++ Templates and C# Generics (C# Programming Guide)
  4. Finding Max value using Java Generics. 11th Hour. 17. siječnja 2013. Inačica izvorne stranice arhivirana 2. srpnja 2015. Pristupljeno 12. kolovoza 2022.
  5. a b Java Generics Example Tutorial - Generic Method, Class, Interface. Pristupljeno 12. kolovoza 2022.
  6. a b c Generic Types. Oracle Java Documentation. Inačica izvorne stranice arhivirana 5. svibnja 2022. Pristupljeno 12. kolovoza 2022.