T°φda a jejφ rozhranφ

V tomto Φlßnku se zamyslφme nad tφm, co tvo°φ rozhranφ t°φdy v C++. Oklikou se pak vrßtφme k prostor∙m jmen a k d∙sledk∙m Koenigova vyhledßvßnφ.

T°φda jako datov² typ

Nejprve si zopakujeme, ₧e pod pojmem t°φda v objektov∞ orientovanΘm programovßnφ v jazycφch, jako je C++, rozumφme urΦitou mno₧inu dat (hodnot) spolu s operacemi, kterΘ lze s t∞mito daty provßd∞t. Tyto operace se zpravidla oznaΦujφ jako metody. Dopl≥me jeÜt∞, ₧e urΦφme-li n∞jakou mno₧inu hodnot, musφme takΘ urΦit zp∙sob, jak²m budou tyto hodnoty v pam∞ti poΦφtaΦe reprezentovßny.

VÜimn∞te si, ₧e tato definice se vlastn∞ kryje s b∞₧nou definicφ datovΘho typu. T°φdy v C++ tedy p°edstavujφ prost∞ jen dalÜφ datovΘ typy; jde sice o typy, kterΘ definuje sßm programßtor, ale to na v∞ci nic nem∞nφ; ostatn∞ t°φdy majφ v C++ prakticky rovnocennΘ postavenφ jako vestav∞nΘ typy.

(Jist∞ jste si uv∞domili, ₧e zde v∙bec nehovo°φme o d∞diΦnosti a o polymorfizmu. I kdy₧ jde bezpochyby o d∙le₧itΘ vlastnosti, pro naÜe ·vahy o rozhranφ t°φdy nebudou podstatnΘ.)

Co tvo°φ rozhranφ t°φdy

Pod rozhranφm rozumφme v tΘto souvislosti nßstroje, kterΘ umo₧≥ujφ t°φdu pou₧φvat û p°istupovat k dat∙m, kterß jsou ulo₧ena v jednotliv²ch instancφch nebo ve t°φd∞ jako celku (statickΘ datovΘ slo₧ky).

Co tedy tvo°φ rozhranφ t°φdy? TΘm∞° automatickß odpov∞∩ na tuto otßzku znφ, ₧e rozhranφ t°φdy tvo°φ jejφ ve°ejn∞ p°φstupnΘ metody a datovΘ slo₧ky. M²m cφlem v tomto Φlßnku je ukßzat, ₧e tato odpov∞∩ nenφ ·plnß.

Podφvejme se nejprve na schΘmatick² p°φklad:

class X

{ // N∞jakΘ datovΘ slo₧ky a metody

}

void f(X& x)

{ // à

}

Pat°φ funkce f() do rozhranφ t°φdy X?

NejspφÜ odpovφte, ₧e nikoli, nebo¥ nejde o metodu, ale o samostatnou funkci. Co kdy₧ ale deklarujeme funkci f() jako sp°ßtelenou, nap°. takto?

class X

{

// N∞jakΘ datovΘ slo₧ky a metody

áááá friend void f(X& x);

}

void f(X& x)

{ // ...

}

Funkce f() m∙₧e manipulovat se soukrom²mi daty ulo₧en²mi v instancφch t°φdy X, nebo¥ jinak bychom ji nepot°ebovali deklarovat jako sp°ßtelenou; umo₧≥uje tedy t°φdu X pou₧φvat. To ale znamenß, ₧e je rozumnΘ pova₧ovat ji za souΦßst rozhranφ tΘto t°φdy.

Poj∩me jeÜt∞ o krok dßl: Musφ takovß funkce b²t sp°ßtelenß?

Ne₧ odpovφme, podφvßme se na p°φklad; poslou₧φ nßm t°φda cplx p°edstavujφcφ komplexnφ Φφsla. Je samoz°ejmΘ, ₧e souΦßstφ jejφ implementace bude i p°etφ₧en² operßtor << pro v²stup.

Lze navrhnout nejmΘn∞ dv∞ varianty implementace tohoto operßtoru. V prvnφ z nich deklarujeme operßtor pro v²pis jako sp°ßtelenou funkci:

// Prvnφ implementace: operßtor << jako friend

#include <iostream>

using std::ostream;

using std::cout;

using std::endl;

class cplx

{

double re, im;

public:

cplx(double r = 0, double i = 0): re(r), im(i){}

double& Re(){return re;}

double& Im(){return im;}

friend ostream& operator<<(ostream& proud, cplx c)

{

return proud << "(" << c.re << ", " << c.im << ")";

}

};

Ve druhΘ implementaci se deklaraci friend vyhneme tφm, ₧e ve t°φd∞ cplx deklarujeme ve°ejn∞ p°φstupnou metodu vypis(), kterß se postarß o vÜe pot°ebnΘ, a v operßtoru <<á se na ni odvolßme.

// Druhß implementace: nepou₧φvß friend

class cplx

{

double re, im;

public:

cplx(double r = 0, double i = 0): re(r), im(i){}

double& Re(){return re;}

double& Im(){return im;}

ostream& vypis(ostream& proud) {

return proud << "(" << re << ", " << im << ")";

}

};

ostream& operator<<(ostream& proud, cplx c)

{

return c.vypis(proud);

}

Implementace operßtoru << pro v²pis komplexnφch Φφsel je sice pon∞kud jednoduÜÜφ, ne₧ by bylo pro praktickΘ pou₧itφ pot°ebnΘ, ale pro naÜe ·Φely pln∞ postaΦuje.

Rozdφl mezi t∞mito dv∞ma implementacemi t°φdy cplx je minimßlnφ; v obou p°φpadech lze oprßvn∞n∞ tvrdit, ₧e operßtor << je souΦßstφ rozhranφ t°φdy cplx. To znamenß, ₧e souΦßstφ rozhranφ t°φdy m∙₧e b²t i samostatnß funkce, kterß nenφ deklarovßna jako sp°ßtelenß, pokud mß tuto t°φdu jako parametr.

N∞kdy m∙₧eme volit

V p°φpad∞ operßtoru << jsme nem∞li jinou mo₧nost ne₧ deklarovat jej jako samostatnou funkci, nebo¥ pravidla jazyka C++ jasn∞ °φkajφ, ₧e pro jak²koli p°et∞₧ovateln² binßrnφ operßtor @ znamenß zßpis

a @ b

volßnφ bu∩ funkce operator@(a, b), nebo metody a.operator@(b). Kdybychom cht∞li definovat operßtor << jako metodu, museli bychom jej p°idat do knihovnφ t°φdy ostream û ale m∞nit standardnφ knihovny nenφ nejlepÜφ nßpad.

V n∞kter²ch p°φpadech si ale m∙₧eme vybrat. Jako p°φklad vezmeme op∞t t°φdu cplx a vybavφme ji navφc operßtorem + pro sΦφtßnφ instancφ.

Implementace operßtoru + jako metody je na prvnφ pohled logiΦt∞jÜφ, nebo¥ jde o operaci nad daty instance:

// Implementace operßtoru + jako metody

class cplx

{

double re, im;

public:

cplx(double r = 0, double i = 0): re(r), im(i){}

double& Re(){return re;}

double& Im(){return im;}

ostream& vypis(ostream& proud) {

return proud << "(" << re << ", " << im << ")";

}

cplx operator+(cplx c)

{

return cplx(re+c.re, im+c.im);

}

};

P°esto mß tato implementace nejmΘn∞ jednu zßva₧nou nev²hodu: Operandy v nφ nemajφ symetrickΘ postavenφ. O tom se snadno p°esv∞dΦφme, deklarujeme-li prom∞nnΘ

cplx c, a(1,2);

a napφÜeme-li p°φkazy

c = a+1; áááá // OK

resp.

c = 1+a; áááá // Chyba

Prvnφ p°φkaz se p°elo₧φ bez problΘm∙, nebo¥ p°ekladaΦ najde ve t°φd∞ cplx metodu operator+() a jejφ parametr konvertuje na typ cplx pomocφ konstruktoru tΘto t°φdy. OvÜem druh² p°φkaz p°ekladaΦ oznaΦφ jako chybn², nebo¥ nenajde odpovφdajφcφ operßtor û nelze nejprve implicitn∞ konvertovat Φφslo 1 typu int na typ cplx a pak volat metodu takto vytvo°enΘ instance.

To ovÜem odporuje naÜφ b∞₧nΘ zkuÜenosti: Z matematiky jsme zvyklφ, ₧e sΦφtßnφ je komutativnφ, ₧e m∙₧eme po°adφ jednotliv²ch sΦφtanc∙ libovoln∞ zam∞≥ovat.

M∙₧eme si sice pomoci explicitnφm p°etypovßnφm,

c = cplx(1)+a;

avÜak ani tento zßpis nenφ p°φliÜ intuitivnφ.

To znamenß, ₧e vhodn∞jÜφ bude pou₧φt implementaci operßtoru + jako samostatnΘ funkce. Pou₧ijeme op∞t triku zalo₧enΘho na ve°ejn∞ p°φstupnΘ metod∞, kterß se postarß o vÜe pot°ebnΘ:

// Implementace operßtoru + jako samostatnΘ funkce

class cplx

{

double re, im;

public:

cplx(double r = 0, double i = 0): re(r), im(i){}

double& Re(){return re;}

double& Im(){return im;}

ostream& vypis(ostream& proud) {

return proud << "(" << re << ", " << im << ")";

}

cplx Plus(cplx c)

{

return cplx(re+c.re, im+c.im);

}

};

cplx operator +(cplx a, cplx b)

{

return a.Plus(b);

}

Tentokrßt p°ijme p°ekladaΦ oba p°φkazy

c = a+1; áááá // stßle OK

c = 1+a; áááá // Nynφ takΘ OK

bez nßmitek.

Podobn∞ i p°i deklaraci dalÜφch operßtor∙ implementujφcφch b∞₧nΘ aritmetickΘ operace s komplexnφmi Φφsly zjistφme, ₧e je rozumnΘ deklarovat je jako samostatnΘ funkce. Ukazuje se tedy, ₧e p°φpad∙, kdy je nutnΘ nebo v²hodnΘ deklarovat n∞kterou z operacφ s daty t°φdy nebo instance jako samostatnou funkci, m∙₧e b²t vφce, ne₧ se na prvnφ pohled zdß.

TakΘ v tomto p°φpad∞ je naprosto logickΘ tvrdit, ₧e operßtor +, stejn∞ jako ostatnφ aritmetickΘ operßtory, je souΦßstφ rozhranφ t°φdy cplx, a to bez ohledu na to, zda je implementovßn jako metoda nebo jako samostatnß funkce.

Nejen parametr

Podobn²mi ·vahami dosp∞jeme k zßv∞ru, ₧e za souΦßsti rozhranφ t°φdy X m∙₧eme pova₧ovat i samostatnΘ funkce, kterΘ vracejφ instance tΘto t°φdy nebo kterΘ ji n∞jak²m jin²m zp∙sobem äzmi≥ujφô.

Princip rozhranφ

Z p°edchozφch ·vah by se mohlo zdßt, ₧e souΦßstφ rozhranφ t°φdy X je jakßkoli funkce, kterß mß parametr typu X nebo kterß t°φdu X jak²mkoli jin²m zp∙sobem äzmi≥ujeô. P°edstavme si ale, ₧e X je knihovnφ t°φda ostream. Mß smysl pova₧ovat za souΦßst jejφho rozhranφ funkci operator<<(ostream&, cplx), kterou napφÜeme ve svΘm programu pro naÜi vlastnφ t°φdu cplx?

TakovΘ pojetφ pojmu rozhranφ by nejspφÜ vedlo ke zmatk∙m, nebo¥ pak by se rozhranφ t°φdy mohlo m∞nit od programu k programu, a to je (p°inejmenÜφm v p°φpad∞ knihovnφch t°φd) ne₧ßdoucφ.

Rozumn∞jÜφ je zahrnout do rozhranφ t°φdy X pouze ty samostatnΘ funkce, kterΘ nejen pracujφ s X, ale takΘ se spolu s touto t°φdou dodßvajφ. Ostatn∞ takovΘto funkce budou spolu s nφ takΘ dokumentovßny.

