Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: mysql_connect(): Headers and client library minor version mismatch. Headers:50562 Library:100020 in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: Cannot modify header information - headers already sent by (output started at /home/www/novacisko.cz/subdomains/bredy/init.php:11) in /home/www/novacisko.cz/subdomains/bredy/index.php on line 38
Funkce s volitelným počtem argumentů v C++ II - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

Funkce s volitelným počtem argumentů v C++ II

V předchozím díle jsme si ukázali, jak pomocí templatů vytvořit funkci přijímající libovolný počet argumentů stejného typu. Šlo by to udělat i pro argumenty libovolných typů?


Zkusme si nejprve vzpomenout, jak napsat funkci přijímající jeden argument libovolného typu. Máme několik možností.

  1. Použít společného předka
  2. Použít void * spolu s informací o typu RTTI.
  3. Použít template

Ad 1) máme situaci jednoduchou. I v případě libovolného počtu argumentů můžeme použít template z prvního dílu.

Ad 2) zatím přeskočíme, na konci článku budeme chtít i tuto variantu implementovat pro libovolný počet argumentů.

Ad 3) Zbývá nám template. Funkce s jedním parametrem bude mít tuto formu.

template<class T1>
void fn(const T1 &param1)
{
}

pro dva parametry

template<class T1,class T2>
void fn(const T1 &param1,const T2 &param2)
{
}

pro tři parametry

template<class T1,class T2, class T3>
void fn(const T1 &param1,const T2 &param2, const T3 &param3)
{
}

A tak dále.

Trochu se nám to komplikuje, protože to znamená, že budeme potřebovat nejen libovolný počet argumentů funkce, ale i libovolný počet argumentů template. Ale standardní cestou toho nedocílíme.

Rekurzivní seznam

Šablony a rekurze jsou nerozlučnými přáteli. Naštětí máme už dob programovacího jazyka PROLOG nástroj jak rekurzivně tvořit seznamy.

Proč vlastně potřebujeme seznam?

Vzpomeňme si na první díl, kde jsme vytvářeli prostor pro argumenty a z výhodou jsme použili klasické pole, protože typ byl stejný. Zde však budeme muset vytvářet seznam typů podle použitých argumentů a následně i prostor pro každý argument adekvátního typu.

Takže jak na ten seznam?

Definujme si seznam rekurzivně tak, že seznam se skládá z hlavy H a těla T. Hlava je jeden prvek zatímco tělo T je prostě zbytek seznamu skládající ze zbývajících prvků. Tento seznam samozřejmě je opět rozdělen na hlavu a tělo. Situace se dá zobrazit takto:

a|b,c,d,e,f,g,h
b|c,d,e,f,g,h
c|d,e,f,g,h
d|e,f,g,h
atd.

nebo ještě jeden způsob

(a,(b,(c,(d,(e,(f,(g,(h))))))))

Důležité je, že umíme v libovolné části seznamu oddělit hlavu od těla. Další vyhodou je, že seznam je v každém případě vyjádřen dvěm prvky. Hlavou a Tělem. A tím vyřešíme to, že seznam může být libovolně dlouhý.

Třída VarArg

Založme si třídu VarArg. Bude na začátku prázdná, a postupně do ní budeme přidávat členy;

class VarArg
{
}

funkce nemusí být template, protože typy jsou různé a protože nealokujeme žádné pole, nemusíme znát dopředu počet argumentů.

Určitě budeme potřebovat operátor čárky. Aby třída byla schopna přijmout první argument za čárkou.

class VarArg
{
template<class T>
List<T,EmptyTail> operator,(const T &other)
{
return List<T,EmptyTail>(other,EmptyTail());
}
}

Jako návratový typ z funkce využiji třídu List, kterou následně nadefinujeme. Už nyní musíme počítat s tím, co jsme si uvedli v předchozím odstavci, že každý seznam se skládá z hlavy a těla. Třída List bude právě představovat tuto dvojici. Jako hlavu specifikujeme typ argumentu, a jako tělo musíme vložit něco co představuje prázdný seznam, zde to bude třída EmptyTail, kterou také nadefinujeme.

