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++ - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

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


Běžný přístup

Pokud z nějakých důvodů potřebuji deklarovat funkci s proměnným počtem argumentů, použiji tento syntaxtický zápus

void FunkceSPromennymPoctemArgumentu(int pocet,...);

K přístupu ostatních parametrů se používá se hlavičkový soubor <stdarg.h>. Nebudu se rozepisovat jak to funguje, článek je určen programátorům, kteří už toto znají. Rozeberme si nevýhody tohoto řešení.

Nutnost znát počet parametrů.
Jedním z parametrům můsí být hodnota udávající počet parametrů. Nebo musí být nějak označen konec seznamu.
Nelze zkontrolovat zda je počet správný
Pokud při volání uvedeme jiný počet, pak většinou dochází k pádům nebo nedefinovanému chování.
Nelze oveřit správnost typů parametru
Tohle je velká nevýhoda. Nejen že nemohu zkontrolovat zda byl vložen typ jaký očekávám, ale už vůbec není možné zjistit typ zadaného argumentu.
Problematické uložení dat v zásobníku
Uložení parametru v zásobníku může být někdy problematické. Generické typy se často konvertují do int nebo do double. Jak se překonvertují ostatní typy nemáme moc pod kontrolou. To může být problém u ukazatelů na polymorfní typy, které se ještě před uložením musí přepočítat.

Řešení pomocí templatů - proměnný počet parametrů stejného typu

V prvním díle se podíváme na jednoduché řešení, jak předat proměnný počet parametrů pokud všechny jsou stejného typu. Máme výchozí syntaxi:

void VypisCisla(int pocet, ...);

VypisCisla(10,1,2,3,4,5,6,7,8,9,10);

Aby jsme mohli předat volitelný počet argumentů a nepoužít "...", musíme parametry předat v jednom objektu.

void VypisCisla(const ObjektObsahujeParametry &params);

Tady malou odbočku. Je možné také použít pole:

void VypisCisla(const int *cisla, int pocet);

Programátor v C++ však použije objekt.

template<class T>
class Array
{
T *_arr;
int _cnt;
public:
Array():_arr(0),_cnt(0) {]
Array(T *arr, int cnt):_arr(arr),_cnt(cnt) {}

T &operator[](int index) {return _arr[index];}
const T &operator[](int index) const {return _arr[index];}
int Size() const {return _cnt;}
};

Třída Array slouží k předání ukazatele a velikosti jako jednoho objektu. Pozor, jedná se jen o referenci, tj kopírování pole nezpůsobí vytvoření kopie, ale pouze se přenese ukazatel a velikost.

S tímto nástrojem jsme skoro hotovi.

void VypisCisla(const Array<int> &parametry);