Shrneme-li v²sledky p°edchozφch ·vah, dosp∞jeme k tzv. principu rozhranφ [1]:

Pro jakoukoli t°φdu X a pro jakoukoliá funkci f() platφ, ₧e pokud funkce f() pracuje s X a je spolu s nφ i dodßvßna, je souΦßstφ jejφho rozhranφ, a tedy tvo°φ jejφ logickou Φßst.

Rozhranφ t°φdy a prostory jmen

Podφvejme se nynφ, jak do p°edchozφch ·vah zapadajφ prostory jmen.

Minule jsme si °ekli, ₧e na prostory jmen se m∙₧eme dφvat jako na p°φjmenφ p°ipojovanß k identifikßtor∙m, kterß pomßhajφ zajistit v rozsßhl²ch programech jejich jednoznaΦnost. OvÜem tφm takΘ usnad≥ujφ organizaci programu: V∞ci, kterΘ tvo°φ logick² celek, uklßdßme do stejnΘho prostoru jmen. Uka₧me si, jak m∙₧eme uspo°ßdat naÜi miniknihovnu pro komplexnφ Φφsla.

T°φdu cplx a funkce, kterΘ s nφ souvisφ, deklarujeme v prostoru jmen komplex. Do hlaviΦkovΘho souboru cplx.h ulo₧φme deklaraci t°φdy a prototypu obou samostatn²ch funkcφ:

// Soubor cplx.h

#ifndef _KOMPLEX_H_

á #define _KOMPLEX_H_

#include <iostream>

namespace komplex {

using std::ostream;

class cplx

{

double re, im;

public:

cplx(double r = 0, double i = 0): re(r), im(i){}

double& Re(){return re;}

double& Im(){return im;}

ostream& Vypis(ostream& proud) {

return proud << "(" << re << ", " << im << ")";

}

cplx Plus(cplx c)

{

return cplx(re+c.re, im+c.im);

}

};

cplx operator +(cplx a, cplx b);

ostream& operator<<(ostream& proud, cplx c);

}

#endif

Do souboru cplx.cpp ulo₧φme definice obou samostatn∞ deklarovan²ch operßtor∙.

// Soubor cplx.cpp

#include "cplx.h"

namespace komplex {

cplx operator +(cplx a, cplx b)

{

return a.Plus(b);

}

ostream& operator<<(ostream& proud, cplx c)

{

return c.Vypis(proud);

}

}

Do tohoto souboru pat°φ samoz°ejm∞ i definice vÜech metod, kterΘ nejsou vlo₧enΘ (inline). NaÜe zjednoduÜenß implementace t°φdy komplex::cplx vÜak zatφm ₧ßdnΘ takovΘto metody neobsahuje.

Nynφ m∙₧eme v programu, kter² pracuje s t°φdou komplex::cplx, napsat nap°.

#include "cplx.h"

using komplex::cplx;

using std::cout;

// ...

cplx a(1,2);

cout << 1+a;

Dφky pravidlu vyhledßvßnφ funkcφ i v prostorech jmen parametr∙ (Koenigovu vyhledßvßnφ) nenφ t°eba zp°φstup≥ovat operßtory + a <<.

P°edchozφ p°φklad ukazuje, ₧e Koenigovo vyhledßvßnφ velmi dob°e dopl≥uje princip rozhranφ. Logickou souvislost t°φdy a spolu s nφ dodßvan²ch samostatn²ch funkcφ, kterΘ dopl≥ujφ jejφ rozhranφ, lze podtrhnout tφm, ₧e t°φdu i tyto funkce umφstφme do spoleΦnΘho prostoru jmen.

NejednoznaΦnost

Prostory jmen zdßnliv∞ umo₧≥ujφ klientsk²m programßtor∙m, tj. u₧ivateli t°φdy deklarovanΘ v prostoru jmen, nestarat se o samostatnΘ funkce a operßtory deklarovanΘ spolu s t°φdou. Zdßnφ ovÜem m∙₧e klamat. Podφvejme se nynφ na p°φpad, kdy programßtor pou₧φvajφcφ t°φdu X deklaruje funkci se stejn²m prototypem, jako mß jedna z funkcφ doprovßzejφcφch tuto t°φdu.

namespace Alfa

{

class X{};

void f(X x){ /*...*/ }áááá // (1)

}

void f(Alfa::X x){ /*...*/ }áááá // (2)

int main()

{

Alfa::X x;

f(x) áááá áááá // KterΘ f()?

return 0;

}

Volßnφ funkce f() ve funkci main() je nejednoznaΦnΘ, nebo¥ vyhledßvacφ mechanizmus najde krom∞ globßlnφ funkce oznaΦenΘ v komentß°i Φφslem (1) jeÜt∞ i funkci (2) v prostoru jmen Alfa.

Na prvnφ pohled se zdß, ₧e jde o nevφtan² d∙sledek Koenigova vyhledßvßnφ. Je ale opravdu tak ne₧ßdoucφ? Podφvejme se op∞t na konkrΘtnφ p°φklad: Vezm∞me naÜi oblφbenou t°φdu komplex::cplx a do role funkce f() dosa∩me operßtor <<. Dostaneme n∞co jako

#include <iostream>

using std::ostream;ááá

namespace komplex {

class cplx

{áááá // T°φda cplx stejnß jako p°edtφm

};

cplx operator +(cplx a, cplx b);

ostream& operator<<(ostream& proud, cplx c);

}

ostream& operator<<(ostream& proud, komplex::cplx c)

{

return c.vypis(proud);

}

int main()

{

using komplex::cplx;

using std::cout;

cplx a(1,2);

cout << 1+a;á áááá // Chyba û nejednoznaΦnΘ

return 0;

}

a p°ekladaΦ ohlßsφ, ₧e narazil na nejednoznaΦnost p°i vyhledßvßnφ operßtoru <<. Na tomto p°φkladu je ale naprosto z°ejmΘ, ₧e klientsk² programßtor p°ehlΘdl, ₧e s t°φdou komplex::cplx se dodßvß ji₧ hotov² operßtor <<, a pokusil se naprogramovat si ho sßm. Tφm, ₧e zde p°ekladaΦ ohlßsφ chybu, jej na toto nedopat°enφ upozornφ.

M∙₧e se samoz°ejm∞ stßt, ₧e klientskΘmu programßtorovi nevyhovuje p∙vodnφ implementace n∞kterΘ ze samostatn²ch funkcφ, dopl≥ujφcφch rozhranφ t°φdy, a ₧e si ji chce naprogramovat sßm. P∙jde-li o obyΦejnou (nikoli operßtorovou) funkci, nenφ t°eba p∙vodnφ program p°φliÜ m∞nit, staΦφ p°i volßnφ kvalifikovat jejφ identifikßtor prostorem jmen.

Uka₧me si schΘmatick² p°φklad:

namespace Alfa

{

class X{};

void f(X x){ /*...*/ }áááá // (1)

}

void f(Alfa::X x){ /*...*/ }áááá // (2)

int main()

{

ááááá Alfa::X x;

::f(x);á áááá áááá // Volß (2)

á áááá komplex::f(x);ááá áááá // Volß (1)

return 0;

}

V p°φpad∞ operßtor∙ nelze kvalifikaci pou₧φt. Pak nezb²vß, ne₧ se uch²lit k funkcionßlnφmu zßpisu operßtoru, nap°.

komplex::operator<<(cout, c);

To je sice velmi nepohodlnΘ, ale na druhΘ stran∞ nejde v ₧ßdnΘm p°φpad∞ o b∞₧nou situaci.

Na zßv∞r

P°φklady, kterΘ doprovßzely naÜe ·vahy, ukazujφ, ₧e princip rozhranφ a Koenigovo vyhledßvßnφ se vzßjemn∞ dopl≥ujφ. OvÜem jako vÜechny nßstroje v C++, i tyto dva mohou p°i nevhodnΘm pou₧itφ zp∙sobit problΘmy.

Miroslav Virius

Odkaz:
1. Herb Sutter: Exceptional C++. Addison-Wesley 2000.