Konstruktor třídy List pak pojme dva argumenty, první představuje přímo referenci na vkládaný prvek a druhý reference na zbytek seznamu, zde prázdnou instanci třídy EmptyTail.

Třída List

 template<class H,class T>
class List
{
const H &head;
const T &tail;
typedef List<H,T> ThisType;
public:
List(const H &head, const T &tail):head(head),tail(tail) {}
}

Jak vidíme, třída obsahuje jak referenci na prvek který je hlavou, tak na zbytek seznamu. Instance třídy si nese nejen hodnoty argumentů, ale i jejich typy.

Aby bylo možné řetězit operátor čárky, musíme operátor definovat i v této třídě (protože první čárka vrátila instanci třídy List)

template<class Type>
List<Type,ThisType> operator,(const Type &other) const
{
return List<Type,ThisType>(other,*this);
}

(deklarace bude uvnitř List)

Vidíme, že funkce vytváří novou dvojici hlavy a těla. Do těla vloží sebe a jako hlavu použije argument. Mozná si všimnete, že seznam se tvoří v opačném pořadí, než vkládáme argumenty. To nevadí, můžeme to řešit později.

Každopádně se uloží nejen vlastní hodnota, ale i její typ.

Protože zde není omezení na počet parametrů, můžeme opravdu zadat libovolný počet argumentů, nebude zde žádná zarážka která by nás zastavila. V tom případě jsme hotovi.

Zbývá ještě nadefinovat EmptyTail plnící funkci prázdného seznamu. Zatím neobsahuje nic, ale i do ní postupně dopíšeme operace, které bude třeba provádět i nad prázdným seznamem.

class EmptyTail
{
};

Rozšiřujeme funkce třídy VarArg (List) - přístup k argumentům

Zatím nevíme jak budeme přistupovat k argumentům. Nabízí se několik způsobů:

  1. indexace - jednotlivé argumenty indexujeme od 0 do N kde N je index posledního argumentu.
  2. forEach - pro každý argument je zavolán funktor
  3. forOne - pro zadaný argument (indexem) je zavolán funktor.

Pro indexaci budeme potřebovat vědět, kolik argumentů vlastně bylo přidáno. Tuto informaci získáme už v době překladu, takže v runtime zde bude konstanta:

Do třídy List (public části) dopíšeme:

static const int ListSize=T::ListSize+1;

aby to fungovalo, musíme do třídy EmptyTail dopsat:

static const int ListSize=0;

To proto, že zápis v prvním řádku řiká, že délka seznamu je dána dálkou těla +1. Druhý řádek říká, že délka prázdného seznamu je 0. Výpočet se provede během překladu (protože se sčítají konstanty).

Dopíšeme ještě funkci Size()

static int Size() {return ListSize;}
dodatek
Překvapuje vás, že funkce Size je static? Nebude to překladač hlásit jako chybu? Nebude, celý vtip je v tom, že třídá vzniká na míru daného použití. Pokud tedy voláte funkce s třemi parametry typu int,int,string pak pro tuto kombinaci vzniká konkrétní varianta třídy LIST, která je unikátní v celé aplikace (nebo případně se používá zade jen tam, kde se vyskytne stejná kombinace parametrů). Vlastně je počet parametrů "zapečeno" do identifikace třídy (jako by se třída jmenovala TridaProTriParametryIntIntString - konečně, nějak tak se nakonec jmenovat bude, i když to bude trochu komplikované jmeno: List<int,List<int,List<String,EmptyTail> > > > - ještě někdo pochybuje, že static declarace stačí?)

Indexace

Pokud jde o indexaci prvním způsobem, tak narazíme na problém. V třídě List definuji funkci Get, která vrací argument na zadané pozici. Otázkou je, jakého typu, protože v runtime již typy nejsou známy a během překladu není znám index (není-li to konstanta).

Typ je třeba funkci Get poskytnout.

Dopíšeme do třídy List funkci:

 template<class RetType>