int cisla[]={1,2,3,4,5,6,7,8,9,10}
VypisCisla(Array<int>(cisla,sizeof(cisla)/sizeof(int));

Mohl bych být spokojen, ale tohle je málo. Ve většině situací použití pole vystačí, ale někdy můžeme požadovat aby například volitelný počet parametrů stejného typu dostal konstruktor nějakého objektu. Přidejme si k tomu komplikaci, kdy se tento objekt nevytváří dynamicky, ale automaticky v rámci jiného objektu (a ještě v konstruktoru toho jiného objektu). Zde nemáme prostor vytvářet pole. Jako parametr konstruktoru je potřeba uvést pouze výraz, nějaká deklarace pole zde nemá co dělat.

Bylo by dobré, abysme se dostali k původně zadané formě.
VypisCisla(10,1,2,3,4,5,6,7,8,9,10);

A co víc, argumenty nemusí být jen konstanty, mohou to být i proměnné, obecně výrazy.

Pole uvnitř objektu

Potíž s deklarací pole můžeme vyřešit jeho deklarací uvnitř objektu

template<class T, int count>
class PoleDeklarovaneUvnitr: public Array<T>
{
T buffer[count];
public:
PoleDeklarovaneUvnit():Array(buffer,count) {}
};

Pole alokované máme, ale nepředali jsme mu hodnoty. Tím si tedy moc nepomůžeme, ale můžeme z toho vyjít. Je jasná, že nějaké pole uvnitř objektu určitě bude.

Operátor čárka

Někteří se možná podiví, ale kupodivu lze přetížit i operátor čárky. Operátor se chová podobně jako ostatní operátory, navíc má pro nás výhodu, že má nízkou prioritu. Za čárku lze tedy umístit jakýkoliv výraz (neobsahující čárku), a máme jistotu, že se operátor čárky vyhodnotí až naposledy.

template<class T, int count>
class ArrayArgType
{
mutable T buffer[count]; //musí být mutable, protože se obsah mění, ale instance bude deklarována jako const

ArrayArgType operator,(const T &val) const
{
//co asi bude tady?
}
};

Zápis funkce VypisCisla tedy bude vypadat následovně:

VypisCisla((ArrayArgType<int, 10>(),1,2,3,4,5,6,7,8,9,10));

Mno nepřipomíná vám to původní zadání?. Navíc zde předepisujeme i počet parametrů (aby objekt věděl jak velké pole vyhradit) a jakého jsou typu. Uvedení typu má výhodu v případě, že předáváme ukazatele na potomky a chceme je všechny přetypovat na společného předka.

Udivují vás dvojité závorky? To je proto, že čárka odděluje parametry funkce. Funkce má ale jen jediný parametr, a tak překladač bude protestovat jakmile narazí na čárku (čárka bude mít význam oddělení parametrů). Uvedením další závorky říkáme, že se jedná o jeden parametr, který obsahuje výraz složený s operátorů čárka. Prvním operandem operátoru je instance třídy ArrayArgType, která zahajuje řetězení operátorů čárka. Každá další čárka vrací objekt, který opět definuje operátor čárky, a tak dále až do konce.

Teď nám zbývá napsat obsah operátoru čárka aby to fungovalo.

Implementace ArrayArgType

Bylo by to asi příliš jednoduché mezi členské proměnné přidat čítač, a s každou čárkou jej inkrementovat. Čítač by pak indexoval pole a určoval by, kam se vloží další parametr.

Myslím si ale, že to lze napsat bez čítače a ušetřit si proměnnou. Proměnná totiž zavazuje překladač přeložit třídu právě jedným způsobem. Bylo by však efektní, kdyby se čítání provádělo už během překladu, protože každý argument přece zná svou pozici v seznamu a ví tedy již během překladu svou pozici v poli. Vypočet pozice během překladu uvolní ruce překladači a optimalizaci, a budeme se divit, jak moc nám to zjednodušší výsledný kód.

Jak takové čítání zajistit?

Základní myšlenka je obsažena v návratové hodnotě operátoru čárka. Navrhuji následující deklaraci:

template<class T, int count>
class ArrayArgType //zmizelo dědění z Array, má to svůj důvod.
{
mutable T buffer[count];
friend class ArrayArgIter; //je potřeba umožnit přístup do bufferu pomocné třídě.
public:

template<int remain>
class ArrayArgIter
{
//co bude tady?
};

//funkce vrací instanci pomocné třídy
ArrayArgIter<count-1> operator,(const T &val) const
{
buffer[0]=val; //vlož první číslo do bufferu
return ArrayArgIter<count-1>(*this); //vytvoř třídu a její instanci mající remain=count-1
};
};

Odebráním Array z hlavičky třídy má jednoduchý důvod. Protože operátor čárky nevrací tuto třídu, je už nyní jasné, že na konci čárek bude vytvoře objekt některé vnitřní třidy. Hlavni třída tedy dědění z Array nepotřebuje. Zároveň nám to brání použít neinicalizované pole (jako parametr uvést instanci ArrayArg bez hodnot).

Operátor čárky konstruuje vnitřní třídu ArrayArgIter. Jedná se o template, jehož parametrem je počet zbývajících parametrů do naplnění pole. Tento template bude mít zhruba tuto deklaraci

 template<int remain>
class ArrayArgIter
{
const ArrayArgType &owner;
public:
ArrayArgIter(const ArrayArgType &owner):owner(owner) {}
ArrayArgIter<remain-1> operator,(const T &val) const
{
owner.buffer[count-remain]=val;
return ArrayArgIter<remain-1>(owner);
}
};

A vida. Při použití čárky vložím hodnotu za čárkou na předem spočítané políčko v poli buffer (spočítané za překladu, protože count a remain jsou konstanty. Zároveň vytvořím další variantu třídy ArrayArgItem a jeho instanci, ovšem s hodnotou remain o jedničku sníženou. To se opakuje pro každou čárku až dojdu k poslednímu argumentu. Bylo by dobřé čítání zastavit (všimneme si, že ani ArrayArgIter<int> nedědí Array, takže ani jakýkoliv mezivýsledek nelze použít k předání pole. I to je správně, je totož potřeba, aby pole bylo předáno až po úplném naplnění).

Čítání ukončíme specializací template

 template<>
class ArrayArgIter<0>:public Array<T>
{
public:
ArrayArgIter(const ArrayArgType &owner):Array(owner.buffer,count) {}
};

Nechybí tam něco? Nechybí. S poslední čárkou vznikne varianta ArrayArgIter, která dědí Array (v konstruktoru jej inicializuje) a která nedefinuje další čárku. Instanci tohoto typu můžeme přímo předat jako parametr funkce očekávající typ Array.

Vyčištení a konečné úpravy

Pokusíme-li se program používající ArrayArg přeložit, najdeme spoustu chyb jenž vzniknou nedovoleným přístupem do soukromých oblastí některých objektů. Proto je třeba doplnit některé deklarace friend (viz na konci).

Nyní můžeme zkusit chování překladače při nedodržení počtu argumentů nebo typu. Pokud nedodržíme typ, pak nás překladač upozorní na to, že nemuže převést jeden typ na druhý a ukáže přímo na ten argument, který je špatně. To je jasná, operátor čárka totiž obsahuje typ v deklaraci argumentu.

Uvedeme-li méně argumentů než jsme deklarovali v template, překladač oznámí, že nelze parametr převést na typ Array. Což je správně, to jsme předpokládali. Navíc si všimněte, že v proměnné remain bude vypsáno, kolik argumentů chybí (pokud překladač při chybě vypisuje parametry templatů).

Co se stane, uvedeme-li více argumentů? To už není tak hezké, protože překladač nejspíš oznámí že "<Typ argumentů>" nelze převést na Array. Nic o počtu, navíc zmatená hláška. Je to tím, že čárka za posledním argumentem už funguje tak jak jsme zvyklí. Výsledkem operátoru čárky je pravý argument a v tomto případě je to poslední zadaný argument (což není Array - ale představme si, kdyby náhodou bylo). Chtělo by to nějak ošetřit.

Doplňme tedy třídu ArrayArgType o další vnitřní třídu:

 template<int extra>
class Too_Many_Parameters
{
public:
Too_Many_Parameters() {}
Too_Many_Parameters<extra+1> operator,(const T &val) const
{
return Too_Many_Parameters<extra+1>();
}
};

Třída se jmenuje Too_Many_Parameters, protože ji bude překladač vypisovat do chybové hlášky, tak aby nám hláška pomohla rozluštit, kde je problém. Třída funguje jako past na další argumenty. Deklaruje operátor čárky a pouze si s každým argumentem čítá, kolik jich bylo navíc. Takto můžeme uvést libovolný počet argumentů. Navíc třída nedědí Array, takže není možné výsledný objekt přetypovat na Array a předat ho funkci.

Ještě nám zbývá doplnit čárku do specializace ArrayArgIter<0>

Too_Many_Parameters<1> operator,(const T &val) const
{
return Too_Many_Parameters<1>();
}

Listing

template<class T, int count>
class ArrayArgType
{
protected:
mutable T buffer[count];
friend class ArrayArgIter;

public:
ArrayArgType() {}

template<int remain>
class ArrayArgIter
{
friend class ArrayArgType;
friend class ArrayArgIter<remain+1>;
template <int count> friend class ArrayArg;
const ArrayArgType &owner;

public:
ArrayArgIter(const ArrayArgType &owner):owner(owner) {}


ArrayArgIter<remain-1> operator,(const T &val) const
{
owner.buffer[count-remain]=val;
return ArrayArgIter<remain-1>(owner);
}
};

template<int extra>
class Too_Many_Parameters
{
public:
Too_Many_Parameters() {}
Too_Many_Parameters<extra+1> operator,(const T &val) const
{
return Too_Many_Parameters<extra+1>();
}
};

template<>
class ArrayArgIter<0>:public Array<T>
{
friend class ArrayArgType;
friend class ArrayArgIter<1>;
ArrayArgIter(const ArrayArgIter &other):owner(other.owner) {}
ArrayArgIter& operator=(const ArrayArgIter &other);
template <int count> friend class ArrayArg;
public:
ArrayArgIter(const ArrayArgType &owner):Array(owner.buffer,count) {}
Too_Many_Parameters<1> operator,(const T &val) const
{
return Too_Many_Parameters<1>();
}
};

ArrayArgIter<count-1> operator,(const T &val) const
{
buffer[0]=val;
return ArrayArgIter<count-1>(*this);
}
};

Hmm, ptáte se proč jsem nazval třídu ArrayArgType? To proto, že mám v záloze třídu ArrayArg. Ta vypadá takto:

template<int count>
class ArrayArg
{
template<class T, int count>
class Helper: public ArrayArgType<T,count>
{
public:
Helper(const T &value) {buffer[0]=value;}
ArrayArgIter<count-2> operator,(const T &val) const
{
buffer[1]=val;
return ArrayArgIter<count-2>(*this);
}
};

template<class T>
class Helper<T,1>: public ArrayArgType<T,count>, public Array<T>
{
public:
Helper(const T &value):Array<T>(buffer,1) {buffer[0]=value;}
};

public:
template<class T>
Helper<T,count> operator,(const T &val)
{
return Helper<T,count>(val);
}
};

Totiž doteď jsme při použití proměnného počtu argumentů museli deklarovat typ. Avšak jsem člověk od přírody líný a tak se mi nechce typ vypisovat. Tam kde to jde tedy nechť si typ argumentů zjistí překladač. Využijeme toho, že jde o argumenty stejného typu, stačí nám tedy typ prvního argumentu. ArrayArg toto dělá s tím, že operátor čárky vytváří potomka třídy ArrayArgType. Konstruktor ihned zapíše první hodnotu, další čárka už zapisuje na druhou pozici. Je nutné speciálně ošetřit případ, kdy je zadán jediný parametr, v tom případě musí být i samotný Helper děděný z Array.
(je třeba ještě upravit ArrayArgType aby buffer spřístupnil i potomkům.

Zjistíme, že má smysl mít obě třídy:
Mohu kdykoliv snadno zavolat

VypisCisla((ArrayArg<10>(),1,2,3,4,5,6,7,8,9,10));

ale nemohu udělat

ProjdiObjekty((ArrayArg<3>(),potomek1,potomek2,potomek3));

leda že bych udělal

ProjdiObjekty((ArrayArg<3>(),static_cast<Predek *>(potomek1),potomek2,potomek3)); 

čímž si vynutím typ Předek pro všechny argumenty (a automatickou konverzi. Já ale mohu napsat

ProjdiObjekty((ArrayArgType<Predek *,3>(),potomek1,potomek2,potomek3)); 

což je kratší a výstížnější.

Závěr

Mít možnost deklarovat funkce s proměnným počtem argumentů za použití výše uvedené techniky je výhodnější než tři tečky "...".

  • Jednak sama deklarace funkce s proměnným počtem argumentů je vlastně deklarace funkce, která přijímá pole. Není problém tedy místo vypisování argumentů prostě dosadit pole (máme-li hodnoty v poli).
  • Je zde silná typová kontrola, překladač nám nedovoli použít jako parametr argument jiného typu.
  • Výsledný kód je velmi krátký, najdeme zde jen instrukce plnící pomocné pole.
  • Není ani problém s uložením dat, pokud budeme předávat argumenty typu char, pak nebudou konvertovány na int. Stejně tak float nebude konvertováno na double
  • Ukazatelé na potomky se automaticky převedou na ukazatele na předky.
  • Automatická konveze bude pracovat normálně, čili převod z int na float je samozřejmé. Pokud máme argumenty typu const char *, ale použijeme std::string, i zde dojde k převodu (u tří teček se do zásobníku vloží celá instance třídy string).

V příští článku si povíme o tom, jak naimplementovat volitelný počet argumentů různých typů a jako příklad si ukážeme, jak toho využít k implementaci moderní obdoby funkce printf.

opravenka

Máte pocit, že se tady něco změnilo? Třída ArrayArg byla chybně napsaná. Chyba způsobovala, že se nealokoval buffer, ke kterému pak všichni přistupovali (resp. alokoval, ale jen v zásobníku funkce čárky). Překladač toto neodhalil. Třída je nyní složitější, a aby si vynutila alokaci v kontextu volajícího, používá pomocnou třídu Helper, kterou vrací a tato třída drží hodnoty argumentů v bufferu

vytvořeno: 10.9.2006 15:36:06, změněno: 21.8.2007 09:10:32
Jsou informace v článku pro Vás užitečné?
  • (5)
  • (0)
  • (2)
  • (2)
  • (0)
Nick nebo OpenID
Vzkaz
 
25.7.2016 21:14:35

fGEpqPi40F

Hey there friend, just roaming through the world wide web search data and dioesvercd your site. I’m engraved with the blogposts that you own within this web page. This displays how good you grasp this specific issue. Bookmarked this web site, will certainly come back to get more detail. You, my buddy, Rock!!!
25.7.2016 18:17:06

dUoH4sMBj

Sa akin masyadong. Kawili-wili, how friendships can grow between people who have never met face-to-face, but are sisters in the Spirit. Hugs sa iyoKkama.ailan-post ang Jan..
2.7.2014 23:18:29

Qq6ydNRsm1tk

public boaoeln modThree(int[] nums) { boaoeln result = false; for (int i = 0; i < nums.length 2; i++) { int mod = nums[i] % 2; if (nums[i+1] % 2 == mod && nums[i+2] % 2 == mod) { result = true; } } return result;}

Podobné články

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ů?

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.

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: