Takže již víme, jak deklarovat, definovat a používat funkce v programech. V této kapitole si povíme o jejich speciálním typu – přetížených funkcích. Říká se, že dvě funkce jsou přetížené, pokud mají stejný název, jsou deklarovány ve stejném rozsahu, ale mají různé seznamy formálních parametrů. Probereme, jak jsou takové funkce deklarovány a proč jsou užitečné. Poté zvážíme otázku jejich řešení, tzn. o tom, která z několika přetížených funkcí je volána během provádění programu. Tento problém je jedním z nejobtížnějších v C++. Ti, kteří se chtějí dostat do podrobností, si možná budou chtít přečíst dvě části na konci kapitoly, které se podrobněji zabývají převodem typů argumentů a řešením přetížených funkcí.

9.1. Přetížená deklarace funkcí

Nyní, když jsme se naučili, jak deklarovat, definovat a používat funkce v programech, pojďme se s nimi seznámit přetížení– další aspekt v C++. Přetížení vám umožňuje mít několik funkcí stejného jména, které provádějí podobné operace s argumenty různých typů.
Již jste použili předdefinovanou přetíženou funkci. Například k vyhodnocení výrazu

je volána operace sčítání celého čísla, zatímco se vyhodnocuje výraz

1.0 + 3.0

provádí sčítání s plovoucí desetinnou čárkou. Volba jedné nebo druhé operace je uživatelem bez povšimnutí. Operátor sčítání je přetížen, aby se přizpůsobil různým typům operandů. Je odpovědností kompilátoru, nikoli programátora, rozpoznat kontext a aplikovat operaci vhodnou pro typy operandů.
V této kapitole si ukážeme, jak definovat vlastní přetížené funkce.

9.1.1. Proč potřebujete přetížit název funkce?

Stejně jako u vestavěné operace sčítání můžeme potřebovat sadu funkcí, které provádějí stejnou akci, ale na parametrech různých typů. Předpokládejme, že chceme definovat funkce, které vracejí největší z předávaných hodnot parametrů. Pokud by nedocházelo k přetěžování, každá taková funkce by musela mít jedinečný název. Například rodina funkcí max() může vypadat takto:

Int i_max(int, int); int vi_max(konst vektor &); int matice_max (konst. matice &);

Všechny však dělají totéž: vracejí největší z hodnot parametrů. Z uživatelského hlediska je zde pouze jedna operace – výpočet maxima a detaily její implementace nejsou příliš zajímavé.
Uvedená lexikální složitost odráží omezení softwarového prostředí: každé jméno vyskytující se ve stejném rozsahu musí odkazovat na jedinečnou entitu (objekt, funkci, třídu atd.). Toto omezení vytváří v praxi určité nepohodlí, protože programátor si musí pamatovat nebo nějak najít všechna jména. Přetížení funkcí pomáhá tento problém řešit.
Při použití přetížení může programátor napsat něco takového:

Int ix = max(j, k); vektor vec; //... int iy = max(vec);

Tento přístup se ukazuje jako velmi užitečný v mnoha situacích.

9.1.2. Jak přetížit název funkce

V C++ mohou mít dvě nebo více funkcí stejný název, pokud se jejich seznamy parametrů liší buď počtem parametrů, nebo jejich typy. V v tomto příkladu deklarujeme přetíženou funkci max():

Int max(int, int); int max (konst. vektor &); int max(const matrix &);

Každá přetížená deklarace vyžaduje samostatnou definici funkce max() s odpovídajícím seznamem parametrů.
Pokud je v určitém rozsahu název funkce deklarován více než jednou, pak druhou (a následující) deklaraci interpretuje kompilátor následovně:

  • pokud se seznamy parametrů dvou funkcí liší počtem resp typy parametrů, pak jsou funkce považovány za přetížené: // přetížené funkce void print(const string &); neplatný tisk (vektor &);
  • pokud jsou návratový typ a seznamy parametrů v deklaracích dvou funkcí stejné, pak se druhá deklarace považuje za opakovanou: // deklarace stejné funkce void print(const string &str); void print(const string &); Názvy parametrů se při porovnávání deklarací neberou v úvahu;
    pokud jsou seznamy parametrů dvou funkcí stejné, ale typy návratových hodnot se liší, pak je druhá deklarace považována za nesprávnou (nekonzistentní s první) a je kompilátorem označena jako chyba: unsigned int max(int ​​​​i1 , int i2); int max(int ​​​​i1, int i2);
    // chyba: liší se pouze typy
    // návratové hodnoty

Přetížené funkce se nemohou lišit pouze v typech návratů; Pokud se seznamy parametrů dvou funkcí liší pouze ve výchozích hodnotách argumentů, pak se druhá deklarace považuje za opakovanou:

// deklarace stejné funkce int max (int *ia, int sz); int max(int*ia, int = 10);

Klíčové slovo typedef vytvoří alternativní název pro stávající typ dat, nový typ se nevytvoří. Pokud se tedy seznamy parametrů dvou funkcí liší pouze tím, že jedna používá typedef a druhá typ, pro který je typedef alias, jsou seznamy parametrů považovány za stejné, jako v následujících dvou deklaracích funkce calc( ) funkce. V tomto případě druhá deklarace způsobí chybu kompilace, protože vrácená hodnota se liší od hodnoty uvedené dříve:

// typedef nezavádí nový typ typedef double DOLLAR; // chyba: identické seznamy parametrů, ale různé typy // návratových hodnot externí DOLLAR calc(DOLLAR); extern int calc(double);

Konstantní nebo těkavé specifikátory se v tomto srovnání neberou v úvahu. Následující dvě prohlášení jsou tedy považována za stejná:

// deklarujeme stejnou funkci void f(int); void f(const int);

Specifikátor const je důležitý pouze v rámci definice funkce: označuje, že je zakázáno měnit hodnotu parametru v těle funkce. Argument předaný hodnotou však lze použít v těle funkce jako normální iniciovanou proměnnou: změny nejsou viditelné mimo funkci. (Metody předávání argumentů, zejména předávání hodnotou, jsou popsány v části 7.3.) Přidání specifikátoru const k parametru předávanému hodnotou neovlivní jeho interpretaci. Funkci deklarovanou jako f(int) lze předat jakoukoli hodnotu typu int, stejně jako funkci f(const int). Protože obě přebírají stejnou sadu hodnot argumentů, výše uvedené deklarace nejsou považovány za přetížené. f() lze definovat jako

Void f(int i) ( )

Void f(const int i) ( )

Mít tyto dvě definice v jednom programu je chyba, protože stejná funkce je definována dvakrát.
Pokud se však na parametr typu ukazatel nebo odkaz použije konst nebo volatile specifikátor, vezme se to v úvahu při porovnávání deklarací.

// různé funkce jsou deklarovány void f(int*); void f(const int*); // a zde jsou deklarovány různé funkce
void f(int&);
void f(const int&);

9.1.3. Kdy byste neměli přetěžovat název funkce

Kdy není přetěžování jmen prospěšné? Například při zadávání různých jmen funkcím usnadňuje čtení programu. Zde jsou nějaké příklady. Následující funkce fungují se stejným typem abstraktního data. Na první pohled jsou vhodnými kandidáty na přetížení:

Void setDate(Date&, int, int, int); Datum &convertDate(const string &); void printDate(const Date&);

Tyto funkce pracují se stejným datovým typem, třídou Date, ale provádějí sémanticky odlišné akce. V tomto případě lexikální složitost spojená s používáním různých názvů pramení z programátorské konvence poskytovat sadu operací s datovým typem a pojmenovávat funkce v souladu se sémantikou těchto operací. Mechanismus třídy C++ však činí takovou konvenci zbytečnou. Bylo by lepší učinit takové funkce členy třídy Date, ale zároveň ponechat různá jména, která odrážejí význam operace:

#zahrnout class Date ( public: set(int, int, int); Date& convert(const string &); void print(); // ...
};

Uveďme další příklad. Následujících pět funkcí členů obrazovky provádí různé operace s kurzorem na obrazovce, který je členem stejné třídy. Může se zdát rozumné přetížit tyto funkce pod obecným názvem move():

Screen&moveHome(); Screen& moveAbs(int, int); Screen& moveRel(int, int, char *direction); Screen&moveX(int); Screen& moveY(int);

Poslední dvě funkce však nelze přetížit, protože mají stejné seznamy parametrů. Aby byl podpis jedinečný, kombinujeme je do jedné funkce:

// funkce kombinující moveX() a moveY() Screen& move(int, char xy);

Nyní mají všechny funkce různé seznamy parametrů, takže mohou být přetíženy pod názvem move(). To by se však nemělo dělat: různá jména nesou informace, bez kterých bude programu obtížnější porozumět. Operace pohybu kurzoru prováděné těmito funkcemi se tedy liší. Například moveHome() provádí speciální druh pohybu doleva horní roh obrazovka. Který ze dvou níže uvedených hovorů je pro uživatele srozumitelnější a lépe zapamatovatelný?

// které volání je jasnější? myScreen.home(); // myslíme si tohle! myScreen.move();

V některých případech není potřeba přetěžovat název funkce nebo přiřazovat různé názvy: použití výchozích hodnot argumentů vám umožňuje kombinovat několik funkcí do jedné. Například funkce ovládání kurzoru

MoveAbs(int, int); moveAbs(int, int, char*);

se liší přítomností třetího parametru typu char*. Pokud jsou jejich implementace podobné a lze najít rozumnou výchozí hodnotu pro třetí argument, lze obě funkce nahradit jednou. V tomto případě bude jako výchozí hodnota sloužit ukazatel s hodnotou 0:

Move(int, int, char* = 0);

Určité funkce by měly být použity, když to aplikační logika vyžaduje. Není nutné zahrnout přetížené funkce do programu jen proto, že existují.

9.1.4. Přetížení a rozsah A

Všechny přetížené funkce jsou deklarovány ve stejném oboru. Například lokálně deklarovaná funkce nepřetěžuje, ale jednoduše skryje tu globální:

#zahrnout void print(const string &); void print(double); // přetíží print() void fooBar(int ival)
{
// samostatný rozsah: skryje obě implementace print().
extern void print(int); // chyba: print(const string &) není v této oblasti vidět
print("Hodnota: ");
tisknout(ival); // správně: print(int) je vidět
}

Protože každá třída definuje svůj vlastní rozsah, funkce, které jsou členy dvou různé třídy, nepřetěžujte se navzájem. (Funkce členů třídy jsou popsány v kapitole 13. Řešení přetížení pro funkce členů třídy je popsáno v kapitole 15.)
Je také možné deklarovat takové funkce v rámci jmenného prostoru. Ke každému z nich je také přiřazen samostatný rozsah, takže funkce deklarované v různých prostorech se vzájemně nepřetěžují. Například:

#zahrnout jmenný prostor IBM ( extern void print(const string &); extern void print(double); // přetěžuje print() ) jmenný prostor Disney ( // samostatný rozsah: // nepřetěžuje funkci print() ze jmenného prostoru IBM extern void tisknout (int); )

Použití deklarací a direktiv pomáhá zpřístupnit členy jmenného prostoru v jiných oborech. Tyto mechanismy mají určitý dopad na přetížené deklarace funkcí. (Použití deklarací a použití direktiv bylo diskutováno v části 8.6.)

Jak deklarace using ovlivňuje přetížení funkcí? Připomeňme, že zavádí alias pro člena jmenného prostoru v rozsahu, ve kterém se deklarace vyskytuje. Co taková prohlášení dělají v následujícím programu?

Jmenný prostor libs_R_us ( int max(int, int); int max(double, double); extern void print(int);
extern void print(double);
) // pomocí deklarací
pomocí libs_R_us::max;
pomocí libs_R_us::print(double); // chyba void func()
{
max(87, 65); // volá libs_R_us::max(int, int)
max(35,5, 76,6); // volá libs_R_us::max(double, double)

První pomocí deklarace přináší obě funkce libs_R_us::max do globálního rozsahu. Nyní lze libovolnou z funkcí max() volat uvnitř func(). Typy argumentů určují, která funkce se má volat. Druhá pomocí deklarace je chyba: nemůže specifikovat seznam parametrů. Funkce libs_R_us::print() je deklarována pouze takto:

Použití libs_R_us::print;

Deklarace using vždy zpřístupní všechny přetížené funkce se zadaným názvem. Toto omezení zajišťuje, že rozhraní jmenného prostoru libs_R_us nebude narušeno. Je jasné, že v případě hovoru

Tisk(88);

Autor jmenného prostoru očekává volání libs_R_us::print(int). Pokud uživateli umožníte selektivně zahrnout do rozsahu pouze jednu z několika přetížených funkcí, stane se chování programu nepředvídatelným.
Co se stane, když deklarace pomocí vloží do rozsahu funkci s již existujícím názvem? Tyto funkce vypadají, jako by byly deklarovány přímo tam, kde se objeví deklarace using. Proto se vložené funkce účastní procesu řešení názvů všech přetížených funkcí přítomných v daném rozsahu:

#zahrnout jmenný prostor libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // libs_R_us::print(int) a libs_R_us::print(double)
// přetížení print(const string &)
pomocí libs_R_us::print; void fooBar (int ival)
{
// print(const string &)
}

Deklarace using přidá do globálního rozsahu dvě deklarace: jednu pro print(int) a jednu pro print(double). Jsou to aliasy v prostoru libs_R_us a jsou součástí sady přetížených funkcí nazývaných print, kde globální print(const string &) již existuje. Při řešení přetížení tisku ve fooBar se berou v úvahu všechny tři funkce.
Pokud deklarace using zavádí funkci do oboru, který již obsahuje funkci se stejným názvem a stejným seznamem parametrů, je to považováno za chybu. Nemůžete použít deklaraci using jako alias funkce print(int) v oboru názvů libs_R_us, pokud print(int) již v globálním rozsahu existuje. Například:

Jmenný prostor libs_R_us ( void print(int); void print(double); ) void print(int); pomocí libs_R_us::print; // chyba: opakovaná deklarace print(int) void fooBar(int ival)
{
tisknout(ival); // jaký tisk? ::print nebo libs_R_us::print
}

Ukázali jsme, jak spolu souvisí používání deklarací a přetížené funkce. Nyní se podíváme na funkce používání pomocí direktiv. Direktiva using způsobí, že členové jmenného prostoru se zdají být deklarováni mimo tento jmenný prostor a přidá je do nového oboru. Pokud již v tomto oboru existuje funkce se stejným názvem, dojde k přetížení. Například:

#zahrnout jmenný prostor libs_R_us ( extern void print(int); extern void print(double); ) extern void print(const string &); // pomocí direktivy
// print(int), print(double) a print(const string &) jsou prvky
// stejná sada přetížených funkcí
pomocí jmenného prostoru libs_R_us; void fooBar (int ival)
{
print("Hodnota: "); // volá globální funkci
// print(const string &)
tisknout(ival); // volá libs_R_us::print(int)
}

To platí také v případě, že existuje více direktiv použití. Funkce stejného jména, které jsou členy různých prostorů, jsou zahrnuty ve stejné sadě:

Jmenný prostor IBM ( int print(int); ) jmenný prostor Disney ( double print(double); ) // using-directive // ​​​​vytváří mnoho přetížených funkcí z různých // jmenných prostorů pomocí jmenného prostoru IBM; pomocí jmenného prostoru Disney; dlouhý dvojitý tisk (dlouhý dvojitý); int main() (
tisknout(1); // s názvem IBM::print(int)
tisk (3.1); // s názvem Disney::print(double)
návrat 0;
}

Sada přetížených funkcí pojmenovaných print v globálním rozsahu zahrnuje funkce print(int), print(double) a print(long double). Všechny jsou brány v úvahu v main() při řešení přetížení, ačkoli byly původně definovány v různých jmenných prostorech.
Takže zopakujme, že přetížené funkce jsou ve stejném rozsahu. Končí tam zejména v důsledku používání deklarací a použití direktiv, které zpřístupňují jména z jiných oborů.

9.1.5. externí direktiva "C" a přetížené A funkce

V sekci 7.7 jsme viděli, že externí direktivu vazby "C" lze použít v programu C++ k označení, že nějaký objekt je v části napsané v C. Jak tato direktiva ovlivňuje přetížené deklarace funkcí? Mohou být funkce napsané v C++ i C ve stejné sadě?
Závazná směrnice umožňuje zadat pouze jednu z mnoha přetížených funkcí. Například následující program je nesprávný:

// chyba: direktiva určená pro dvě přetížené funkce extern "C" extern "C" void print(const char*); extern "C" void print(int);

Následující příklad přetížené funkce calc() ilustruje typické použití direktivy extern "C":

Třída SmallInt (/* ... */); class BigNum (/* ... */); // funkci napsanou v C lze volat buď z programu,
// napsaný v C az programu napsaného v C++.
// Funkce C++ zpracovávají parametry, které jsou třídami
externí "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);

Funkci calc() napsanou v C lze volat z programu C nebo C++. Zbývající dvě funkce berou třídu jako parametr, a proto je lze použít pouze v programu C++. Pořadí, ve kterém se oznámení objeví, je nepodstatné.
Závazná směrnice nezáleží při rozhodování, kterou funkci volat; Důležité jsou pouze typy parametrů. Je vybrána funkce, která nejlépe odpovídá typům předávaných argumentů:

Smallint si = 8; int main() ( calc(34); // Funkce C se nazývá calc(double) calc(si); // Funkce C++ se nazývá calc(const SmallInt &) // ... return 0; )

9.1.6. Ukazatele na přetížené funkce A

Můžete deklarovat ukazatel na jednu z mnoha přetížených funkcí. Například:

Externí void ff(vektor ); extern void ff(unsigned int); // na jakou funkci ukazuje pf1?
void (*pf1)(unsigned int) =

Protože je funkce ff() přetížená, nestačí k výběru samotný inicializátor &ff správná možnost. Aby zjistil, která funkce inicializuje ukazatel, kompilátor hledá v sadě všech přetížených funkcí takovou, která má stejný návratový typ a seznam parametrů jako funkce, na kterou odkazuje ukazatel. V našem případě bude zvolena funkce ff(unsigned int).
Co když neexistuje žádná funkce, která by přesně odpovídala typu ukazatele? Poté kompilátor zobrazí chybovou zprávu:

Externí void ff(vektor ); extern void ff(unsigned int); // chyba: shoda nenalezena: neplatný seznam parametrů void (*pf2)(int) = // chyba: shoda nenalezena: nesprávný návratový typ double (*pf3)(vektor) ) = &ff;

Přiřazení funguje podobně. Pokud má být hodnotou ukazatele adresa přetížené funkce, pak se typ ukazatele funkce použije k výběru operandu na pravé straně operátoru přiřazení. A pokud kompilátor nenajde funkci, která přesně odpovídá požadovanému typu, vydá chybovou zprávu. Převod typů mezi ukazateli funkcí se tedy nikdy neprovádí.

Matrix calc(const matrix &); int calc(int, int); int (*pc1)(int, int) = 0;
int (*pc2)(int, double) = 0; //...
// správně: je vybrána funkce calc(int, int).
pc1 = // chyba: žádná shoda: neplatný typ druhého parametru
pc2 =

9.1.7. Bezpečná vazba A

Při použití přetížení se zdá, že program může mít několik funkcí stejného jména s různými seznamy parametrů. Tato lexikální vymoženost však existuje pouze na úrovni zdrojového textu. Většina systémů kompilace vyžaduje, aby všechny názvy programů, které zpracovávají tento text, aby vytvořily spustitelný kód, byly odlišné. Editory odkazů obecně umožňují externí odkazy lexikálně. Pokud takový editor narazí na název print dvakrát nebo vícekrát, nedokáže je rozlišit analýzou typu (při kterém se obvykle informace o typu ztratí). Takže pouze vytiskne zprávu o předefinovaném symbolu tisku a skončí.
Chcete-li tento problém vyřešit, je název funkce spolu se seznamem parametrů upraven tak, aby vytvořil jedinečný interní název. Programy volané po kompilátoru vidí pouze tento interní název. Jak přesně se tato konverze názvu provádí, závisí na implementaci. Obecnou myšlenkou je reprezentovat počet a typy parametrů jako znakový řetězec a připojit jej k názvu funkce.
Jak je uvedeno v části 8.2, takové kódování zajišťuje zejména to, že dvě deklarace funkcí stejného jména s různými seznamy parametrů, umístěné v různých souborech, nejsou editorem odkazů vnímány jako deklarace stejné funkce. Vzhledem k tomu, že tato metoda pomáhá rozlišit přetížené funkce během fáze editace vazby, hovoříme o bezpečné vazbě.
Dekorace názvů se nevztahuje na funkce deklarované pomocí direktivy extern "C", protože pouze jedna z mnoha přetížených funkcí může být zapsána v čistém C. Dvě funkce s různými seznamy parametrů deklarované jako extern "C" jsou linkerem považovány za jednu stejný symbol.

Cvičení 9.1

Proč možná budete muset deklarovat přetížené funkce?

Cvičení 9.2

Jak bychom měli deklarovat přetížení funkce error(), aby následující volání byla správná:

Int index; int horní hranice; char selectVal; // ... error("Pole mimo hranice: ", index, horní hranice); error("Dělení nulou"); error("Neplatný výběr", selectVal);

Cvičení 9.3

Vysvětlete účinek druhého prohlášení v každém z následujících příkladů:

(a) int calc(int, int); int calc(const int, const int); (b) int get(); double get(); (c) int *reset(int *); double *reset(double *): (d) extern "C" int compute(int *, int); extern "C" double compute(double *, double);

Cvičení 9.4

Která z následujících inicializací vede k chybě? Proč?

(a) void reset(int *); void (*pf)(void *) = reset; (b) int calc(int, int); int (*pf1)(int, int) = calc; (c) extern "C" int compute(int *, int); int (*pf3)(int*, int) = vypočítat; (d) void (*pf4) (konst. matice &) = 0;

9.2. Tři kroky řešení přetížení

Umožňuje přetížení funkcí je proces výběru, kterou funkci volat z mnoha přetížených funkcí. Tento proces je založen na argumentech zadaných během volání. Podívejme se na příklad:

Tti, t2; void f(int, int); void f(float, float); int main() (
f(ti, t2);
návrat 0;
}

Zde se při procesu řešení přetížení v závislosti na typu T určí, zda bude volána funkce f(int,int) nebo f(float,float) nebo bude zaznamenána chyba při zpracování výrazu f(t1). ,t2).
Řešení přetížení funkcí je jedním z nejobtížnějších aspektů jazyka C++. Ve snaze porozumět všem detailům budou začínající programátoři čelit vážným potížím. Proto v tato sekce budeme pouze prezentovat krátká recenze jak dochází k řešení přetížení, abyste si z procesu mohli udělat alespoň nějaký dojem. Pro ty, kteří se chtějí dozvědět více, další dvě části poskytují podrobnější popis.
Proces řešení přetížení funkcí se skládá ze tří kroků, které si ilustrujeme na následujícím příkladu:

Void f(); void f(int); void f(double, double = 3,4); void f(char *, char *); void main() (
f(5,6);
návrat 0;
}

Když je povoleno přetížení funkce, provedou se následující kroky:

  1. Zvýrazní se sada přetížených funkcí pro dané volání a také vlastnosti seznamu argumentů předávaných funkci.
  2. Vyberou se ty z přetížených funkcí, které lze volat s danými argumenty, s ohledem na jejich počet a typy.
  3. Je nalezena funkce, která nejlépe odpovídá volání.

Zvažme každý bod postupně.
Prvním krokem je identifikace sady přetížených funkcí, které budou uvažovány pro dané volání. Funkce obsažené v této sadě se nazývají kandidáti. Kandidátská funkce je funkce se stejným názvem jako volaná a její deklarace je viditelná v místě volání. V našem příkladu jsou čtyři takoví kandidáti: f(), f(int), f(double, double) a f(char*, char*).
Poté jsou identifikovány vlastnosti seznamu předaných argumentů, tzn. jejich počet a druhy. V našem příkladu se seznam skládá ze dvou dvojitých argumentů.
Ve druhém kroku se z mnoha kandidátů vyberou životaschopné - ty, které lze volat s danými argumenty. Životaschopná funkce má buď stejný počet formálních parametrů, jako je počet skutečných argumentů předávaných volané funkci, nebo více, ale pak pro každý další parametr musí být specifikována výchozí hodnota. Aby funkce přežila, jakýkoli skutečný argument předaný ve volání musí mít převod na typ formálního parametru uvedeného v deklaraci.

V našem příkladu existují dvě stojící funkce, které lze volat s danými argumenty:

  • funkce f(int) přežije, protože má pouze jeden parametr a dochází ke konverzi ze skutečného argumentu typu double na formální parametr typu int;
  • funkce f(double,double) přežila, protože pro druhý argument existuje výchozí hodnota a první formální parametr je typu double, což je přesně typ skutečného argumentu.

Pokud po druhém kroku neexistují žádné přežívající funkce, je volání považováno za chybné. V takových případech říkáme, že došlo k nesouladu.
Třetím krokem je výběr funkce, která nejlépe vyhovuje kontextu volání. Tato funkce se nazývá nejlepší postavení (nebo nejlepší přizpůsobení). Tento krok řadí transformace použité k převodu typů skutečných argumentů na typy formálních parametrů zavedené funkce. Za nejvhodnější funkci se považuje ta, pro kterou jsou splněny následující podmínky:
transformace aplikované na skutečné argumenty nejsou horší než transformace potřebné k volání jakékoli jiné zavedené funkce;
U některých argumentů jsou použité převody lepší než převody potřebné k přetypování stejných argumentů ve volání jiných starších funkcí.
Převody typů a jejich pořadí jsou podrobněji popsány v části 9.3. Zde se jen krátce podíváme na žebříček transformací pro náš příklad. Pro stojící funkci f(int) je třeba použít přetypování ze skutečného argumentu typu double na typ int, což je jeden ze standardních. Pro stálou funkci f(double,double) je typ aktuálního argumentu double přesně stejný jako typ formálního parametru. Vzhledem k tomu, že přesná shoda je lepší než standardní konverze (žádná konverze je vždy lepší než jedna), je f(double,double) považována za nejvhodnější funkci pro toto volání.
Pokud ve třetím kroku není možné najít jedinou nejlepší přežívající funkci, jinými slovy neexistuje žádná přežívající funkce, která by byla vhodnější než všechny ostatní, pak je volání považováno za nejednoznačné, tzn. špatně.
(Všechny kroky řešení přetížení funkcí jsou podrobněji popsány v části 9.4. Proces řešení se také používá při volání přetížené funkce člena třídy a přetíženého operátoru. Část 15.10 pojednává o pravidlech pro řešení přetížení, která se vztahují na funkce členů třídy, a Část 15.11 pojednává o pravidlech pro přetížené operátory. Při řešení přetížení byste měli vzít v úvahu také funkce vytvořené ze šablon. Část 10.8 pojednává o tom, jak šablony ovlivňují takové rozlišení.)

Cvičení 9.5

Co se stane v posledním (třetím) kroku procesu řešení přetížení funkcí?

9.3. Konverze typu argumentu A

Ve druhém kroku procesu řešení přetížení funkcí kompilátor identifikuje a seřadí transformace, které by měly být aplikovány na každý aktuální argument volané funkce, aby jej přetypoval na typ odpovídajícího formálního parametru kterékoli z přežívajících funkcí. Hodnocení může přinést jeden ze tří možných výsledků:

  • přesná shoda. Typ skutečného argumentu přesně odpovídá typu formálního parametru. Pokud například sada přetížených funkcí print() obsahuje následující: void print(unsigned int); void print(const char*); void print(char);
  • pak každé z následujících tří volání dává přesnou shodu:
    unsigned int a;
print("a"); // odpovídá print(char); print("a"); // odpovídá print(const char*); tisknout(a); // odpovídá print(unsigned int);
  • soulad s převodem typu. Typ skutečného argumentu neodpovídá typu formálního parametru, ale lze na něj převést: void ff(char); ff(0); // argument typu int je přetypován na typ char
  • nedostatek shody. Typ skutečného argumentu nelze přetypovat na typ formálního parametru v deklaraci funkce, protože požadovaný převod neexistuje. Pro každé z následujících dvou volání print() neexistuje žádná shoda:
  • // funkce print() jsou deklarovány stejným způsobem jako výše int *ip; class SmallInt ( /* ... */ ); SmallInt si; print(ip); // chyba: žádná shoda
    tisknout(si); // chyba: žádná shoda
  • Aby bylo možné stanovit přesnou shodu, typ skutečného argumentu nemusí být stejný jako typ formálního parametru. Na argument lze použít některé triviální transformace, konkrétně:

    • převést l-hodnotu na r-hodnotu;
    • převod pole na ukazatel;
    • převod funkce na ukazatel;
    • transformace specifikátoru.

    (Jsou podrobněji diskutovány níže.) Nejsložitější je shoda kategorie s převodem typu. Je třeba zvážit několik typů castingu: propagace, standardní konverze a uživatelem definované konverze. (V této kapitole se prozkoumáme rozšíření typů a standardní převody. Uživatelsky definované převody budou představeny později, až se podrobně podíváme na třídy; provádí je převodník, členská funkce, která třídě umožňuje definovat vlastní sadu „standardních“ transformací. Na tyto převodníky se podíváme v kapitole 15 a na to, jak ovlivňují rozlišení přetížení funkcí.)
    Při výběru nejlepší existující funkce pro dané volání kompilátor hledá funkci, pro kterou jsou transformace aplikované na skutečné argumenty „nejlepší“. Převody typů jsou seřazeny následovně: přesná shoda je lepší než rozšíření typu, rozšíření typu je lepší než standardní převod, což je zase lepší než převod definovaný uživatelem. K hodnocení se vrátíme v sekci 9.4, ale zatím jednoduché příklady Pojďme si ukázat, jak vám pomůže vybrat tu nejvhodnější funkci.

    9.3.1. Další informace o přesné shodě

    Nejjednodušší případ nastane, když typy skutečných argumentů odpovídají typům formálních parametrů. Níže jsou například uvedeny dvě přetížené funkce max(). Potom každé z volání max() přesně odpovídá jedné z deklarací:

    Int max(int, int); double max(double, double); int i1; void calc(double d1) (
    max(56, i1); // přesně odpovídá max(int, int);
    max(dl, 66,9); // přesně odpovídá max(double, double);
    }

    Typ výčtu přesně odpovídá pouze těm, které jsou v něm definovány výčtové prvky, stejně jako objekty, které jsou deklarovány jako tohoto typu:

    Enum Tokens ( INLINE = 128; VIRTUAL = 129; ); Tokeny curTok = INLINE; enum Stat(Fail, Pass); extern void ff(Tokeny);
    extern void ff(Stat);
    extern void ff(int); int main() (
    ff(Pass); // přesně odpovídá ff(Stat)
    ff(0); // přesně odpovídá ff(int)
    ff(curTok); // přesně odpovídá ff(Tokens)
    // ...
    }

    Výše bylo zmíněno, že skutečný argument může přesně odpovídat formálnímu parametru, i když k přetypování jejich typů je nutná nějaká triviální konverze, z nichž první je převod l-hodnoty na r-hodnotu. L-hodnota je chápána jako objekt, který splňuje následující podmínky:

    • můžete získat adresu objektu;
    • můžete získat hodnotu objektu;
    • tuto hodnotu lze snadno upravit (pokud není v deklaraci objektu specifikátor const).

    Naproti tomu r-hodnota je výraz, jehož hodnota se vyhodnocuje, nebo výraz, který označuje dočasný objekt, pro který nelze získat adresu a jehož hodnotu nelze upravit. Zde je jednoduchý příklad:

    Int calc(int); int main() ( int lval, res; lval = 5; // lvalue: lval; rvalue: 5
    res = calc(lval);
    // lvalue: res
    // rvalue: dočasný objekt pro uložení hodnoty,
    // vrácené funkcí calc().
    návrat 0;
    }

    V prvním příkazu přiřazení je proměnná lval l-hodnota a literál 5 je r-hodnota. V druhém příkazu přiřazení je res hodnota l a dočasný objekt, který ukládá výsledek vrácený funkcí calc(), je hodnota r.
    V některých situacích, v kontextu, kdy se očekává hodnota, můžete použít výraz, který představuje hodnotu l:

    Int obj1; int obj2; int main() (
    // ...
    int local = obj1 + obj2;
    návrat 0;
    }

    Zde obj1 a obj2 jsou l-hodnoty. Aby však bylo možné provést sčítání ve funkci main(), jsou proměnné obj1 a obj2 extrahovány s jejich hodnotami. Akce získání hodnoty objektu reprezentovaného výrazem l-hodnoty se nazývá převod l-hodnoty na r-hodnotu.
    Když funkce očekává argument předaný hodnotou, pokud je argumentem l-hodnota, převede se na r-hodnotu:

    #zahrnout string color("purple"); void print(string); int main() (
    tisknout(barva); // přesná shoda: lvalue conversion
    // k rvalue
    návrat 0;
    }

    Vzhledem k tomu, že argument print(color) je předán hodnotou, provede se převod l-hodnoty na r-hodnotu, aby se extrahovala hodnota barvy a předala ji funkci s prototypem print(string). I když však k takovému přetypování došlo, má se za to, že skutečný argument barvy přesně odpovídá deklaraci print(string).
    Při volání funkcí není vždy nutné aplikovat takovou transformaci na argumenty. Odkaz je l-hodnota; má-li funkce referenční parametr, pak při volání funkce obdrží l-hodnotu. Popsaný převod se tedy nevztahuje na skutečný argument, kterému odpovídá parametr formální reference. Nechte například deklarovat následující funkci:

    #zahrnout neplatný tisk (seznam &);

    V níže uvedeném volání je li l-hodnota představující objekt seznamu předáno funkci print():

    Seznam li(20); int main() (
    // ...
    tisknout(li); // přesná shoda: žádná lvalue to conversion
    // rvalue
    návrat 0;
    }

    Shoda mezi li a referenčním parametrem je považována za přesnou shodu.
    Druhý převod, který stále dosahuje přesné shody, je převod pole na ukazatel. Jak je uvedeno v části 7.3, parametr funkce není nikdy typu pole, ale transformuje se na ukazatel na svůj první prvek. Podobně je argument typu aktuálního pole z NT (kde N je počet prvků v poli a T typ každého prvku) vždy přetypován na typ ukazatele na T. Tato typová konverze aktuálního argumentu se nazývá převod pole na ukazatel. Bez ohledu na to se má za to, že skutečný argument přesně odpovídá formálnímu parametru typu „ukazatel na T“. Například:

    Int ai; void putValues(int *); int main() (
    // ...
    putValues(ai); // přesná shoda: převést pole na
    // ukazatel
    návrat 0;
    }

    Před voláním funkce putValues() se pole převede na ukazatel, což má za následek, že skutečný argument ai (pole tří celých čísel) je přetypován na ukazatel na int. Ačkoli formálním parametrem putValues() je ukazatel a skutečný argument je při volání převeden, existuje mezi nimi přesné mapování.
    Při přesné shodě je také možné převést funkci na ukazatel. (Bylo to zmíněno v sekci 7.9.) Stejně jako parametr pole se parametr funkce stává ukazatelem funkce. Vlastní argument typu „funkce“ je také automaticky přetypován na typ ukazatele funkce. Tento typ konverze skutečného argumentu se nazývá konverze z funkce na ukazatel. Ačkoli je transformace provedena, předpokládá se, že skutečný argument přesně odpovídá formálnímu parametru. Například:

    Int lexicoCompare(const string &, const string &); typedef int (*PFI)(const string &, const string &);
    void sort(řetězec *, řetězec *, PFI); řetězec jako; int main()
    {
    // ...
    seřadit (jako,
    as + sizeof(as)/sizeof(as - 1),
    lexicoCompare // přesná shoda
    // převod funkce na ukazatel
    ); návrat 0;
    }

    Před voláním metody sort() se použije převod funkce na ukazatel, který převede argument lexicoCompare z typu „funkce“ na typ „ukazatel na funkci“. Ačkoli formálním parametrem funkce je ukazatel a skutečným parametrem je název funkce, a proto byla funkce převedena na ukazatel, skutečný argument je považován za přesně třetí formální parametr funkce sort(). .
    Poslední z výše uvedených je převod specifikátorů. To platí pouze pro ukazatele a zahrnuje přidání const nebo volatile (nebo obojí) k typu, který ukazatel adresuje:

    Int a = (4454, 7864, 92, 421, 938); int *pi = a; bool is_equal(const int * , const int *); void func(int *parm) ( // přesná shoda mezi pi a parm: převod specifikátorů
    if (je_rovná se(pí, parm))
    // ... návrat 0;
    }

    Před voláním is_equal() jsou skutečné argumenty pi a parm převedeny z ukazatele na int na ukazatel na const int. Tato transformace zahrnuje přidání specifikátoru const k adresovanému typu, a proto je klasifikována jako konverze specifikátoru. Přestože funkce očekává, že obdrží dva ukazatele na const int a skutečné argumenty jsou ukazatele na ints, formální a skutečné parametry funkce is_equal() jsou považovány za přesně odpovídající.
    Konverze specifikátoru se vztahuje pouze na typ, který adresuje ukazatel. Nepoužívá se, když je formální parametr konstantní nebo volatilní, ale skutečný argument nikoli.

    Exter void takeCI(const int); int main() (
    int ii = ...;
    takeCI(ii); // není použita žádná konverze specifikátorů
    návrat 0;
    }

    Ačkoli formální parametr funkce takeCI() je typu const int a je volán s argumentem ii typu int, nedochází k žádné konverzi specifikátorů: existuje přesná shoda mezi skutečným argumentem a formálním parametrem.
    Vše výše uvedené platí také v případě, že argument je ukazatel a specifikátory const nebo volatile odkazují na tento ukazatel:

    Exter void init(int *const); extern int *pi; int main() (
    // ...
    init(pi); // není použita žádná konverze specifikátorů
    návrat 0;
    }

    Specifikátor const na formálním parametru funkce init() odkazuje na samotný ukazatel, nikoli na typ, který adresuje. Proto kompilátor nebere tento specifikátor v úvahu při analýze transformací, které mají být použity na skutečný argument. Na argument pi není aplikována žádná konverze specifikátoru: předpokládá se, že argument a formální parametr si přesně odpovídají.
    První tři z diskutovaných transformací (l-hodnota na r-hodnotu, pole na ukazatel a funkce na ukazatel) se často nazývají transformace l-hodnoty. (V sekci 9.4 uvidíme, že zatímco transformace l-hodnoty i transformace specifikátoru jsou klasifikovány jako transformace, které neporušují přesnou shodu, míra shody se považuje za vyšší v případě, kdy je nutná pouze první transformace. Budeme mluvit o to trochu podrobněji v další části.)
    Přesnou shodu lze vynutit použitím explicitního přetypování. Pokud například existují dvě přetížené funkce:

    Exter void ff(int); extern void ff(void *);

    Ff(0xffbc); // voláno ff(int)

    bude přesně odpovídat ff(int), ačkoli doslovný 0xffbc je zapsán jako hexadecimální konstanta. Programátor může přinutit kompilátor, aby zavolal funkci ff(void *) explicitním provedením operace typu cast:

    Ff(reinterpret_cast (0xffbc)); // voláno ff(void*)

    Pokud se takové přetypování použije na skutečný argument, převezme typ, na který se převádí. Přetypování explicitního typu pomáhá řídit proces řešení přetížení. Pokud například řešení přetížení vede k nejednoznačnému výsledku (skutečné argumenty se stejně dobře shodují se dvěma nebo více zavedenými funkcemi), pak lze k vyřešení nejednoznačnosti použít přetypování explicitního typu, což donutí kompilátor vybrat konkrétní funkci.

    9.3.2. Další informace o rozšířeních typů

    Rozšířením typu rozumíme jednu z následujících transformací:

    • skutečný argument typu char, unsigned char nebo short je rozšířen na typ int. Skutečný argument typu unsigned short se rozšíří na typ int, pokud je velikost stroje int větší než velikost short, a na typ unsigned int jinak;
    • argument typu float je rozšířen na typ double;
    • argument typu enum se rozšíří na první z následujících typů, který je schopen reprezentovat všechny hodnoty prvků výčtu: int, unsigned int, long, unsigned long;
    • boolovský argument se rozšíří na int.

    Rozšíření, jako je toto, se používá, když je typ skutečného argumentu jedním z právě uvedených typů a formální parametr je odpovídajícího rozšířeného typu:

    Exter void manip(int); int main() (
    manip("a"); // typ znaku se rozšíří na int
    návrat 0;
    }

    Znakový literál je typu char. Rozšiřuje se na int. Protože rozšířený typ odpovídá typu formálního parametru manip(), říkáme, že jeho volání vyžaduje rozšíření typu argument.
    Zvažte následující příklad:

    Externí void print(unsigned int); extern void print(int); extern void print(char); nepodepsaný char uc;
    print(uc); // print(int); uc vyžaduje pouze rozšíření typu

    Pro hardwarovou platformu, kde unsigned char zabírá jeden bajt paměti a int zabírá čtyři bajty, rozšíření převede unsigned char na int, protože může reprezentovat všechny hodnoty unsigned char. Pro tuto architekturu stroje ze sady přetížených funkcí uvedených v příkladu nejlepší shodu pro argument znak bez znaménka poskytuje print(int). U ostatních dvou funkcí vyžaduje párování standardní obsazení.
    Následující příklad ilustruje rozšíření skutečného argumentu typu výčtu:

    Enum Stat (nevyhovuje, vyhovuje); extern void ff(int);
    extern void ff(char); int main() (
    // správně: prvek výčtu Pass je rozbalen na typ int
    ff(Pass); // ff(int)
    ff(0); // ff(int)
    }

    Někdy rozšíření výčtu přináší překvapení. Kompilátoři často volí reprezentaci výčtu v závislosti na hodnotách jeho prvků. Předpokládejme, že ve výše uvedené architektuře (jeden bajt pro char a čtyři bajty pro int) je definován takovýto výčet:

    Výčet el (al, bl, cl);

    Vzhledem k tomu, že existují pouze tři prvky: a1, b1 a c1 s hodnotami 0, 1 a 2 - a protože všechny tyto hodnoty mohou být reprezentovány jako char, kompilátor obvykle zvolí char, aby reprezentoval typ e1. Uvažujme však výčet e2 s následující sadou prvků:

    Výčet e2 (a2, b2, c2=0x80000000);

    Protože jedna z konstant má hodnotu 0x80000000, musí kompilátor zvolit reprezentaci e2 typ, který je dostatečný pro uložení hodnoty 0x80000000, tedy unsigned int.
    Takže i když jsou e1 i e2 výčty, jejich reprezentace se liší. Z tohoto důvodu jsou e1 a e2 rozšířeny na různé typy:

    #zahrnout formát řetězce (int);
    formát řetězce (unsigned int); int main() (
    format(a1); // je voláno format(int).
    formát(a2); // je volán format(unsigned int).
    návrat 0;
    }

    Při prvním volání format() je skutečný argument rozšířen na typ int, protože char se používá k reprezentaci typu e1, a proto je volána přetížená funkce format(int). Při druhém volání je typ skutečného argumentu e2 reprezentován jako unsigned int a argument je rozšířen na unsigned int, což způsobí volání přetíženého formátu funkce (unsigned int). Uvědomte si proto, že chování těchto dvou výčtů s ohledem na proces řešení přetížení se může lišit a závisí na hodnotách prvků, které určují, jak dochází k rozšíření typu.

    9.3.3. Další informace o standardní konverzi

    Existuje pět typů standardních transformací, a to:

    1. převody celočíselných typů: přetypování z celočíselného typu nebo výčtu na jakýkoli jiný celočíselný typ (s výjimkou transformací, které byly výše kategorizovány jako rozšíření typu);
    2. konverze typu s plovoucí desetinnou čárkou: přetypování z jakéhokoli typu s plovoucí desetinnou čárkou na jakýkoli jiný typ s plovoucí desetinnou čárkou (kromě transformací, které byly výše kategorizovány jako rozšíření typu);
    3. převody mezi typem celého čísla a typem s plovoucí desetinnou čárkou: přetypování z libovolného typu s plovoucí desetinnou čárkou na jakýkoli typ celého čísla nebo naopak;
    4. konverze ukazatelů: přetypování celočíselné hodnoty 0 na typ ukazatele nebo transformace ukazatele libovolného typu na typ void*;
    5. převody bool: Přetypování z libovolného celého čísla, typu float, výčtu nebo ukazatele na typ bool.

    Zde jsou nějaké příklady:

    Exter void print(void*); extern void print(double); int main() (
    int i;
    tisknout(i); // odpovídá print(double);
    // i prochází standardní převod z int na double
    print(&i); // odpovídá print(void*);
    // &i prochází standardní konverzí
    // od int* do void*
    návrat 0;
    }

    Konverze ve skupinách 1, 2 a 3 jsou potenciálně nebezpečné, protože cílový typ nemusí reprezentovat všechny hodnoty zdroje. Například float nemůže adekvátně reprezentovat všechny hodnoty int. Z tohoto důvodu jsou transformace zahrnuté v těchto skupinách kategorizovány jako standardní transformace spíše než typová rozšíření.

    Int i; void calc(float); int main() ( calc(i); // standardní převod mezi celým číslem a typem s plovoucí desetinnou čárkou // je potenciálně nebezpečný v závislosti na // hodnotě i return 0; )

    Při volání calc() se použije standardní převod z typu integer na typ float s plovoucí desetinnou čárkou. V závislosti na hodnotě i nemusí být možné jej uložit jako plovoucí bez ztráty přesnosti.
    Předpokládá se, že všechny standardní změny vyžadují stejné množství práce. Například převod ze znaku na znak bez znaménka nemá vyšší prioritu než převod ze znaku na dvojitý. Podobnost typů se nebere v úvahu. Pokud dvě stojící funkce vyžadují standardní transformaci skutečného argumentu, aby se shodoval, je volání považováno za nejednoznačné a kompilátor jej označí jako chybu. Pokud jsou například zadány dvě přetížené funkce:

    Exter void manip(long); extern void manip(float);

    pak je následující volání nejednoznačné:

    Int main() ( manip(3.14); // chyba: nejednoznačnost // manip(float) není o nic lepší než manip(int) return 0; )

    Konstanta 3.14 je typu double. Pomocí jednoho nebo druhého standardního převodu lze vytvořit shodu s kteroukoli z přetížených funkcí. Vzhledem k tomu, že k cíli vedou dvě transformace, je výzva považována za nejednoznačnou. Žádná transformace není nadřazena jiné. Programátor může nejednoznačnost vyřešit buď explicitním přetypováním:

    Manip(static_cast (3,14)); // manip(long)

    nebo pomocí přípony označující, že konstanta je typu float:

    Manip (3,14F)); // manip(float)

    Zde je několik dalších příkladů nejednoznačných volání, která jsou označena jako chyby, protože odpovídají více přetíženým funkcím:

    Exter void farith(unsigned int); extern void farith(float); int main() (
    // každé z následujících volání je nejednoznačné
    farith("a"); // argument je typu char
    farith(0); // argument je typu int
    farith(2uL); // argument je typu unsigned long
    farith(3,14159); // argument je typu double
    farith(pravda); // argument je typu bool
    }

    Standardní převody ukazatelů jsou někdy neintuitivní. Konkrétně je hodnota 0 přetypována na ukazatel na jakýkoli typ; takto získaný ukazatel se nazývá nulový ukazatel. Hodnota 0 může být reprezentována jako konstantní výraz typu celé číslo:

    Void set(int*); int main() (
    // převod ukazatele z 0 na int* aplikovaný na argumenty
    // v obou voláních
    sada(0L);
    set(0x00);
    návrat 0;
    }

    Konstantní výraz 0L (hodnota long int 0) a konstantní výraz 0x00 (hexadecimální celočíselná hodnota 0) jsou celočíselného typu a lze je tedy převést na nulový ukazatel typu int*.
    Ale protože výčty nejsou celočíselné typy, prvek rovný 0 nelze přetypovat na typ ukazatele:

    Enum EN (zr = 0); set(zr); // chyba: zr ​​nelze převést na int*

    Volání set() je chybou, protože nedochází k žádné konverzi mezi hodnotou zr prvku výčtu a formálním parametrem typu int*, i když zr je 0.
    Je třeba poznamenat, že konstantní výraz 0 je typu int. K přetypování na typ ukazatele je vyžadován standardní převod. Pokud je v množině přetížených funkcí funkce s formálním parametrem typu int, bude přetížení vyřešeno v její prospěch v případě, že skutečný argument je 0:

    Void print(int); void print(void *); void set(const char *);
    void set(char *); int main()(
    print(0); // voláno print(int);
    set(0); // dvojznačnost
    návrat 0;
    }

    Volání print(int) má za následek přesnou shodu, zatímco volání print(void*) vyžaduje přetypování hodnoty 0 na typ ukazatele. Protože shoda je lepší než transformace, je toto volání vybráno k vyřešení funkce tisku(int). Volání set() je nejednoznačné, protože 0 odpovídá formálním parametrům obou přetížených funkcí aplikací standardní transformace. Protože jsou obě funkce stejně dobré, je nejednoznačnost opravena.
    Poslední z možných převodů ukazatelů vám umožňuje přetypovat ukazatel jakéhokoli typu na typ void*, protože void* je obecný ukazatel na jakýkoli datový typ. Zde jsou nějaké příklady:

    #zahrnout extern void reset(void *); void func(int *pi, řetězec *ps) (
    // ...
    reset(pi); // převod ukazatele: int* na void*
    /// ...
    reset(ps); // převod ukazatele: řetězec* na void*
    }

    Pouze ukazatele na datové typy lze přetypovat na typ void* pomocí standardní konverze; to nelze provést pomocí ukazatelů na funkce:

    Typedef int (*PFV)(); externí PFV testCases; // pole ukazatelů funkcí extern void reset(void *); int main() (
    // ...
    reset(textCases); // chyba: žádná standardní konverze
    // mezi int(*)() a void*
    návrat 0;
    }

    9.3.4. Odkazy

    Skutečným argumentem nebo formálním parametrem funkce může být odkaz. Jak to ovlivní pravidla převodu typů?
    Podívejme se, co se stane, když je odkaz skutečným argumentem. Jeho typ není nikdy referenčním typem. S argumentem reference se zachází jako s hodnotou l, jejíž typ odpovídá typu odpovídajícího objektu:

    Int i; int& ri = i; void print(int); int main() (
    tisknout(i); // argument je lvalue typu int
    print(ri); // stejný
    návrat 0;
    }

    Skutečný argument v obou voláních je typu int. Použití odkazu k jeho předání ve druhém volání neovlivní typ samotného argumentu.
    Standardní převody typu a rozšíření zvažované kompilátorem jsou stejné, když je skutečným argumentem odkaz na typ T a když on sám je takovým typem. Například:

    Int i; int& ri = i; void calc(double); int main() (
    calc(i); // standardní převod mezi celočíselnými typy
    // a typ s plovoucí desetinnou čárkou
    calc(ri); // stejný
    návrat 0;
    }

    Jak formální referenční parametr ovlivňuje transformace použité na skutečný argument? Srovnání dává následující výsledky:

    • aktuální argument je vhodný jako inicializátor referenčního parametru. V tomto případě říkáme, že mezi nimi existuje přesná shoda: void swap(int &, int &); void manip(int i1, int i2) (
      // ...
      swap(i1,i2); // správně: volá se swap(int &, int &).
      // ...
      návrat 0;
      }
    • skutečný argument nemůže inicializovat referenční parametr. V této situaci neexistuje přesná shoda a argument nelze použít k volání funkce. Například:
    • int obj; void frd(double &); int main() ( frd(obj); // chyba: parametr musí být typu const double & return 0; )
    • Volání funkce frd() je chyba. Skutečný argument je typu int a musí být převeden na typ double, aby odpovídal parametru formální reference. Výsledkem této transformace je dočasná proměnná. Protože odkaz nemá specifikátor const, nelze takové proměnné použít k jeho inicializaci.
      Zde je další příklad, kdy neexistuje žádná korespondence mezi formálním referenčním parametrem a skutečným argumentem:
    • třída B; void takeB(B&); B dátB(); int main() (
      takeB(giveB()); // chyba: parametr musí být typu const B &
      návrat 0;
      }
    • Volání funkce takeB() je chyba. Skutečným argumentem je návratová hodnota, tzn. dočasná proměnná, kterou nelze použít k inicializaci odkazu bez specifikátoru const.
      V obou případech vidíme, že pokud má formální referenční parametr specifikátor const, lze mezi ním a skutečným argumentem provést přesné mapování.

    Je třeba poznamenat, že jak převod l-hodnoty na r-hodnotu, tak referenční inicializace jsou považovány za přesné shody. V tomto příkladu vede první volání funkce k chybě:

    Void print(int); void print(int&); int iobj;
    int &ri = iobj; int main() (
    print(iobj); // chyba: nejednoznačnost
    print(ri); // chyba: nejednoznačnost
    tisk(86); // správně: volá se print(int).
    návrat 0;
    }

    Objekt iobj je argument, který lze přiřadit oběma funkcím print(), což znamená, že volání je nejednoznačné. Totéž platí pro další řádek, kde odkaz ri označuje objekt odpovídající oběma funkcím print(). Třetí hovor je však v pořádku. Pro něj není print(int&) udržitelný. Celočíselná konstanta je r-hodnota, takže nemůže inicializovat referenční parametr. Jediná přežívající funkce pro volání print(86) je print(int), proto je zvolena, když je vyřešeno přetížení.
    Stručně řečeno, pokud je formální parametr odkaz, pak skutečný argument je přesná shoda, pokud může inicializovat odkaz, a ne jinak.

    Cvičení 9.6

    Vyjmenujte dvě triviální transformace, které jsou povoleny při stanovení přesné shody.

    Cvičení 9.7

    Jaké je pořadí každého z převodů argumentů v následujících voláních funkcí:

    (a) void print(int *, int); int arr; print(arr, 6); // volání funkce (b) void manip(int, int); manip("a", "z"); // volání funkce (c) int calc(int, int); dvojité dobj; double = calc(55.4, dobj) // volání funkce (d) void set(const int *); int *pi; set(pi); // volání funkce

    Cvičení 9.8

    Které z těchto volání se nezdaří, protože mezi typem skutečného argumentu a formálním parametrem nedochází k žádné konverzi:

    (a) enum Stat(nevyhovuje, vyhovuje); void test(Stat); text(0); // volání funkce (b) void reset(void *); reset(0); // volání funkce (c) void set(void *); int *pi; set(pi); // volání funkce (d) #include seznam oper(); void print(oper()); // volání funkce (e) void print(const int); int iobj; print(iobj); // volání funkce

    9.4. Podrobnosti o rozlišení funkce přetížení

    Již jsme zmínili v sekci 9.2, že proces řešení přetížení funkcí se skládá ze tří kroků:

    1. Nastavte sadu kandidátských funkcí pro vyřešení daného volání, stejně jako skutečné vlastnosti seznamu argumentů.
    2. Vyberte z různých kandidátů zavedené funkce - ty, které lze volat s daným seznamem skutečných argumentů, s ohledem na jejich počet a typy.
    3. Vyberte funkci, která nejlépe odpovídá volání, a seřaďte transformace, které je třeba aplikovat na skutečné argumenty, aby odpovídaly formálním parametrům přežívající funkce.

    Nyní jsme připraveni prostudovat tyto kroky podrobněji.

    9.4.1. Kandidátské funkce

    Kandidátská funkce je funkce, která má stejný název jako volaná funkce. Kandidáti se hledají dvěma způsoby:

    • Deklarace funkce je viditelná v místě volání. V následujícím příkladu
      void f(); void f(int); void f(double, double = 3,4); void f(char*, char*); int main() (
      f(5,6); // jsou čtyři kandidáti na vyřešení tohoto hovoru
      návrat 0;
      }
    • všechny čtyři funkce f() splňují tuto podmínku. Sada kandidátů proto obsahuje čtyři prvky;
    • pokud je typ skutečného argumentu deklarován v rámci jmenného prostoru, pak členské funkce tohoto jmenného prostoru, které mají stejný název jako volaná funkce, jsou přidány do kandidátní sady: jmenný prostor NS ( class C ( /* ... */ ); void takeC( C&); ) // typ cobj je třída C deklarovaná ve jmenném prostoru NS
      NS::Cobj; int main() (
      // žádná z funkcí takeC() není v místě volání viditelná
      takeC(cobj); // správně: NS::takeC(C&) se nazývá,
      // protože argument je typu NS::C, tedy
      // funkce takeC() je brána v úvahu,
      // deklarováno v jmenném prostoru NS
      návrat 0;
      }

    Množina kandidátů je tedy svaz mnoho funkcí, viditelné v bodě volání, a sada funkcí deklarovaných ve stejném jmenném prostoru, do kterého patří skutečné typy argumentů.
    Při identifikaci sady přetížených funkcí viditelných v místě volání platí pravidla popsaná dříve.
    Funkce deklarovaná ve vnořeném oboru spíše skrývá než přetěžuje funkci stejného názvu ve vnějším rozsahu. V takové situaci budou kandidáty pouze funkce z vnořeného rozsahu, tzn. ty, které se při volání neskryjí. V následujícím příkladu jsou kandidátské funkce viditelné v místě volání format(double) a format(char*):

    Char* formát (int); void g() ( char *format(double); char* format(char*); format(3); // formát volání (double)
    }

    Protože format(int) deklarovaný v globálním rozsahu je skrytý, není zahrnut v sadě kandidátských funkcí.
    Kandidáty lze představit pomocí prohlášení viditelných na kontaktním místě:

    Jmenný prostor libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char); void func()
    {
    // funkce z jmenného prostoru jsou neviditelné
    // všechna tři volání jsou vyřešena ve prospěch globální funkce max(char, char)
    max(87, 65);
    max(35,5, 76,6);
    max("J", "L");
    }

    Funkce max() definované ve jmenném prostoru libs_R_us jsou v bodě volání neviditelné. Jedinou viditelnou funkcí je max() z globálního rozsahu; pouze je součástí sady kandidátských funkcí a je volána při každém ze tří volání funkce func(). Můžeme použít deklaraci using, abychom zviditelnili funkce max() ve jmenném prostoru libs_R_us. Kam umístit deklaraci použití? Pokud to zahrnete do globálního rozsahu:

    Char max(char, char); pomocí libs_R_us::max; // pomocí deklarace

    pak jsou funkce max() z libs_R_us přidány do sady přetížených funkcí, která již obsahuje max() deklarovanou v globálním rozsahu. Nyní jsou všechny tři funkce viditelné uvnitř funkce func() a stávají se kandidáty. V této situaci jsou volání funkce func() vyřešena následovně:

    Void func() ( max(87, 65); // voláno libs_R_us::max(int, int) max("J", "L"); // voláno::max(char, char) )

    Co se ale stane, když do místního rozsahu funkce func() zavedeme deklaraci using, jak ukazuje tento příklad?

    Void func() ( // pomocí deklarace pomocí libs_R_us::max; // volání stejné funkce jako výše
    }

    Které funkce max() by byly zahrnuty do sady kandidátů? Připomeňme, že pomocí deklarací jsou vnořeny do sebe. Pokud je takové prohlášení v místní působnosti globální funkce max(char, char) je skrytý, takže pouze

    Libs_R_us::max(int, int); libs_R_us::max(double, double);

    Jsou to kandidáti. Volání funkce func() jsou nyní vyřešena následovně:

    Void func() ( // pomocí deklarace // globální funkce max(char, char) je skryta pomocí libs_R_us::max; max(87, 65); // je volána libs_R_us::max(int, int)
    max(35,5, 76,6); // voláno libs_R_us::max(double, double)
    max("J", "L"); // s názvem libs_R_us::max(int, int)
    }

    Použití direktiv také ovlivňuje složení množiny kandidátských funkcí. Řekněme, že se je rozhodneme použít ke zviditelnění funkcí max() z jmenného prostoru libs_R_us ve funkci func(). Pokud do globálního rozsahu umístíme následující direktivu using, sada kandidátských funkcí se bude skládat z globální funkce max(char, char) a funkcí max(int, int) a max(double, double) deklarovaných v libs_R_us:

    Jmenný prostor libs_R_us ( int max(int, int); double max(double, double); ) char max(char, char);
    pomocí jmenného prostoru libs_R_us; // pomocí direktivy void func()
    {
    max(87, 65); // s názvem libs_R_us::max(int, int)
    max(35,5, 76,6); // voláno libs_R_us::max(double, double)
    }

    Co se stane, když vložíte direktivu using do místního rozsahu, jako v následujícím příkladu?

    Void func() ( // pomocí direktivy pomocí jmenného prostoru libs_R_us; // stejné volání funkce jako výše
    }

    Které funkce max() by byly mezi kandidáty? Připomeňme, že direktiva using zviditelní členy jmenného prostoru, jako by byly deklarovány mimo jmenný prostor v místě, kde je direktiva umístěna. V našem příkladu jsou členové libs_R_us viditelné v lokálním rozsahu funkce func(), jako by byly deklarovány mimo prostor – v globálním rozsahu. Z toho vyplývá, že množina přetížených funkcí viditelných uvnitř func() je stejná jako dříve, tzn. zahrnuje

    Max(char, char); libs_R_us::max(int, int); libs_R_us::max(double, double);

    Direktiva using se objevuje v místním nebo globálním rozsahu; neovlivňuje rozlišení volání funkce func():

    Void func() ( s použitím jmenného prostoru libs_R_us; max(87, 65); // s názvem libs_R_us::max(int, int)
    max(35,5, 76,6); // voláno libs_R_us::max(double, double)
    max("J", "L"); // voláno::max(int, int)
    }

    Sada kandidátů se tedy skládá z funkcí viditelných v okamžiku volání, včetně funkcí zavedených pomocí deklarací a pomocí direktiv, a také z funkcí deklarovaných ve jmenných prostorech spojených s typy skutečných argumentů. Například:

    Jmenný prostor basicLib ( int print(int); double print(double); ) matice jmenného prostoruLib ( matice třídy ( /* ... */ ); void print(const maxtrix &); ) void display() ( pomocí basicLib::print matrixLib::matrix mObj;
    print(mObj); // voláno maxtrixLib::print(const maxtrix &) print(87); // s názvem basicLib::print(const maxtrix &)
    }

    Kandidáty na print(mObj) jsou funkce basicLib::print(int) a basicLib::print(double) zavedené deklarací using uvnitř display(), protože jsou viditelné v bodě volání. Protože skutečný argument funkce je typu matrixLib::matrix, funkce print() deklarovaná ve jmenném prostoru matrixLib bude také kandidátem. Jaké jsou kandidátské funkce pro print(87)? V místě volání jsou viditelné pouze basicLib::print(int) a basicLib::print(double). Protože argument je typu int, další jmenný prostor se při hledání dalších kandidátů nebere v úvahu.

    9.4.2. Starší funkce

    Zavedená funkce je jedním z kandidátů. Jeho formální seznam parametrů má buď stejný počet prvků jako skutečný seznam argumentů volané funkce, nebo více. V druhém případě pro dodatečné parametry jsou zadány výchozí hodnoty, jinak nelze funkci volat s daným počtem argumentů. Aby funkce přežila, musí existovat konverze každého aktuálního argumentu na typ odpovídajícího formálního parametru. (Takové převody byly diskutovány v části 9.3.)
    V následujícím příkladu má volání f(5.6) dvě stálé funkce: f(int) a f(double).

    Void f(); void f(int); void f(double); void f(char*, char*); int main() (
    f(5,6); // 2 zavedené funkce: f(int) a f(double)
    návrat 0;
    }

    Funkce f(int) přežila, protože má pouze jeden formální parametr, který odpovídá počtu skutečných argumentů ve volání. Navíc je zde standardní převod argumentu typu double na int. Funkce f(double) také přežila; má také jeden parametr typu double a přesně odpovídá skutečnému argumentu. Kandidátské funkce f() a f(char*, char*) jsou vyloučeny z přežívajícího seznamu, protože je nelze volat s jediným argumentem.
    V následujícím příkladu je jedinou přežívající funkcí pro volání format(3) format(double). Přestože kandidát format(char*) lze volat s jediným argumentem, nedochází k žádné konverzi z typu skutečného argumentu na typ formálního parametru char*, a proto nelze funkci považovat za nefunkční.

    Char* formát (int); void g() ( // formát globální funkce (int) je skrytý char* format(double); char* format(char*); format(3); // existuje pouze jedna přežívající funkce: format(double) )

    V následujícím příkladu všechny tři kandidátské funkce přežijí volání max() uvnitř func(). Všechny je lze vyvolat dvěma argumenty. Protože skutečné argumenty jsou typu int, odpovídají přesně formálním parametrům funkce libs_R_us::max(int, int) a lze je přetypovat na typy parametrů funkce libs_R_us::max(double, double) převodem celá čísla na plovoucí, stejně jako na parametry typů funkce libs_R_us::max(char, char) převodem celočíselných typů.


    pomocí libs_R_us::max; char max(char, char);
    void func()
    {
    // všechny tři funkce max() jsou zastaralé
    max(87, 65); // voláno pomocí libs_R_us::max(int, int)
    }

    Všimněte si, že kandidátní funkce s více parametry je vyřazena z postavení, jakmile se zjistí, že jeden ze skutečných argumentů nelze přetypovat na typ odpovídajícího formálního parametru, i když takový převod existuje pro všechny ostatní argumenty. V následujícím příkladu je funkce min(char *, int) vyloučena ze zbývající sady, protože neexistuje způsob, jak transformovat typ prvního argumentu, int, na typ odpovídajícího parametru char *. K tomu dochází, přestože druhý argument přesně odpovídá druhému parametru.

    Exter double min(double, double); extern double min(char*, int); void func()
    {
    // jedna kandidátní funkce min(double, double)
    min(87,65); // je voláno min(double, double).
    }

    Pokud po vyloučení z množiny kandidátů všech funkcí s nevhodným počtem parametrů a těch, pro které neexistovala vhodná transformace pro parametry, neexistují žádné přeživší, pak zpracování volání funkce skončí s chybou kompilace. V tomto případě říkají, že nebyla nalezena žádná shoda.

    Void print(unsigned int); void print(char*); void print(char); int *ip;
    class SmallInt ( /* ... */ );
    SmallInt si; int main() (
    print(ip); // chyba: žádné přežívající funkce: nebyla nalezena shoda
    tisknout(si); // chyba: žádné přežívající funkce: nebyla nalezena shoda
    návrat 0;
    }

    9.4.3. Nejlepší funkce vestoje

    Nejlépe zavedená funkce je ta, jejíž formální parametry nejvíce odpovídají typům skutečných argumentů. U každé takové funkce jsou převody typu použité na každý argument seřazeny, aby se určilo, jak dobře odpovídá parametru. (Oddíl 6.2 popisuje podporované převody typů.) Nejlépe fungující funkce je taková, pro kterou jsou současně splněny dvě podmínky:

    • transformace aplikované na argumenty nejsou horší než transformace potřebné k volání jakékoli jiné zavedené funkce;
    • Alespoň pro jeden argument je aplikovaný převod lepší než pro stejný argument v jakékoli jiné stojící funkci.

    Může se stát, že k přetypování skutečného argumentu na typ odpovídajícího formálního parametru je nutné provést několik konverzí. Takže v následujícím příkladu

    Int arr; void putValues(const int *); int main() (
    putValues(arr); // Jsou potřeba 2 transformace
    // převod pole na ukazatel + specifikátor
    návrat 0;
    }

    Chcete-li převést argument arr z typu „array of three int“ na typ „pointer to const int“, použije se sekvence transformací:

    1. Převod pole na ukazatel, který transformuje pole tří int na ukazatel na int.
    2. Konverze specifikátoru, která transformuje ukazatel na int na ukazatel na const int.

    Proto by bylo správnější říci, že k přetypování skutečného argumentu na typ formálního parametru zavedené funkce je zapotřebí sekvence konverzí. Protože je aplikováno více než jedna transformace, třetí krok procesu řešení přetížení funkcí ve skutečnosti řadí posloupnost transformací.
    Pořadí takové posloupnosti se považuje za pořadí nejhorší z transformací, které jsou v něm obsaženy. Jak je vysvětleno v části 9.2, převody typu jsou seřazeny následovně: přesná shoda je lepší než rozšíření typu a rozšíření typu je lepší než standardní převod. V předchozím příkladu mají obě změny přesné pořadí shody. Proto má celá sekvence stejnou hodnost.
    Taková sada se skládá z několika transformací aplikovaných v určeném pořadí:

    Převod l-hodnoty -> rozšíření typu nebo standardní převod -> převod specifikátoru

    Termín konverze l-hodnoty se vztahuje k prvním třem transformacím přesné shody diskutovaným v části 9.2: konverze l-hodnoty na r-hodnotu, konverze pole na ukazatel a konverze funkce na ukazatel. Posloupnost transformací se skládá z nulové nebo jedné konverze l-hodnoty, po níž následuje nulová nebo jedna typová rozšíření nebo standardní konverze a nakonec nulová nebo jedna konverze specifikátoru. K přetypování skutečného argumentu na typ formálního parametru lze použít pouze jednu transformaci každého typu.

    Popsaná sekvence se nazývá sekvence standardních transformací. Existuje také sekvence definované uživatelem konverze, která je spojena s funkcí převodníku, která je členem třídy. (Konvertory a uživatelsky definované konverzní sekvence jsou popsány v kapitole 15.)

    Jaké jsou sekvence změn skutečných argumentů v následujícím příkladu?

    Jmenný prostor libs_R_us ( int max(int, int); double max(double, double); ) // pomocí deklarace
    pomocí libs_R_us::max; void func()
    {
    char cl, c2;
    max(cl, c2); // s názvem libs_R_us::max(int, int)
    }

    Argumenty volání funkce max() jsou typu char. Posloupnost transformací argumentů při volání funkce libs_R_us::max(int,int) je následující:

    1a. Protože argumenty jsou předávány hodnotou, hodnoty argumentů c1 a c2 se získávají převodem l-hodnoty na r-hodnotu.

    2a. Type extension převádí argumenty z char na int.
    Posloupnost transformací argumentů při volání funkce libs_R_us::max(double,double) je následující:
    1b. Převedením l-hodnoty na r-hodnotu se extrahují hodnoty argumentů c1 a c2.

    2b. Standardní převod mezi celým číslem a plovoucím typem přetypuje argumenty z typu char na typ double.

    Hodnost první sekvence je rozšířením typu (nejhorší z použitých změn), zatímco hodnost druhé je standardní převod. Vzhledem k tomu, že rozšíření typu je lepší než konverze, je funkce libs_R_us::max(int,int) vybrána jako nejlepší přežívající funkce pro toto volání.
    Pokud klasifikace sekvence transformací argumentů nedokáže identifikovat jedinou přežívající funkci, je volání považováno za nejednoznačné. V tomto příkladu vyžadují obě volání calc() následující sekvenci:

    1. Převeďte l-hodnotu na r-hodnotu, abyste extrahovali hodnoty argumentů i a j.
    2. Standardní převod pro přetypování typů skutečných argumentů na typy odpovídajících formálních parametrů.

    Protože není možné říci, která z těchto sekvencí je lepší než druhá, je volání nejednoznačné:

    Int i, j; extern long calc(long, long); extern double calc(double, double); void jj() ( // chyba: nejednoznačnost, žádná nejlepší shoda
    calc(i, j);
    }

    Konverze specifikátoru (přidání const nebo volatile specifikátoru k typu, který adresuje ukazatel) má úroveň přesné shody. Pokud se však dvě sekvence transformací liší pouze tím, že jedna z nich má na konci další transformaci specifikátoru, pak je sekvence bez ní považována za lepší. Například:

    Void reset(int *); void reset(const int *); int*pi; int main() (
    reset(pi); // bez převodu specifikátorů je lepší:
    // vyberte reset(int *)
    návrat 0;
    }

    Sekvence standardních transformací aplikovaných na skutečný argument pro první kandidátskou funkci reset(int*) je přesná shoda, která vyžaduje pouze přechod z hodnoty l na hodnotu r, aby se extrahovala hodnota argumentu. Druhá kandidátní funkce reset(const int *) také používá transformaci l-hodnoty na r-hodnotu, ale je také následována převodem specifikátoru pro převod výsledné hodnoty z ukazatele na int na ukazatel na const int. Obě sekvence se přesně shodují, ale neexistuje žádná nejednoznačnost. Protože se druhá sekvence od první liší přítomností transformace specifikátorů na konci, je sekvence bez takové transformace považována za lepší. Proto by nejlepší stálá funkce byla reset(int*).
    Zde je další příklad, kdy specifikátory castingu ovlivňují zvolenou sekvenci:

    Int extrakt(void *);
    int extrakt(const void *);

    int main() (
    extrakt(pi); // vybrat extrakt(void *)
    návrat 0;
    }

    Zde lze volat dvě zavedené funkce: extract(void*) a extract(const void*). Posloupnost transformací pro funkci extract(void*) sestává z transformace l-hodnoty na r-hodnotu k extrakci. hodnoty argumentů, doprovázený standardní konverzí ukazatele: z ukazatele na int na ukazatel na void. U funkce extract(const void*) se tato sekvence od první liší dodatečnou konverzí specifikátorů pro převod typu výsledku z ukazatele na void na ukazatel na const void. Vzhledem k tomu, že se sekvence liší pouze touto transformací, je první zvolena jako vhodnější, a proto nejlépe přežívající funkcí bude funkce extrakt (const void*).
    Specifikátory const a volatile také ovlivňují pořadí inicializace referenčních parametrů. Pokud se dvě takové inicializace liší pouze přidáním specifikátoru const a volatile, pak se inicializace bez dodatečné specifikace považuje za lepší při řešení přetížení:

    #zahrnout void manip(vektor &); void manip(const vector &); vektor F();
    externí vektor vec; int main() (
    manip(vec); // vyberte manip(vector &)
    manip(f()); // vyberte manip(const vector &)
    návrat 0;
    }

    V prvním volání je inicializace odkazů pro volání jakékoli funkce přesnou shodou. Tato výzva ale nebude dvojznačná. Vzhledem k tomu, že obě inicializace jsou ve všech směrech totožné s výjimkou přítomnosti dodatečné specifikace const ve druhém případě, inicializace bez takové specifikace se považuje za lepší, takže přetížení bude vyřešeno ve prospěch starší funkce manip(vector &).
    Pro druhé volání existuje pouze jedna přežívající funkce manip(const vector &). Protože skutečný argument je dočasná proměnná obsahující výsledek vrácený f(), takový argument je r-hodnota, kterou nelze použít k inicializaci nekonstantního formálního referenčního parametru na manip(vector &). Proto je nejlepší jediný přežívající manip(const vector &).
    Funkce samozřejmě mohou mít více skutečných argumentů. Výběr toho nejlepšího ze stálých by měl být proveden s ohledem na pořadí sekvencí transformací všech argumentů. Podívejme se na příklad:

    Extern int ff(char*, int); extern int ff(int, int); int main() ( ff(0, "a"); // ff(int, int)
    návrat 0;
    }

    Funkce ff(), která přebírá dva argumenty int, je vybrána jako nejvhodnější funkce z následujících důvodů:

    1. její první argument je lepší. 0 poskytuje přesnou shodu s formálním parametrem int, zatímco pro shodu s parametrem char * je vyžadována standardní konverze ukazatele;
    2. jeho druhý argument má stejnou hodnost. Argument "a" typu char musí podléhat posloupnosti převodů, které mají úroveň rozšíření typu, aby odpovídaly druhému formálnímu parametru kterékoli funkce.

    Zde je další příklad:

    Int compute(const int&, short); int vypočítat(int&, double); extern int iobj;
    int main() (
    compute(iobj, "c"); // compute(int&, double)
    návrat 0;
    }

    Funkce compute (const int&, short) i compute (int&, double) přežily. Druhý je vybrán jako nejlepší z následujících důvodů:

    1. její první argument je lepší. Inicializace odkazu pro první přežívající funkci je horší, protože vyžaduje přidání specifikátoru const, který není potřeba pro druhou funkci;
    2. jeho druhý argument má stejnou hodnost. Na argument "c" typu char musí být aplikována sekvence transformací majících úroveň standardní transformace, aby se vytvořila korespondence s druhým formálním parametrem kterékoli z těchto dvou funkcí.

    9.4.4. Argumenty s výchozími hodnotami

    Argumenty s výchozími hodnotami mohou rozšířit mnoho zavedených funkcí. Zbývající funkce jsou ty, které jsou volány s daným seznamem skutečných argumentů. Ale taková funkce může mít více formálních parametrů než skutečné dané argumenty v případě, že pro každý nespecifikovaný parametr existuje výchozí hodnota:

    Exter void ff(int); extern void ff(long, int = 0); int main() (
    ff(21); // odpovídá ff(long, 0); ff(0, 0); // odpovídá ff(long, int);
    ff(0); // odpovídá ff(int);
    ff(3,14); // chyba: nejednoznačnost
    }

    U prvního a třetího volání funkce ff() přetrvá, i když je předán pouze jeden skutečný argument. Důvodem jsou následující důvody:

    1. druhý formální parametr má výchozí hodnotu;
    2. první parametr typu long přesně odpovídá skutečnému argumentu v prvním volání a může být přetypován na typ argumentu ve třetím volání sekvencí se standardním stupněm konverze.

    Poslední volání je nejednoznačné, protože obě přetrvávající funkce lze vybrat použitím standardního převodu na první argument. Funkce ff(int) není preferována jen proto, že má jeden parametr.

    Cvičení 9.9

    Vysvětlete, co se stane, když je vyřešeno přetížení pro volání funkce compute() uvnitř main(). Jaké funkce jsou kandidáti? Které z nich přežijí první krok? Jaká sekvence transformací musí být aplikována na aktuální argument, aby odpovídal formálnímu parametru pro každou přežívající funkci? Jaká by byla nejlepší zbývající funkce?

    Jmenný prostor primerLib ( void compute(); void compute(const void *); ) pomocí primerLib::compute;
    void compute(int);
    void compute(double, double = 3,4);
    void compute(char*, char* = 0); int main() (
    compute(0);
    návrat 0;
    }

    Co se stane, když před voláním compute() vložíte deklaraci using do main()? Odpovězte na stejné otázky.

    Přetížení funkcí znamená definování několika funkcí (dvě nebo více) se stejným názvem, ale různými parametry. Sady parametrů přetížených funkcí se mohou lišit v pořadí, počtu a typu. Přetížení funkcí je tedy nezbytné, aby se zabránilo duplikaci názvů funkcí, které provádějí podobné akce, ale s odlišnou logikou programu. Zvažte například funkci areaRectangle(), která vypočítá plochu obdélníku.

    Float areaRectangle(float, float) //funkce, která vypočítá plochu obdélníku se dvěma parametry a(cm) a b(cm) (vrátí a * b; // vynásobí délky stran obdélníku a vrátí se výsledný produkt)

    Jedná se tedy o funkci se dvěma parametry typu float a argumenty předávané funkci musí být v centimetrech, návratová hodnota typu float musí být také v centimetrech.

    Předpokládejme, že naše počáteční údaje (strany obdélníku) jsou uvedeny v metrech a centimetrech, například: a = 2m 35 cm; b = 1m 86 cm V tomto případě by bylo vhodné použít funkci se čtyřmi parametry. To znamená, že každá délka stran obdélníku je předána funkci podle dvou parametrů: metrů a centimetrů.

    Float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // funkce, která vypočítá plochu obdélníku se 4 parametry a(m) a(cm); b(m) b(cm) ( návrat (a_m * 100 + a_sm) * (b_m * 100 + b_sm); )

    V těle funkce jsou hodnoty, které byly přeneseny v metrech (a_m a b_m), převedeny na centimetry a sečteny s hodnotami a_sm b_sm, načež součty vynásobíme a získáme plochu obdélník v cm. Samozřejmě bylo možné převést původní data na centimetry a použít první funkci, ale o tom teď ne.

    Nyní je nejdůležitější, že máme dvě funkce s různými podpisy, ale stejnými názvy (přetížené funkce). Podpis je kombinací názvu funkce a jejích parametrů. Jak tyto funkce volat? A volání přetížených funkcí se neliší od volání běžných funkcí, například:

    AreaRectangle(32, 43); // bude zavolána funkce, která vypočítá plochu obdélníku se dvěma parametry a(cm) a b(cm) areaRectangle(4, 43, 2, 12); // bude volána funkce, která vypočítá plochu obdélníku se 4 parametry a(m) a(cm); b(m) b(cm)

    Jak vidíte, kompilátor vybere nezávisle požadovanou funkci, analyzující pouze signatury přetížených funkcí. Obejít přetížení funkcí, jeden mohl jednoduše deklarovat funkci s jiným jménem, ​​a to by dělalo svou práci dobře. Představte si ale, co se stane, když takových funkcí potřebujete víc než dvě, třeba 10. A pro každou je potřeba vymyslet smysluplný název a nejtěžší je si je zapamatovat. To je přesně důvod, proč je jednodušší a lepší přetěžovat funkce, pokud to samozřejmě není potřeba. Zdroj program je uveden níže.

    #include "stdafx.h" #include << "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    // kód Kód::Blocks

    // Kód Dev-C++

    #zahrnout pomocí jmenného prostoru std; // prototypy přetížených funkcí float areaRectangle(float a, float b); float areaRectangle(float a_m, float a_sm, float b_m, float b_sm); int main() ( cout<< "S1 = " << areaRectangle(32,43) << endl; // вызов перегруженной функции 1 cout << "S2 = " << areaRectangle(4, 43, 2, 12) << endl; // вызов перегруженной функции 2 return 0; } // перегруженная функция 1 float areaRectangle(float a, float b) //функция, вычисляющая площадь прямоугольника с двумя параметрами a(см) и b(см) { return a * b; // умножаем длинны сторон прямоугольника и возвращаем полученное произведение } // перегруженная функция 2 float areaRectangle(float a_m, float a_sm, float b_m, float b_sm) // функция, вычисляющая площадь прямоугольника с 4-мя параметрами a(м) a(см); b(м) b(cм) { return (a_m * 100 + a_sm) * (b_m * 100 + b_sm); }

    Výsledek programu je na obrázku 1.

    Přetížení funkcí

    Přetěžující operace (operátoři, funkce, procedury)- v programování - jeden ze způsobů implementace polymorfismu, který spočívá v možnosti současné existence několika různých variant operace (operátor, funkce nebo procedura) v jednom rozsahu, které mají stejný název, ale liší se v typech parametrů na které jsou aplikovány.

    Terminologie

    Termín „overloading“ je překladem anglického „overloading“, který se objevil v ruských překladech knih o programovacích jazycích v první polovině 90. Možná to není nejlepší možnost překladu, protože slovo „přetížení“ v ruštině má zavedený význam, který se radikálně liší od nově navrhovaného, ​​přesto se zakořenilo a je poměrně rozšířené. V publikacích ze sovětské éry byly podobné mechanismy v ruštině nazývány „redefinice“ nebo „redefinice“ operací, ale tato možnost není kontroverzní: v překladech anglických „override“, „overload“ a „redefine“ vznikají nesrovnalosti a zmatek. .

    Důvody vzhledu

    Většina raných programovacích jazyků měla omezení, že v programu nemohla být současně dostupná více než jedna operace se stejným názvem. V souladu s tím musí mít všechny funkce a procedury viditelné v daném bodě programu různé názvy. Názvy a označení funkcí, procedur a operátorů, které jsou součástí programovacího jazyka, nemůže programátor použít k pojmenování vlastních funkcí, procedur a operátorů. V některých případech může programátor vytvořit svůj vlastní programový objekt s názvem jiného, ​​který již existuje, ale pak nově vytvořený objekt „překrývá“ předchozí a není možné použít obě možnosti současně.

    Tato situace je v některých poměrně běžných případech nepohodlná.

    • Někdy je potřeba popsat a aplikovat operace na datové typy vytvořené programátorem, které jsou svým významem ekvivalentní těm, které jsou již v daném jazyce k dispozici. Klasickým příkladem je knihovna pro práci s komplexními čísly. Stejně jako běžné číselné typy podporují aritmetické operace a bylo by přirozené vytvořit pro tento typ operace „plus“, „mínus“, „násobit“, „dělit“ a označovat je stejnými operátorovými znaménky jako pro ostatní číselné typy. ty typy. Zákaz používání prvků definovaných v jazyce nutí k vytvoření mnoha funkcí s názvy jako ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat a tak dále.
    • Když jsou operace stejného významu aplikovány na operandy různých typů, jsou nuceny být pojmenovány odlišně. Nemožnost používat funkce se stejným názvem pro různé typy vede k nutnosti vymýšlet různé názvy pro stejnou věc, což vytváří zmatek a může dokonce vést k chybám. Například v klasickém jazyce C existují dvě verze standardní knihovní funkce pro zjištění modulu čísla: abs() a fabs() - první je určena pro celočíselný argument, druhá pro skutečný argument. Tato situace v kombinaci se slabým ovládáním typu C může vést k těžko odhalitelné chybě: pokud programátor zapíše abs(x) do výpočtu, kde x je skutečná proměnná, některé kompilátory tiše vygenerují kód, který převede x na celé číslo vyřazením zlomkových částí a vypočítat modul výsledného celého čísla!

    Problém je částečně vyřešen pomocí objektového programování - když jsou nové datové typy deklarovány jako třídy, operace s nimi mohou být navrženy jako metody tříd, včetně těch se stejným názvem (protože metody různých tříd nemusí mít různá jména) , ale za prvé, návrh podobného Způsob operací s hodnotami různých typů je nepohodlný a za druhé to neřeší problém vytváření nových operátorů.

    Samotné přetěžování operátorů je jen syntaktický cukr, i když i jako takové může být užitečné, protože umožňuje vývojáři programovat přirozenějším způsobem a vlastní typy se chovají spíše jako vestavěné typy. Přistoupíme-li k problematice z obecnější pozice, vidíme, že nástroje, které nám umožňují jazyk rozšiřovat, doplňovat o nové operace a syntaktické konstrukce (a přetěžování operací je jedním z těchto nástrojů, spolu s objekty, makropříkazy, atd.). funkcionály a uzávěry) ji již přeměňují na metajazyk - prostředek k popisu jazyků orientovaných na konkrétní úkoly. S jeho pomocí si můžete pro každý konkrétní problém sestavit jazykovou nadstavbu, která je pro něj nejvhodnější, což vám umožní popsat jeho řešení co nejpřirozenější, nejsrozumitelnější a nejjednodušší formou. Například v aplikaci na přetěžování operací: vytvoření knihovny složitých matematických typů (vektory, matice) a popis operací s nimi v přirozené, „matematické“ podobě, vytváří „jazyk pro vektorové operace“, ve kterém se složitost výpočtů je skrytá a je možné popsat řešení problémů pomocí vektorových a maticových operací se zaměřením na podstatu problému spíše než na techniku. Z těchto důvodů byly takové nástroje najednou zahrnuty do jazyka ALGOL-68.

    Mechanismus přetížení

    Implementace

    Přetěžování operací zahrnuje zavedení dvou vzájemně souvisejících funkcí do jazyka: schopnost deklarovat několik procedur nebo funkcí se stejnými názvy v jednom rozsahu a schopnost popsat vlastní implementace operací (tj. operační znaky, obvykle zapsané v infixové notaci). , mezi operandy). V zásadě je jejich implementace poměrně jednoduchá:

    • K umožnění existence více operací stejného jména postačí zavést do jazyka pravidlo, podle kterého operace (procedura, funkce nebo operátor) rozpozná překladač nejen podle názvu (označení), ale také podle typů jeho parametrů. Takže abs(i), kde i je deklarováno jako celé číslo, a abs(x), kde x je deklarováno jako reálné, jsou dvě různé operace. V zásadě neexistují žádné potíže s poskytnutím právě takového výkladu.
    • Aby bylo možné definovat a redefinovat operace, je nutné zavést do jazyka vhodné syntaktické struktury. Možností může být poměrně hodně, ale v podstatě se od sebe neliší, jen si pamatujte, že záznam vypadá jako „<операнд1> <знакОперации> <операнд2>"je v zásadě podobné volání funkce"<знакОперации>(<операнд1>,<операнд2>)". Stačí umožnit programátorovi popsat chování operátorů formou funkcí – a problém popisu je vyřešen.

    Možnosti a problémy

    Přetěžování postupů a funkcí na úrovni obecné myšlenky zpravidla není obtížné ani implementovat, ani pochopit. I zde však existují určitá úskalí, která je třeba vzít v úvahu. Umožnění přetížení operátorů vytváří mnohem více problémů jak pro implementátor jazyka, tak pro programátora pracujícího v tomto jazyce.

    Problém s identifikací

    První otázka, před kterou stojí vývojář jazykového překladače, který umožňuje přetěžování procedur a funkcí, zní: jak vybrat ze stejnojmenných procedur tu, která by se měla v tomto konkrétním případě použít? Vše je v pořádku, pokud existuje varianta procedury, jejíž formální typy parametrů přesně odpovídají skutečným typům parametrů použitým v tomto volání. Téměř ve všech jazycích však existuje určitá míra svobody v používání typů, což naznačuje, že kompilátor v určitých situacích automaticky provede typově bezpečné převody. Například v aritmetických operacích s reálnými a celočíselnými argumenty je celé číslo obvykle přetypováno na reálný typ automaticky a výsledek je reálný. Předpokládejme, že existují dvě varianty funkce add:

    Int add(int a1, int a2); float add(float a1, float a2);

    Jak by měl kompilátor zacházet s výrazem y = add(x, i), kde x je float a i je int? Očividně neexistuje přesná shoda. Existují dvě možnosti: buď y=add_int((int)x,i) nebo y=add_flt(x, (float)i) (zde názvy add_int a add_float označují první a druhou možnost funkce).

    Nabízí se otázka: má překladatel umožnit takovéto použití přetížených funkcí, a pokud ano, tak na základě čeho zvolí konkrétní použitou možnost? Zejména ve výše uvedeném příkladu by měl překladatel při výběru zohlednit typ proměnné y? Je třeba poznamenat, že výše uvedená situace je nejjednodušší, jsou možné mnohem komplikovanější případy, které jsou zhoršeny tím, že podle pravidel jazyka lze převádět nejen vestavěné typy, ale také třídy deklarované programátorem , pokud mají související vztahy, lze přehazovat z jednoho do druhého. Existují dvě řešení tohoto problému:

    • Úplně zakázat nepřesnou identifikaci. Požadujte, aby pro každý daný pár typů existovala přesně odpovídající varianta přetížené procedury nebo operace. Pokud taková možnost neexistuje, měl by překladač vyvolat chybu. Programátor v tomto případě musí použít explicitní převod, aby převedl skutečné parametry na požadovanou sadu typů. Tento přístup je nepohodlný v jazycích jako C++, které umožňují značnou volnost v zacházení s typy, protože vede k výraznému rozdílu v chování vestavěných a přetížených operátorů (aritmetické operace lze bez přemýšlení aplikovat na běžná čísla, ale jiné typy - pouze s explicitní konverzí) nebo ke vzniku velkého množství provozních možností.
    • Stanovte si určitá pravidla pro výběr „nejbližší vhodné možnosti“. V této volbě kompilátor obvykle vybere ty možnosti, jejichž volání lze získat ze zdroje pouze bezpečnými konverzemi typu (nevedoucími ke ztrátě informací), a pokud jich je několik, může vybrat podle toho, která možnost vyžaduje méně takových konverze. Pokud to ponechává několik možností, kompilátor vyvolá chybu a vyžaduje, aby programátor explicitně specifikoval možnost.

    Specifická problematika přetěžovacích operací

    Na rozdíl od procedur a funkcí mají infixové operace programovacích jazyků dvě další vlastnosti, které významně ovlivňují jejich funkčnost: prioritu a asociativitu, jejíž přítomnost je dána možností „řetězového“ záznamu operátorů (jak rozumět a+b* c: jako (a+b)*c nebo jako a+(b*c)? Je výraz a-b+c (a-b)+c nebo a-(b+c)?).

    Operace zabudované do jazyka mají vždy předem nastavené tradiční priority a asociativitu. Nabízí se otázka: jaké priority a asociativitu budou mít předefinované verze těchto operací nebo zejména nové operace vytvořené programátorem? Existují další jemnosti, které mohou vyžadovat objasnění. Například v C existují dvě formy inkrementačních a dekrementačních operátorů ++ a -- - prefix a postfix, jejichž chování je odlišné. Jak by se měly chovat přetížené verze takových operací?

    Různé jazyky řeší tyto problémy různě. V C++ je tedy priorita a asociativita přetížených verzí operací zachována stejně jako ty, které jsou definovány v jazyce; Je možné samostatně přetížit předpony a postfixové formy inkrementačních a dekrementačních operátorů pomocí speciálních signatur:

    Int se tedy používá k odlišení podpisů

    Oznámení o nových provozech

    S vyhlášením nových provozů je situace ještě složitější. Zahrnout do jazyka samotnou možnost takové deklarace není těžké, ale její implementace je plná značných potíží. Deklarace nové operace je ve skutečnosti vytvořením nového klíčového slova programovacího jazyka, což je komplikované tím, že operace v textu mohou být obvykle následovány jinými tokeny bez oddělovačů. Když se objeví, vyvstanou další potíže s organizací lexikálního analyzátoru. Pokud například jazyk již má operace „+“ a unární „-“ (změna znaménka), pak výraz a+-b lze přesně interpretovat jako a + (-b), ale pokud nová operace +- je deklarován v programu, okamžitě vzniká nejednoznačnost, protože stejný výraz již lze analyzovat jako a (+-) b . Vývojář a implementátor jazyka musí takové problémy nějak vyřešit. Možnosti mohou být opět různé: požadovat, aby všechny nové operace byly jednoznakové, předpokládejte, že v případě jakýchkoliv nesrovnalostí bude vybrána „nejdelší“ verze operace (tj. pokud se přečte další sada znaků tím, že se překladač shoduje s nějakou operací, čte se dál), snažte se odhalit kolize při překladu a házet chyby v kontroverzních případech... Tak či onak jazyky, které umožňují deklaraci nových operací, tyto problémy řeší.

    Nemělo by se zapomínat, že u nových operací je zde také otázka určování asociativnosti a priority. Žádné hotové řešení v podobě standardní jazykové operace neexistuje a většinou stačí tyto parametry nastavit pomocí jazykových pravidel. Například udělejte ze všech nových operací levě asociativní a přidělte jim stejnou pevnou prioritu nebo zaveďte do jazyka nástroje pro specifikaci obou.

    Přetížení a polymorfní proměnné

    Když se přetížené operátory, funkce a procedury používají v silně typizovaných jazycích, kde má každá proměnná předdefinovaný typ, překladatel řeší úlohu výběru verze přetíženého operátoru, která se má v každém konkrétním případě použít, bez ohledu na jeho složitost. . To znamená, že u kompilovaných jazyků nevede použití přetěžování operátorů ke snížení výkonu – v každém případě je v objektovém kódu programu dobře definovaná operace nebo volání funkce. Jiná situace je, když je možné v jazyce použít polymorfní proměnné, tedy proměnné, které mohou v různých časech obsahovat hodnoty různých typů.

    Protože typ hodnoty, na kterou bude přetížená operace aplikována, není v době překladu kódu znám, kompilátor nemůže předem vybrat požadovanou možnost. V tomto případě je nucen zabudovat do kódu objektu fragment, který bezprostředně před provedením této operace určí typy hodnot v argumentech a dynamicky vybere možnost odpovídající této sadě typů. Navíc takové určení musí být provedeno pokaždé, když je operace provedena, protože i tentýž kód, když je volán podruhé, může být dobře proveden odlišně.

    Použití přetěžování operátorů v kombinaci s polymorfními proměnnými tedy činí dynamické určení kódu, který má být nazýván, nevyhnutelný.

    Kritika

    Použití přetížení není všemi specialisty považováno za dobré. Pokud přetěžování funkcí a procedur obecně nenachází závažné námitky (zčásti proto, že nevede k některým typickým „provozovatelským“ problémům, zčásti proto, že je menší pokušení používat je pro jiné účely), pak přetěžování operací, např. v principu, a ve specifických jazykových implementacích, je podřízen poměrně tvrdé kritice ze strany mnoha teoretiků a programátorů.

    Kritici poukazují na to, že výše uvedené problémy s identifikací, prioritou a asociativitou často činí práci s přetíženými operacemi nepřiměřeně obtížnou nebo nepřirozenou:

    • Identifikace. Pokud jazyk přijme přísná identifikační pravidla, pak je programátor nucen si pamatovat, pro které kombinace typů existují přetížené operace, a ručně do nich přetypovat operandy. Pokud jazyk umožňuje „přibližnou“ identifikaci, nikdy nemůžete zaručit, že v nějaké poměrně složité situaci bude provedena přesně ta verze operace, kterou měl programátor na mysli.
    • Priorita a asociativita. Pokud jsou striktně definovány, může to být nepohodlné a nemusí to odpovídat oblasti (např. u operací s množinami se priority liší od aritmetických). Pokud je může určit programátor, stává se to dalším zdrojem chyb (už jen proto, že různé verze stejné operace mají různé priority a dokonce i asociativitu).

    Nakolik může pohodlnost používání vlastních operací převážit nepohodlí se zhoršující se ovladatelností programu, to je otázka, která nemá jednoznačnou odpověď.

    Z hlediska jazykové implementace vedou stejné problémy ke komplikování překladatelů a snižování jejich efektivity a spolehlivosti. A použití přetížení ve spojení s polymorfními proměnnými je také samozřejmě pomalejší než volání operace napevno zakódované během kompilace a poskytuje méně příležitostí pro optimalizaci objektového kódu. Specifické rysy implementace přetížení v různých jazycích jsou předmětem samostatné kritiky. V C++ tak může být předmětem kritiky nedostatečná shoda ohledně vnitřní reprezentace názvů přetížených funkcí, což vede k nekompatibilitě na úrovni knihoven kompilovaných různými kompilátory C++.

    Někteří kritici argumentují proti přetěžování operací, založených na obecných principech teorie vývoje softwaru a aktuální průmyslové praxe.

    • Zastánci „puritánského“ přístupu ke konstrukci jazyka, jako je Wirth nebo Hoare, se staví proti přetížení operátorů jednoduše proto, že se od něj lze snadno obejít. Podle jejich názoru takové nástroje pouze komplikují jazyk a překladatele, aniž by poskytovaly další možnosti odpovídající této komplikaci. Podle jejich názoru samotná myšlenka vytvoření rozšíření jazyka zaměřeného na úkoly vypadá pouze atraktivně. Ve skutečnosti použití nástrojů pro rozšíření jazyka činí program srozumitelným pouze jeho autorovi - tomu, kdo toto rozšíření vyvinul. Program se stává mnohem obtížnějším pro ostatní programátory pochopit a analyzovat, což ztěžuje údržbu, úpravy a skupinový vývoj.
    • Je třeba poznamenat, že samotná možnost použití přetížení často hraje provokativní roli: programátoři jej začnou používat všude, kde je to možné, v důsledku toho se nástroj určený ke zjednodušení a zefektivnění programu stává důvodem jeho komplikací a zmatků.
    • Přetížené operace nemusí dělat přesně to, co se od nich na základě jejich typu očekává. Například a + b obvykle (ale ne vždy) znamená totéž jako b + a, ale „jedna“ + „dva“ se liší od „dva“ + „jedna“ v jazycích, kde je operátor + přetížen řetězcem. zřetězení.
    • Přetížení operátorů činí fragmenty programu citlivějšími na kontext. Bez znalosti typů operandů zahrnutých ve výrazu je nemožné pochopit, co výraz dělá, pokud používá přetížené operátory. Například v programu C++ operátor<< может означать и побитовый сдвиг, и вывод в поток. Выражение a << 1 возвращает результат побитового сдвига значения a на один бит влево, если a - целая переменная, но если a является выходным потоком , то же выражение выведет в этот поток строку «1» .

    Klasifikace

    Níže je uvedena klasifikace některých programovacích jazyků podle toho, zda umožňují přetěžování operátorů a zda jsou operátoři omezeni na předem definovanou sadu:

    Operace Žádné přetížení Dochází k přetížení
    Omezený soubor operací
    • Cíl-C
    • Krajta
    Lze definovat nové operace
    • PostgreSQL
    • viz také

      Nadace Wikimedia. 2010.

      Podívejte se, co je „Přetížení funkcí“ v jiných slovnících:

        - (operátory, funkce, procedury) v programování, jeden ze způsobů implementace polymorfismu, který spočívá v možnosti současné existence několika různých verzí operace v jednom rozsahu (operátor, funkce nebo ... ... Wikipedia



    Jak dosáhnout přetížení funkcí v C? (10)

    Existuje způsob, jak dosáhnout přetížení funkcí v C? Dívám se na jednoduché funkce, které mohou být přetíženy jako

    Foo (int a) foo (char b) foo (float c , int d)

    Myslím, že neexistuje žádná přímá cesta; Hledám řešení, pokud existují.

    Doufám, že níže uvedený kód vám pomůže pochopit přetížení funkcí

    #zahrnout #zahrnout int fun(int a, ...); int main(int argc, char *argv)( fun(1,10); fun(2,"cquestionbank"); return 0; ) int fun(int a, ...)( va_list vl; va_start(vl,a ); if(a==1) printf("%d",va_arg(vl,int)); else printf("\n%s",va_arg(vl,char *)); )

    Co tím myslíš - ne, nemůžeš.

    Funkci va_arg můžete deklarovat jako

    void my_func(formát znak*, ...);

    Ale budete muset předat nějaké informace o počtu proměnných a jejich typech v prvním argumentu - například printf() .

    Ano, tak nějak.

    Zde uvedete příklad:

    Void printA(int a)( printf("Ahoj světe z printA: %d\n",a); ) void printB(const char *buff)( printf("Ahoj světe z printB: %s\n",buff) ; ) #define Max_ITEMS() 6, 5, 4, 3, 2, 1, 0 #define __VA_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N #define _Num_ARGS_(... ) __VA_ARG_N(__VA_ARGS__) #define NUM_ARGS(...) (_Num_ARGS_(_0, ## __VA_ARGS__, Max_ITEMS()) - 1) #define CHECK_ARGS_MAX_LIMIT(t) if(NUM_ARGS(definice)>CHEARGS_MINCK_LIMIT if(NUM_ARGS(args) #define print(x , args ...) \ CHECK_ARGS_MIN_LIMIT(1) printf("chyba");fflush(stdout); \ CHECK_ARGS_MAX_LIMIT(4) printf("chyba");fflush(stdout) ; \ (( \ if (__builtin_types_compatible_p (typeof (x), int)) \ printA(x, ##args); \ else \ printB (x,##args); \ )) int main(int argc, char* * argv) ( int a=0; print(a); print("ahoj"); return (EXIT_SUCCESS); )

    Vypíše 0 a ahoj.. z printA a printB.

    Pokud je váš kompilátor gcc a nevadí vám ruční aktualizace pokaždé, když přidáte nové přetížení, můžete udělat makro a získat výsledek, který chcete z pohledu volajícího, není to tak hezké psát... ale je to možné

    podívejte se na __builtin_types_compatible_p a poté jej použijte k definování makra, které dělá něco podobného

    #define foo(a) \ ((__builtin_types_compatible_p(int, a)?foo(a):(__builtin_types_compatible_p(float, a)?foo(a):)

    ale jo, je to hnusné, prostě ne

    UPRAVIT: C1X získá podporu pro typové výrazy, které vypadají takto:

    #define cbrt(X) _Generic((X), long double: cbrtl, \ default: cbrt, \ float: cbrtf)(X)

    Jak již bylo řečeno, přetěžování ve smyslu, který máte na mysli, nepodporuje C. Běžným idiomem k vyřešení problému je mít funkci označenou konjunkci. To je implementováno pomocí parametru struct, kde samotná struktura se skládá z nějakého typu indikátoru typu, jako je enum , a spojení různých typů hodnot. Příklad:

    #zahrnout typedef enum ( T_INT, T_FLOAT, T_CHAR, ) můj_typ; typedef struct ( my_type type; union ( int a; float b; char c; ) my_union; ) my_struct; void set_overload (my_struct *cokoliv) ( switch (cokoliv->typ) ( case T_INT: cokoliv->my_union.a = 1; break; case T_FLOAT: what->my_union.b = 2.0; break; case T_CHAR: what-> my_union.c = "3"; ) ) void printf_overload (my_struct *cokoliv) ( switch (cokoliv->typ) ( case T_INT: printf("%d\n", cokoliv->my_union.a); break; case T_FLOAT : printf("%f\n", cokoliv->my_union.b); break; case T_CHAR: printf("%c\n", cokoliv->my_union.c); break; ) ) int main (int argc, char* argv) ( my_struct s; s.type=T_INT; set_overload(&s); printf_overload(&s); s.type=T_FLOAT; set_overload(&s); printf_overload(&s); s.type=T_CHAR; set_overload(&s) ; printf_overload(&s); )

    Nemůžete prostě používat C++ a nepoužívat všechny ostatní funkce C++ kromě toho?

    Pokud zde ještě nebylo striktní C, doporučil bych místo toho varadiické funkce.

    Následující přístup je podobný a2800276, ale s některými makry C99:

    // potřebujeme `size_t` #include // typy argumentů pro akceptování výčtu sum_arg_types ( SUM_LONG, SUM_ULONG, SUM_DOUBLE ); // struktura pro uložení argumentu struct sum_arg ( typ enum sum_arg_types; union ( long as_long; unsigned long as_ulong; double as_double; ) value; ); // určení velikosti pole #define count(ARRAY) ((sizeof (ARRAY))/(sizeof *(ARRAY))) // takto se bude naše funkce jmenovat #define sum(...) _sum( count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__)) // vytvoření pole `struct sum_arg` #define sum_args(...) ((struct sum_arg )( __VA_ARGS__ )) // vytvoření inicializátorů pro argumenty #define sum_long (VALUE) ( SUM_LONG, ( .as_long = (VALUE) ) ) #define sum_ulong(VALUE) ( SUM_ULONG, ( .as_ulong = (VALUE) ) ) #define sum_double(VALUE) ( SUM_DOUBLE, ( .as_double = (VALUE) ) ) // naše polymorfní funkce long double _sum(size_t count, struct sum_arg * args) ( long double value = 0; for(size_t i = 0; i< count; ++i) { switch(args[i].type) { case SUM_LONG: value += args[i].value.as_long; break; case SUM_ULONG: value += args[i].value.as_ulong; break; case SUM_DOUBLE: value += args[i].value.as_double; break; } } return value; } // let"s see if it works #include int main() ( unsigned long foo = -1; long double value = sum(sum_long(42), sum_ulong(foo), sum_double(1e10)); printf("%Le\n", hodnota); return 0; )

    V současné době je _Generic _Generic tato otázka, standardní C (bez přípon) účinná přijaté podpora přetěžování funkcí (spíše než operátorů) díky přidání slova _Generic v C11. (podporováno v GCC od verze 4.9)

    (Přetížení není skutečně „zabudováno“ způsobem uvedeným v otázce, ale je snadné zničit něco, co takto funguje.)

    Generic je operátor v době kompilace ve stejné rodině jako sizeof a _Alignof. Je to popsáno ve standardní části 6.5.1.1. Vyžaduje dva hlavní parametry: výraz (který nebude vyhodnocen za běhu) a seznam přidružení typu/výrazu, což je trochu jako blok přepínače. _Generic získá obecný typ výrazu a poté na něj „přepne“, aby pro svůj typ vybral konečný výsledný výraz ze seznamu:

    Generic(1, float: 2.0, char *: "2", int: 2, default: get_two_object());

    Výše uvedený výraz se vyhodnotí jako výraz typu se 2 ovládacími prvky - int , takže jako hodnotu vybere výraz spojený s výrazem int. Nic z toho není během provádění zachováno. (Výchozí klauzule je povinná: pokud ji nezadáte a typ se neshoduje, způsobí to chybu kompilace.)

    Způsob, který je užitečný pro přetížení funkcí, je ten, že jej lze vložit preprocesorem C a vybrat výsledný výraz na základě typu argumentů předávaných do řídicího makra. Takže (příklad ze standardu C):

    #define cbrt(X) _Generic((X), \ long double: cbrtl, \ default: cbrt, \ float: cbrtf \)(X)

    Toto makro implementuje přetíženou operaci cbrt předáním typu argumentu makru, výběrem vhodné implementační funkce a předáním původního makra této funkci.

    Pro implementaci vašeho původního příkladu bychom mohli udělat toto:

    Foo_int (int a) foo_char (char b) foo_float_int (float c , int d) #define foo(_1, ...) _Generic((_1), \ int: foo_int, \ char: foo_char, \ float: _Generic(( FIRST(__VA_ARGS__,)), \ int: foo_float_int))(_1, __VA_ARGS__) #define FIRST(A, ...) A

    V tomto případě bychom mohli použít výchozí: asociace pro třetí případ, ale to neukazuje, jak rozšířit princip na více argumentů. Konečným výsledkem je, že můžete použít foo(...) ve svém kódu, aniž byste se (moc) starali o typ svých argumentů.

    Ve složitějších situacích, jako jsou funkce, které přetěžují více argumentů nebo měnící se čísla, můžete použít makra obslužného programu k automatickému vytvoření statických struktur odeslání:

    Void print_ii(int a, int b) ( printf("int, int\n"); ) void print_di(double a, int b) ( printf("double, int\n"); ) void print_iii(int a, int b, int c) ( printf("int, int, int\n"); ) void print_default(void) ( printf("neznámé argumenty\n"); ) #define print(...) PŘETÍŽIT(tisk, (__VA_ARGS__), \ (print_ii, (int, int)), \ (print_di, (double, int)), \ (print_iii, (int, int, int)) \) #define OVERLOAD_ARG_TYPES (int, double) #define OVERLOAD_FUNCTIONS (print) #include "activate-overloads.h" int main(void) ( print(44, 47); // vytiskne "int, int" print(4.4, 47); // vytiskne "double, int" print (1, 2, 3); // vypíše "int, int, int" print(""); // vypíše "neznámé argumenty" )

    (realizace zde). S určitým úsilím můžete snížit počet šablon tak, aby vypadaly docela podobně jako jazyk s vestavěnou podporou přetížení.

    Do strany už bylo možné přetížit Množství argumenty (nikoli typ) v C99.

    Vezměte prosím na vědomí, že způsob hodnocení C vás může ovlivnit. Toto vybere foo_int, pokud se mu například pokusíte předat doslovný znak, a potřebujete nějaký foo_int, pokud chcete, aby vaše přetížení podporovala řetězcové literály. Celkově je to však docela fajn.

    Leushenkoova odpověď je opravdu skvělá: pouze příklad foo se nezkompiluje s GCC, což selže s foo(7) , zasáhne PRVNÍ makro a skutečné volání funkce ((_1, __VA_ARGS__) , takže s čárkou navíc. 'narazí na problémy, pokud chceme poskytnout další přetížení, jako je foo(double) .

    Rozhodl jsem se tedy na tuto otázku odpovědět podrobněji, včetně povolení prázdného přetížení (foo(void) - což způsobilo určité potíže...).

    Myšlenka je nyní tato: definovat více než jeden generický v různých makrech a nechat vybrat ten správný podle počtu argumentů!

    Počet argumentů je poměrně jednoduchý, na základě této odpovědi:

    #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) #define CONCAT(X, Y) CONCAT_(X, Y) # definovat CONCAT_(X, Y) X ## Y

    To je v pořádku, rozhodujeme se buď o SELECT_1 nebo SELECT_2 (nebo více argumentech, pokud je chcete/potřebujete), takže potřebujeme pouze příslušné definice:

    #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char: foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double : _Generic((_2), \ int: foo_double_int \) \)

    Za prvé, prázdné volání makra (foo()) stále vytváří token, ale prázdný. Makro počítání tedy ve skutečnosti vrací 1 místo 0, i když je makro nazýváno prázdné. Tento problém můžeme „snadno“ vyřešit, když za __VA_ARGS__ vložíme čárku podmíněně, v závislosti na tom, zda je seznam prázdný nebo ne:

    #define NARG(...) ARG4_(__VA_ARGS__ COMMA(__VA_ARGS__) 4, 3, 2, 1, 0)

    Tento podíval se snadné, ale makro COMMA je poměrně těžké; Naštěstí se toto téma již na blogu Jense Gustedta zabývá (díky Jensi). Hlavní trik je v tom, že makra funkcí se nerozšiřují, pokud nejsou dodrženy závorky, další vysvětlení viz Jensův blog... Jen je potřeba makra trochu upravit pro naše potřeby (pro stručnost použiji kratší názvy a méně argumentů ).

    #define ARGN(...) ARGN_(__VA_ARGS__) #define ARGN_(_0, _1, _2, _3, N, ...) N #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0 ) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA(SET_COMMA __VA_ARGS__), \ HAS_COMMA_SET_ARGS_COMMA \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3 definovat COMMA_0000 , #define COMMA_0001 #define COMMA_0010 , // ... (všechny ostatní s čárkou) #define COMMA_1111 ,

    A teď jsme v pohodě...

    Vyplňte kód v jednom bloku:

    /* * demo.c * * Vytvořeno: 2017-09-14 * Autor: sboehler */ #include void foo_void(void) ( puts("void"); ) void foo_int(int c) ( printf("int: %d\n", c); ) void foo_char(char c) ( printf("char: %c \n", c); ) void foo_double(double c) ( printf("double: %.2f\n", c); ) void foo_double_int(double c, int d) ( printf("double: %.2f, int: %d\n", c, d); ) #define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__) #define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__) # definovat CONCAT(X, Y) CONCAT_(X, Y) #define CONCAT_(X, Y) X ## Y #define SELECT_0() foo_void #define SELECT_1(_1) _Generic ((_1), \ int: foo_int, \ char : foo_char, \ double: foo_double \) #define SELECT_2(_1, _2) _Generic((_1), \ double: _Generic((_2), \ int: foo_double_int \) \) #define ARGN(...) ARGN_( __VA_ARGS__) #define ARGN_(_0, _1, _2, N, ...) N #define NARG(...) ARGN(__VA_ARGS__ COMMA(__VA_ARGS__) 3, 2, 1, 0) #define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0) #define SET_COMMA(...) , #define COMMA(...) SELECT_COMMA \ (\ HAS_COMMA(__VA_ARGS__), \ HAS_COMMA(__VA_ARGS__ ()), \ HAS_COMMA__VA_ARGS_ , \ HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \) #define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3) #define SELECT_COMMA_(_0, _1, _2, _0 #3) # _1 ## _2 ## _3 #define COMMA_0000 , #define COMMA_0001 #define COMMA_0010 , #define COMMA_0011 , #define COMMA_0100 , #define COMMA_0101 , #0_define 110 COMMA,1 000 , #define COMMA_1001 , #define COMMA_1010 , #define COMMA_1011 , #define COMMA_1100 , #define COMMA_1101 , #define COMMA_1110 , #define COMMA_1111 , int main(int argc, char** argv) ( foo(); foo(7); foo(10,12); foo(12,10, 7); foo((char)"s"); návrat 0; )

    Jazyk C++ implementuje možnost používat jeden identifikátor pro funkce, které provádějí různé akce na různých typech dat, v důsledku čehož je možné použít několik funkcí se stejným názvem, ale s různými seznamy parametrů, jak v počtu, tak v typu. .

    Takové funkce se nazývají přetížené a samotný mechanismus – přetíženífunkcí.

    Kompilátor určuje, která funkce se stejným názvem má být volána, porovnáním typů skutečných argumentů s typy formálních parametrů v záhlaví všech těchto funkcí, tzn. Kompilátor v závislosti na typu a počtu argumentů vytvoří potřebné volání odpovídající funkce.

    Nalezení funkce, kterou chcete volat, trvá tři samostatné kroky:

    1. Vyhledejte funkci s přesnou shodou parametrů a použijte ji, pokud je nalezena.

    2. Najděte odpovídající funkci pomocí vestavěných převodů datových typů.

    3. Najděte odpovídající funkci pomocí uživatelsky definovaných transformací.

    Příklad přetížení funkce

    Uveďme příklad funkce S 1 se dvěma parametry X,na, který funguje v závislosti na typu předávaných argumentů takto:

    – pokud je typ parametru celé číslo, funkce S 1 sečte jejich hodnoty a vrátí výsledný součet;

    – pokud typ parametrů dlouho, funkce S 1 vynásobí jejich hodnoty a vrátí výsledný produkt;

    – pokud je typ parametrů skutečný, funkce S 1 rozdělí jejich hodnoty a vrátí kvocient dělení.

    #zahrnout

    int S1 (int x, int y) (

    dlouhé S1 (dlouhé x, dlouhé y) (

    double S1 (dvojité x, dvojité y) (

    int a = 1, b = 2, c;

    dlouhé i = 3, j = 4, k;

    dvojité x = 10, y = 2, z;

    printf("\n c = %d; k = %ld; z = %lf . \n", c, k, z);

    V důsledku toho dostaneme:

    C = 3; k = 12; z = 5.000000 .

    Funkce s proměnným počtem parametrů

    Pokud není počet argumentů předem znám, použije se v seznamu parametrů uživatelsky definované funkce tři tečky. V tomto případě lze v jeho prototypu zadat neurčitý počet parametrů takto:

    neplatná f1 (int a , dvojité b , …);

    Tento zápis indikuje kompilátoru, že za požadovanými skutečnými argumenty pro parametry A A b Při volání této funkce mohou nebo nemusí následovat další argumenty.

    Uveďme hlavní rysy použití tohoto mechanismu.

    1. Pro přístup k parametrům takových funkcí se používá několik makro příkazů, jsou to:

    va _ seznam A va _ Start – makro příkazy pro přípravu přístupu k parametrům;

    va _ arg – použití parametrů;

    va _ konec – úklid před odjezdem.

    Jsou deklarovány v záhlaví souboru stdarg . h .

    2. Taková funkce musí mít alespoň jeden parametr (pojmenovaný), aby jí byl předán počet argumentů, které mají být předány.

    3. Pro makro va_ Start musíte předat dva argumenty - název seznamu parametrů, který určuje va_ seznam a jejich počet.

    4. Zadané pořadí makro příkazů nemůžete porušit. Jinak můžete mít nepředvídatelné následky.

    5. Pro makro va_ arg Kromě názvu seznamu parametrů musíte předat očekávaný typ. Pokud se typy neshodují, dojde k chybě.

    Použití elips zcela zakáže kontrolu typu parametru. Tři tečky jsou nutné pouze v případě, že se mění počet parametrů i jejich typ.

    Následující příklad tuto možnost ilustruje.

    #zahrnout

    #zahrnout

    void f1(dvojité s, int n ...) (

    va_start(p, n);

    printf(" \n Dvojité S = %lf ", s);

    for(int i=1; i<=n; i++) {

    v = va_arg(p, int);

    printf("\n Argument %d = %d ", i, v);

    void main(void) (

    fl(1,5, 3, 4, 5, 6);

    V důsledku toho dostaneme:

    Dvojnásobek S = 1.500000

    Argument 1 = 4

    Argument 2 = 5

    Argument 3 = 6

    pokračujte stiskem libovolné klávesy