RetType Get(int pos) const
{
if (pos+1!=ListSize) return tail.Get<RetType>(pos);
else return head;
}

Funkce vezme pozici a zkontroluje, zda je pozice rovná délkce seznamu -1. Pakliže ano, víme, že head obsahuje náš argument. Pakliže ne, funkce volá Get na zbytek seznamu.

Uvedení typu je nutnost, protože sama funkce neví, co bude vracet. V době překladu totiž nezná i. Je-li pozice konstanta, můžeme použít jinou variantu funkce Get, která typ argumentu zná, protože si jej zjistí v době překladu.

Do List dopíšeme:

 template<int index>
struct GetType
{
typedef typename T::GetType<index>::Type Type;
};
template<>
struct GetType<ListSize-1>
{
typedef typename H Type;
};
template<int index>
struct GetValueAt
{
template<class Owner>
static typename GetType<index>::Type Get(const Owner &owner)
{
return T::GetValueAt<index>::Get(owner.tail);
}
};
template<>
struct GetValueAt<ListSize-1>
{
template<class Owner>
static typename GetType<ListSize-1>::Type Get(const Owner &owner)
{
return owner.head;
}
};
template<int index>
typename GetType<index>::Type Get() const
{
return GetValueAt<index>::Get(*this);
}

template struktury GetType slouží ke zjištění typu na zadané pozici. Opět se využívá rekurzivního procházení seznamu. Ze zadané pozice se stane čítač, který se s každou úrovné zvyšuje až dosáhne indexu posledního argumentu. Zadáme-li 0, vyloví uplně prvně vložený typ (leží na dně seznamu). Pokud v T máme třídu obsahující volitelný počet argumentů, pak typ na pozici 3 získáme:

typedef T::GetType<3>::Type TypNaPozici3;

template struktury GetValueAt slouží ke zjištění hodnoty argumentu. Princip je podobný jako u GetType. Použití - pokud v proměnné a (která je typu T) máme argumenty, pak argument na třetí pozici je:

T::GetType<3>::Type val=a.Get<3>();

ForEach

Funkce forEach zavolá funktor pro každý argument. Funktor přitom konstruujeme tak, aby zpracoval všechny možné typy, které funkce očekává. Můžeme zkonstruovat i pastičku na neznámé typy. Důležité je, že funkce funktoru je volána nejen s hodnotou, ale i s typem, pokud funkční operátor nadefinujeme jako template.

ForEach zajistíme doplněním do třídy List:

 template<class Functor>
bool ForEach(const Functor &functor) const
{
if (tail.ForEach(functor)) return true;
return functor(head);
}

Pokud funkor vrátí true, procházení končí, jinak procházení pokračuje. Všimněte si, že program si to nejprve "bez hlavě" namíří až na dno a argumenty vyzvedává a zpracovává až cestou zpět. To proto, že argumenty jsme vkládaly v opačném pořadí.

Aby si ForEach nerozbil o dno hlavu, musíme na dne definovat trampolínu. Dno - to je třída EmptyTail.

Doplníme do EmptyTail

   bool ForEach(const Functor &functor) const
{
return false; //trampolína
}

ForOne

Kombinuje výhody funktoru a indexace. Index který získáme v runtime indexuje argument s typem který je přístupný jen v době kompilace. To nevadí. Funkce ForOne zavolá funktor, ale jen pro specifický index. Typ argumetu překladač reflektuje v typu volaného funkčního operátoru funktoru.

Doplníme do List:

   template<class Functor>
bool ForOne(int pos,const Functor &functor) const
{
if (pos+1==ListSize) return functor(head);
else return tail.ForOne(pos,functor);
}

a do EmptyTail:

   template<class Functor>
bool ForOne(int pos,const Functor &functor) const
{
return false;
}

Celá třída

A to je celé přátelé, následuje celá třída v parádě:

 class VarArgs
{
public:

class EmptyTail
{
public:
static const int ListSize=0;
template<class Functor>
bool ForEach(const Functor &functor) const
{
return false;
}
template<class Functor>
bool ForOne(int pos,const Functor &functor) const
{
return false;
}
static int Size() {return ListSize;}
};
template<class H,class T>
class List
{
const H &head;
const T &tail;
typedef List<H,T> ThisType;
template<class RetType, int cnt> friend class GetValueAt;
public:
static const int ListSize=T::ListSize+1;
List(const H &head, const T &tail):head(head),tail(tail) {}

template<class Type>
List<Type,ThisType> operator,(const Type &other) const
{
return List<Type,ThisType>(other,*this);
}

template<class Functor>
bool ForEach(const Functor &functor) const
{
if (tail.ForEach(functor)) return true;
return functor(head);
}
template<class Functor>
bool ForOne(int pos,const Functor &functor) const
{
if (pos+1==ListSize) return functor(head);
else return tail.ForOne(pos,functor);
}
static int Size() {return ListSize;}
template<class RetType>
RetType Get(int pos) const
{
if (pos+1!=ListSize) return tail.Get<RetType>(pos);
else return head;
}
template<int index>
struct GetType
{
typedef typename T::GetType<index>::Type Type;
};
template<>
struct GetType<ListSize-1>
{
typedef typename H Type;
};
template<int index>
struct GetValueAt
{
template<class Owner>
static typename GetType<index>::Type Get(const Owner &owner)
{
return T::GetValueAt<index>::Get(owner.tail);
}
};
template<>
struct GetValueAt<ListSize-1>
{
template<class Owner>
static typename GetType<ListSize-1>::Type Get(const Owner &owner)
{
return owner.head;
}
};
template<int index>
typename GetType<index>::Type Get() const
{
return GetValueAt<index>::Get(*this);
}
};
template<class T>
List<T,EmptyTail> operator,(const T &other)
{
return List<T,EmptyTail>(other,EmptyTail());
}
};

Použití template

class ProcessArguments
{
mutable int _index;
public:
ProcessArguments():_index(0) {}
template<class T>
bool operator()(const T &arg) const
{
//... udělej něco s arg
//... v _index máš jeho pozici
_index++;
}
};
template<class H,class T>
void Fn(VarArg::List<H,T> &args)
{
ProcessArguments functor;
args.ForEach(functor);
}

Takto nadefinovanou funkci můžeme použít s libovolnými argumenty:

const char *hello="Hello World";
Fn((VarArg(),10,true,12.3,CRect(10,20,30,40),hello,(const char *)"Jdeme na pivo"));

Povšimněte si (const char *). Je to z toho důvodu, že řetězec "str" překladač hlásí jako pole, a v definici třídy jsou syntaxtické zápisy vedoucí na situace, kdy se vrací pole. A to je v C++ nepřípustné. Naštěstí tato situace nebude častá.

Typy do runtime

Funkce přijímající proměnný počet parametrů byla až doposud template. To je super, protože překladač nám přenos parametrů a jejich implementaci zoptimalizuje jedna báseň.

Ale jsou situace, kdy se nám template nehodí. Třeba proto, že chceme implementaci funkce skrýt, nebo třeba proto, že funkce bude knihovní funkcí nějaké DLL.

Jednak je třeba si uvědomit, že runtimová funkce bude přijímat konečnou množinu typů. Je možné použít RTTI a tím defacto pracovat se všemy typy. My si ukážeme jednodušší verzi.

Funkce přijímající libovolné parametry bude stále template, ale tentokrát nám poslouží jako předvoj, který přelouská parametry a předá je runtime funkci v nějaké použitelné formě. Co to znamená?

Musíme si sestavit seznam všech typů, které je možné předat. Dejme tomu, že umíme int, float, double a CRect. Vytvořme enum

enum KnowTypes {typeInt,typeFloat,typeDouble,typyeCRect};

napíšeme si funktor přijímající parametry


class GetParameters
{
mutable KnowTypes *_typy;
mutable const void **_argumenty;
public:
GetParameters(KnowTypes *typy,const void **argumenty):_typy(typy),_argumenty(argumenty) {}

void Add(KnowTypes typ, const void *arg)
{
*_typy++=typ;
*_argymenty++=arg;
}

bool operator()(const int &arg) const
{
Add(typeInt,&arg);
}

bool operator()(const float &arg) const
{
Add(typeFloat,&arg);
}
bool operator()(const double &arg) const
{
Add(typeDouble,&arg);
}
bool operator()(const CRect &arg) const
{
Add(typeCRect,&arg);
}
};

Funktor předpokládá existenci dvou polí. Do jednoho pole vkládá prvky odpovídající daným typům. Do druhého pole vkládá ukazatele na parametry. Runtime funkce bude tedy mít přehled o vložených argumentech a jejich typech. Navíc funktor zajistí, že bude možné použít pouze předdefinované typy, pokud se pokusíme vložit hodnotu jiného typu, nebude možné program přeložit (překladač nenajde funktor pro zadaný typ).

Funkce s vícero parametrů pak může výpadat takto:

template<class H,class T>
void Fn(VarArg::List<H,T> &args)
{
KnowTypes typeArr[args.ListSize]; //to lze ListSize je konstanta!
const void **argArr[args.ListSize]; //to lze ListSize je konstanta!
args.ForEach(GetParameters(typeArr,argArr));
RuntimeFn(typeArr,argArr,args.ListSize); //volání v runtime
}

Prototyp runtime funkce bude následující:

void RuntimeFn(KnownTypes *types, const void *arguments, size_t count);

Runtime verze funkce přijímá přelouskané argumenty přes jedno společné rozhraní. Dostává pole typů a ukazatelů. Už teď vidíme zpoustu výhod. Jednak to, že funkce se sama dozví o tom, jakého typu jsou argumenty, tedy netřeba typ extra specifikovat. Zachovává to však i výhodu runtime volání, tj finální funkce nemusí být template. Další výhodou je typová kontrola, která nám zabrání poslat runtime funkci neplatný ukazatel nebo typ. A abych neopoměl typovou konverzi, jestliže některý typ je převoditelný na jíný typ a patřičný funktor existuje pouze na převedný typ. Pak se zajistí automatická konverze bez nutnosti do ní zasahovat.

Jak využít funkce s volitelným počtem argumentů si ukážeme příště, napíšeme si vlastní verzi funkce printf, která jistě bude užitečná všem programátorům. kteří se kdy trápili s lokalizací aplikací a s lokalizovaným formátování.

vytvořeno: 12.9.2006 21:11:21, změněno: 20.9.2006 09:19:34
Jsou informace v článku pro Vás užitečné?
  • (1)
  • (0)
  • (0)
  • (1)
  • (3)
Nick nebo OpenID
Vzkaz
 
25.7.2016 19:41:05

HFStGrQ1Q

It was dark when I woke. This is a ray of sueinsnh.

Podobné články

Funkce s volitelným počtem argumentů v C++ - Verze pro GCC

Malá odbočka v seriálu Funkce s volitelným počtem argumentů v C++ II. Třídy ArrayArgs a VarArgs jsem nechal přepsat tak aby je pochopilo i GCCčko.

Funkce s volitelným počtem argumentů v C++

Leckterý programátor v C jistě zná syntaxtický zápis funkce s volitelným počtem argumentů. V následujícím článku se podíváme na nevýhody tohoto nástroje a způsoby jak to implementovat jinak

Tuples v C++

Pokud si pamatujete na seriál Funkce s volitelným počtem argumentů v C++, tak v této části najdete jakési pokračování, i když cílem článku je trošku něco jiného

Vracíme z funkce objekty - praktická ukázka

V článku Vracíme z funkce objekty jsem ukazoval, jak probíhá návrat objektu z funkce a jak toto optimalizovat tak, aby se minimalizovalo používání kopírování. Teď si to ukážeme prakticky - použil jsem gcc bez optimalizací

Garbage collector v C++ - Popis API tříd kolekce LightSpeed::GC

V tomto článku si představíme rozhrani Garbage Collectoru v knihovně LightSpeed. Princip GC byl přidstaven v předchozím díle
Reklama: