Prostory jmen se objevily již v neoficiálním standardu jazyka C++ [1] a byly poměrně jednoduché. Přesto je komerční překladače tohoto jazyka implementovaly poměrně pozdě, až v polovině devadesátých let minulého století. Příčinou byly zřejmě mimo jiné nejasnosti kolem jedné zdánlivé drobnosti, kterou standard přidal – vyhledávání jmen volaných funkcí v závislosti na parametrech (tzv. Koenigova vyhledávání).
Podívejme se tedy, jak to s prostory jmen v C++ je.
K čemu jsou prostory
jmen dobré? Začneme zeširoka, od počítačové prehistorie.
Identifikátory prvních
verzích jazyka FORTRAN – tehdy se psal se všemi písmeny velkými – mohly mít
nejvýše 6 znaků; k dispozici bylo 26 písmen anglické abecedy a číslice 0–9.
(Malá a velká písmena se nerozlišovala, většina počítačů znala pouze velká
písmena). Identifikátor musel začínat, jak je dodnes obvyklé, písmenem. Teoreticky
měl tedy programátor dispozici 26
jednoznakových identifikátorů, 26 × 36 dvouznakových, … a 26 × 365 šestiznakových
identifikátorů. To více než 16 miliard různých jmen, a to se autorům FORTRANu
zdálo dostatečné.
Nicméně brzy se ukázalo, že identifikátory jako X235B1 nevedou k nejpřehlednějším programům a pro smysluplná jména je 6 znaků obvykle málo. Proto pozdější jazyky – a hlavně jejich překladače – dovolily používat podstatně delší identifikátory.
Nicméně s růstem rozsahu
projektů, a také s růstem rozsahu programových knihoven, se brzy ukázalo,
že “smysluplné identifikátory” snadno působí problémy jiného druhu: Dochází
ke konfliktům jmen. Poměrně často se stane, že různí členové vývojového týmu
přidělí různým proměnným nebo funkcím stejný identifikátor nebo že pro ně
použijí identifikátor, který zároveň označuje funkci, typ nebo proměnnou z
některé z programových knihoven. Stejně dobře se může stát, že v jednom projektu
použijeme dvě různé knihovny od dvou různých dodavatelů, které shodou okolností
používají týž identifikátor pro dvě různé věci – a problém je na světě.
Někteří programátoři
v jazyce C prý používali poměrně jednoduchý trik: Své globální proměnné deklarovali
jako složky struktury. Například takto:
struct { // Globální struktura
int proud1;
// a další proměnné
} Vstup;
// ...
int f() {
if(Vstup.proud1)
ZpracujTo();
// ... a další příkazy
}
Můžeme si představovat,
že Vstup
je označení části programu, který vyvíjí jeden člen týmu. To může docela dobře
fungovat, pokud se členové tohoto týmu dohodnou, jak se budou jejich struktury
s proměnnými jmenovat.
Toto řešení ovšem funguje
pouze pro identifikátory proměnných, nikoli pro identifikátory funkcí, neboť
ty nemohou být v jazyce C součástí struktury.
V C++ bychom mohli do
struktur ukrýt i funkce a datové typy. Tento jazyk nám ale nabízí elegantnější
řešení – prostory jmen. Na prostor jmen se můžeme dívat jako na příjmení,
které připojíme k identifikátoru a tím zajistíme jeho jednoznačnost v rámci
programu.
Syntax deklarace prostoru jmen je jednoduchá. Deklarace začíná
klíčovým slovem namespace, za nímž následuje identifikátor prostoru
jmen. Pak následují ve složených závorkách deklarace složek prostoru jmen.
Například takto:
// Deklarace prostoru jmen
namespace vstup {
int proud1;
void f();
class X;
}
Zde jsme v prostoru jmen vstup deklarovali globální proměnnou, funkci a třídu. Plné jméno (tzv. kvalifikované jméno) této proměnné je vstup::proud1, plné jméno funkce je vstup::f(), plné jméno třídy je vstup::X. (Říkáme, že identifikátory složek prostoru jmen kvalifikujeme jménem prostoru jmen; k tomu používáme operátor ::.
Uvnitř prostoru jmen můžeme deklarovat datové typy, funkce, proměnné, ale také další prostory jmen.
Funkci vstup::f(), stejně jako třídu X můžeme definovat
někde dále; v tom případě musíme jejich identifikátory kvalifikovat jménem
prostoru jmen.
void vstup::f(){
// ...
}
class vstup::X{ /* ... */ };
Nic nám ovšem nebrání zapsat jejich definice celé do prostoru jmen:
namespace vstup {
int proud1;
void f() { /* ... */ }
class X { /* ... */ };
}
Mimo prostor jmen vstup můžeme v témže programu deklarovat
funkci s prototypem
void f();
nebo třídu X a nedojde ke konfliktu.
Složky prostoru jmen můžeme mimo “jejich” prostor jmen používat,
pokud je kvalifikujeme jménem prostoru jmen (nebo pokud je nezpřístupníme
pomocí deklarace či direktivy using, o nichž si povíme dále).
To znamená, že ve funkci g(), která neleží v prostoru jmen
Vstup,
můžeme napsat
void g()
{
// Voláme funkci
f() z prostoru jmen vstup
vstup::f();
// Voláme funkci
f(), která neleží v prostoru
// jmen vstup
f();
vstup::proud1 = 6589;
// Deklarujeme instanci
třídy X
vstup::X x;
}
Uvnitř prostoru jmen můžeme jeho složky používat bez kvalifikace.
To znamená, že v těle funkce vstup::f() může vypadat např. takto:
void vstup::f()
{ // použije vstup::proud1
scanf("%d", &proud1);
X x;
}
Přitom je jedno, zda zapíšeme definici funkce f()
uvnitř prostoru jmen nebo zda ji v prostoru jmen vstup pouze deklarujeme a definici
zapíšeme někde jinde. Tělo funkce, deklarované v prostoru jmen, je vždy jeho
součástí.
Uvnitř prostoru jmen můžeme deklarovat další prostor jmen.
Například takto:
namespace vnejsi{
namespace vnitrni{
void
h(){/* ... */}
}
void h(){
vnitrni::h();
}
}
Jméno vnořeného prostoru jmen se skládá z identifikátoru tohoto prostoru, ke kterému připojíme operátorem :: jméno vnějšího prostoru jmen. Vnořený prostor jmen v předchozím příkladu se tedy jmenuje vnejsi::vnitrni.
Ve vnějším prostoru jmen stačí jména objektů, deklarovaných ve vnořeném prostoru jmen, kvalifikovat pouze jménem vnitřního prostoru jmen. Například funkci h(), deklarovanou v prostoru jmen vnejsi::vnitrni, můžeme ve funkci vnejsi::h() volat zápisem vnitrni::h().
Hloubka vnořování prostorů jmen není omezena.
Zavedení prostorů jmen znamená mnoho psaní navíc, a programátoři
jsou, jak známo, líní – ostatně jako většina lidí. Proto nabízí C++ několik
možností, jak si ušetřit práci.
Má-li prostor jmen dlouhé jméno, může být jeho opakované vypisování nepohodlné. Proto nabízí C++ možnost prostor jmen přejmenovat – definovat pro něj alias. Například prostor jmen vnejsi::vnitrni přejmenujeme na vv deklarací
namespace vv = vnejsi::vnitrni;
Potom budou zápisy vnejsi::vnitrni::h() a vv::h() ekvivalentní.
Poznamenejme, že alias můžeme definovat pro jakýkoli prostor
jmen, nejen pro vnořený. Alias umožňuje také pracovat s abstraktním prostorem
jmen: Jedinou deklarací aliasu určíme, o jaký prostor jmen jde. (Podobně např.
deklarace typedef
umožňuje pracovat s abstraktním datovým typem.)
Používáme-li často jména z některého prostoru jmen, můžeme
překladači říci, že budeme kvalifikaci jménem prostoru jmen vynechávat. K
tomu slouží tzv. direktiva using, jež má tvar
using namespace jméno;
Za touto direktivou můžeme používat identifikátor z prostoru jmen jméno bez kvalifikace.
Například všechny identifikátory ze standardní knihovny jazyka
C++ leží v prostoru jmen std. To znamená, že napíšeme-li
#include <iostream>
#include <list>
using namespace std;
můžeme psát
list<int> l;
for(int i = 0; i < 10; i++) l.push_front(i);
for(list<int>::iterator i = l.begin();
i != l.end(); i++)
{
cout << *i << endl;
}
Kdybychom vynechali direktivu using, museli bychom psát std::list<int>, std::list<int>::iterator, std::cout a std::endl.
Některé prostory jmen jsou značně rozsáhlé a my z nich potřebujeme
jen některá jména; v takovém případě může direktiva using
napáchat více škody než užitku. Proto nabízí jazyk C++ ještě deklaraci using, jež umožňuje specifikovat
jednotlivá jména z prostoru jmen, která chceme používat bez kvalifikace. Pokud
bychom chtěli např. používat bez kvalifikace jen identifikátory z konce předchozího
odstavce, mohli bychom napsat
using std::list; //
list je jméno šablony
using std::list<int>::iterator;
using std::cout;
using std::endl;
Podobně budeme-li chtít používat bez kvalifikace jméno funkce
Vstup::f(),
napíšeme
using vstup::f; //
Jen jméno
V deklaraci using uvádíme vždy jen jeden identifikátor. Deklarace using std::list; umožňuje používat bez kvalifikace šablonu list s jakýmikoli parametry.
Jednou ze zajímavých vlastností direktivy i deklarace using je, že jsou tranzitivní. To znamená: Uvedeme-li v prostoru jmen B direktivu
using namespace A;
a v prostoru jmen C direktivu
using namespace B;
můžeme v prostoru jmen C používat bez kvalifikace všechna jména z prostoru jmen A. Podobně zpřístupníme-li v prostoru jmen B nějaká jména z prostoru jmen A deklarací using a uvedeme-li v prostoru jmen C direktivu
using namespace B;
budou v prostoru jmen C tato jména přístupná.
Pokud vám to připadá nesrozumitelné, příklad to jistě vyjasní.
#include <iostream>
#include <list>
namespace pomocny {
using namespace std;
// ... další deklarace
}
namespace hlavni {
using namespace pomocny;
int Fufu()
{ // To je OK -
list<int>
l;
// tato jména
cout
<< "ahoj" // netřeba kvalifikovat
<< endl;
// ...
}
}
V prostoru jmen pomocny jsme mj. uvedli direktivu
using
namespace std;, která v něm zpřístupnila všechny identifikátory z prostoru
jmen std.
V prostoru jmen hlavni jsme si zpřístupnili všechna jména z prostoru
jmen pomocny
direktivou using
namespace pomocny;, a proto je můžeme v těle funkce hlavni::Fufu()
použít bez kvalifikace. Kdybychom změnili deklaraci prostoru jmen pomocny
následujícím způsobem,
namespace pomocny {
using std::cout;
using std::endl;
using std::list;
using std::list<int>::iterator;
// ... a další deklarace
}
mohli bychom díky v prostoru jmen hlavni používat bez
kvalifikace jen uvedená jména.
Jména, vnesená do nějakého prostoru jmen pomocí deklarace
nebo direktivy using, se chovají, jako kdyby byla jeho součástí. To
znamená, že platí-li výše uvedená deklarace prostoru jmen pomocny,
můžeme např. ve funkci main(), jež leží mimo jakýkoli prostor jmen, napsat
Pomocny::list<double> dl;
a bude to znamenat totéž, jako kdybychom napsali
std::list<double> dl;
Další zajímavou a užitečnou vlastností prostorů jmen v jazyce
C++ je, že je můžeme deklarovat po částech. Napíšeme-li někde v programu
namespace jupi {
void fupi();
}
nic nám nebrání napsat někde jinde
namespace jupi {
void gupi();
}
a překladač si obě části prostoru jmen jupi spojí do jednoho celku. Přitom tyto části nemusí ležet v témže souboru.
Překladač ovšem může vždy pracovat jen se jmény, která už byla deklarována, nedokáže se “podívat dopředu”. To znamená, že kdybychom napsali
namespace jupi {
void fupi(){/* ...
*/}
}
void dupy(){
jupi::gupi(); // Nelze
}
namespace jupi {
void gupi(){/* ...
*/}
}
ohlásil by překladač, že používáme funkci gupi(), která není součástí prostoru jmen jupi.
Příslušnost k prostoru jmen je třeba pochopitelně vyznačit
i v hlavičkových souborech. V nich ovšem zapisujeme pouze deklarace, nikoli
definice – jak je v C++ obvyklé.
Deklarujeme-li hlavičkový soubor
// Souboru jupi.h
namespace jupi {
void fupi();
void gupi();
}
a vložíme-li tento soubor direktivou
#include "jupi.g"
přijme překladač příklad z předchozího oddílu bez námitek.
Skutečnost, že deklaraci prostoru jmen lze rozdělit na několik
částí, umožňuje rozdělit velké prostory jmen do několika souborů. Typickým
příkladem je standardní knihovna jazyka C++, která leží, jak víme, v prostoru
jmen std.
Tato knihovna je popsána v řadě hlavičkových souborů. Skutečnost, že překladač
vidí jen ta jména, o nichž se dozví z deklarací, které si přečte, umožňuje
pracovat s menší množinou jmen, nikoli s celým prostorem jmen najednou.
Napíšeme-li ve svém programu
#include <iostream>
bude překladač znát jen jména, deklarovaná v tomto hlavičkovém souboru iostream, nikoli však jména deklarovaná v hlavičkových souborech list, queue, map a dalších. (Pozor ovšem na součásti zpřístupněné díky tranzitivitě direktiv a deklarací using. Jazyk C++ bohužel nespecifikuje vzájemné závislosti hlavičkových souborů.)
Poznamenejme, že v tomto ohledu se výrazně liší pojetí prostorů
jmen v C++ od pojetí prostorů jmen v jazyce C#.
V deklaraci prostoru jmen nemusíme uvést jeho jméno; pak
dostaneme tzv. anonymní (nepojmenovaný) prostor jmen. Můžeme např. napsat
namespace {
void huhu() {/* ...
*/ }
int x;
}
Na identifikátory deklarované v anonymním prostoru jmen se
můžeme odvolávat prostřednictvím samotného identifikátoru, případně identifikátoru,
před který připojíme unární operátor :: – ale pouze v
rámci samostatně překládané části programu, v němž je tento prostor jmen deklarován.
Překladač totiž spojí všechny anonymní prostoru jmen v jednom samostatně překládaném modulu v jeden prostor jmen a tomu přidějí jakýsi vnitřní identifikátor. V důsledku toho identifikátory, deklarované v tomto prostoru jmen, nejsou vidět mimo daný modul. Proměnné a funkce, deklarované v anonymním prostoru jmen, se tedy vlastně chovají jako statické (deklarované s modifikátorem static).
Podívejme se na jednu z mnoha variant proslulého programu ”Hello, world”:
// Hello, world – po kolikáté už
include <iostream>
using std::cout;
using std::endl;
int main()
{
cout <<
"Ahoj, lidi" << endl;
return 0;
}
O objektech cout a endl jsme překladači řekli, že patří do prostoru jmen std. Neuvedli jsme ale deklaraci
using std::operator<<;
která by zpřístupnila přetížené operátory <<.
Přesto překladače, které odpovídají současnému standardu jazyka C++, tento
program bez problémů přeloží – operátory << najdou a použijí. Za to
vděčíme pravidlu, které říká, že operátory a funkce se vyhledávají nejen v
kontextu jejich použití (tj. v prostoru jmen, v němž jsou volány), ale i v
prostorech jmen svých operandů, resp. parametrů. (Toto pravidlo se obvykle
označuje jako Koenigovo vyhledávání.)
Operátory << v tomto příkladu jsou sice použity mimo jakýkoli
prostor jmen, ale jejich operandy – cout a endl – leží v prostoru
jmen std,
a proto je bude překladač hledat i tam, a tam je také najde.
Podívejme se ještě na jeden příklad:
namespace prvni
{
class X{};
void f(X x){}
}
int main()
{
Prvni::X xx;
f(xx); // Ok
return 0;
}
Také zde použije překladač vyhledávání závislé na parametrech.
Funkci f()najde bez problémů, i když jsme její jméno nekvalifikovali
jménem prostoru jmen prvni, neboť ji bude hledat nejen
mimo prostory jmen, ale i v prostoru jmen prvni, v němž je deklarován typ
X
skutečného parametru.
K významu Koenigova vyhledávání se vrátíme příště.
Jazyk C++ převzal standardní knihovny jazyka C, a tedy také hlavičkové soubory, v nichž jsou makra, funkce, typy a konstanty z této knihovny deklarovány. Názvy těchto souborů se ovšem v C++ změnily: Odpadla přípona .h a před jméno se připojil znak c. To znamená, že např. hlavičkový soubor, známý v jazyce C pod názvem stdio.h, se v C++ jmenuje cstdio.
Chceme-li některou z konstrukcí z knihovny jazyka C knihovny
použít v C++, máme dvě možnosti:
- Můžeme použít jméno hlavičkového souboru podle pravidel jazyka C++. V tom případě budou identifikátory z něj ležet v prostoru jmen std.
- Můžeme použít hlavičkový soubor z jazyka C tak, jak jsme byli v C zvyklí. V tom případě budeme identifikátory z tohoto hlavičkového souboru používat bez kvalifikace. (Tuto možnost standard připouští, ale pokládá ji za zastaralou.)
To znamená, že napíšeme-li
#include <cstdio>
musíme psát
std::printf("Ahoj, lidi");
nebo použít direktivu či deklaraci using. Na druhé straně
napíšeme-li
#include <stdio.h>
můžeme napsat
printf("Ahoj, lidi");
Hlavičkové soubory jazyka C++ podle standardu nemají příponu .h. Nicméně ve starších verzích jazyka, které neobsahovaly prostory jmen, tyto příponu měly, a proto řada překladačů pro ně používá podobnou konvenci jako pro hlavičkové soubory z jazyka C: Uvedeme-li v jejich jménu příponu .h, nemusíme jména z nich kvalifikovat jménem prostoru jmen std.
V úvodu jsme si řekli, že prostory jmen jedním z posledních velkých rysů jazyka C++, který překladače implementovaly. Doplňme, že mnohé s nimi mají problémy dodnes. Například Visual C++ .NET implementuje Koenigovo vyhledávání pouze pro operátory volané operátorovým zápisem. Pro funkce, a dokonce ani pro operátory volané funkčním zápisem, je nepoužívá.
Tolik o prostorech jmen a o Koenigově vyhledávání. Příště se podíváme na důsledky této konstrukce pro práci s třídami v C++.
1. B. Stroustrup, M. A. Ellis: The Annotated C++ Reference Manual. Addison-Wesley 1991.