23.2 Przestrzenie nazw

Aby ułatwić jeszcze bardziej tworzenie w C++ dużych, pisanych przez wielu programistów aplikacji, dodano do języka mechanizm przestrzeni nazw (ang. name space). Jest to narzędzie pozwalające logicznie grupować nazwy obiektów rozmaitego typu: zmiennych, funkcji, klas, wyliczeń, wzorców itd.

Przypuśćmy, że stworzyliśmy w programie zestaw klas i funkcji do obsługi graficznego interfejsu użytkownika (GUI), a ktoś inny napisał zestaw realizujący tzw. business logic. Możemy te dwa zestawy umieścić w osobnych przestrzeniach nazw za pomocą słowa kluczowego namespace, po którym, w nawiasach klamrowych, umieszczamy deklaracje i ewentualnie definicje:

       namespace GUI {
           class Menu { /* ... */ };
           void show(Menu&);
           double calculate(double) {
               // ...
           }
           // ...
       }

       namespace Business {
           class Tax { /* ... */ };
           double calculate(double&);
           // ...
       }
Zauważmy, że w obu przestrzeniach nazw występuje funkcja calculate. W jednej z nich została już zdefiniowana, w drugiej tylko zadeklarowana. Nie powoduje to żadnego konfliktu, bo obie funkcje należą do różnych przestrzeni nazw i nie mają ze sobą nic wspólnego, niezależnie od tego, czy ich sygnatura jest taka sama czy inna. Jeśli umieściliśmy nazwę jakiegoś obiektu (stałej, klasy, funkcji, wyliczenia...) w przestrzeni nazw, to pełną, kwalifikowaną nazwą tego obiektu staje się nazwa obiektu poprzedzona identyfikatorem przestrzeni nazw i czterokropkiem. Na przykład, aby zdefiniować funkcję calculate zadeklarowaną w przestrzeni nazw Business, musimy użyć w definicji pełnej nazwy (chyba, że samą definicję umieścimy wewnątrz bloku namespace):
       double Business::calculate(double& z) {
           // ...
       }
a żeby zdefiniować poza zakresem przestrzeni nazw np. destruktor klasy Menu z przestrzeni nazw GUI:
       GUI::Menu::~Menu() { /* ... */ }
Do istniejących przestrzeni nazw można dodawać nowe elementy. Na przykład tenże destruktor moglibyśmy zdefiniować tak
       namespace GUI {
           Menu::~Menu() { /* ... */ }
           const double PI = 3.14;
       }
Zauważmy, że nazwy klasy Menu nie musieliśmy teraz kwalifikować nazwą GUI, bo całą definicję umieściliśmy w zakresie tej przestrzeni poprzez użycie słowa kluczowego namespace. Przy okazji dodaliśmy do tej przestrzeni nazw nowy element w postaci nazwy stałej zmiennopozycyjnej PI.

Funkcje calculate z obu przestrzeni można wywoływać bezkonfliktowo:

       double x, y, v, w;
       // ...
       x = GUI::calculate(v);
       y = Business::calculate(w);
W ten sposób nazwy z różnych przestrzeni nazw są od siebie odseparowane. Jeśli na przykład różne fragmenty aplikacji lub bibliotek piszą różni autorzy, to każdy może stworzyć dla swoich klas czy funkcji oddzielną przestrzeń nazw i nie przejmować się ewentualnym konfliktem nazw; użytkownik jego klasy będzie musiał świadomie ją wybrać kwalifikując nazwę klasy nazwą przestrzeni nazw.

Z drugiej strony, jeśli jesteśmy pewni, że żadnego konfliktu nie będzie, to konieczność poprzedzania wszystkich identyfikatorów nazwą przestrzeni może być nieco uciążliwa. Można temu zaradzić poprzez deklarację użycia. Deklaracja taka, wyrażona za pomocą słowa kluczowego using, informuje kompilator, że pewna nazwa oznaczać będzie obiekt nią identyfikowany z pewnej konkretnej przestrzeni nazw, stając się jej synonimem (aliasem). Na przykład po

       using GUI::calculate;
w zasięgu, w którym deklaracja ta została użyta, niekwalifikowana nazwa calculate będzie się odnosić do takiej nazwy z przestrzeni GUI, a zatem będzie synonimem nazwy GUI::calculate. Oczywiście do calculate z przestrzeni Business w dalszym ciągu możemy się odnosić, ale tę nazwę będziemy musieli kwalifikować, czyli pisać jawnie Business::calculate(x).

Można w końcu, co nie jest na ogół zalecane, włączyć do aktualnego zakresu wszystkie nazwy z pewnej przestrzeni nazw. Służy do tego dyrektywa użycia, wyrażona przez dwa słowa kluczowe using namespace. Na przykład po

       using namespace GUI;
w aktualnym zasięgu można używać wszystkich nazw z przestrzeni GUI bez kwalifikowania ich nazwą tej przestrzeni.

Jeśli przestrzeń nazw, którą za pomocą dyrektywy użycia „otwieramy” jest duża i nam niezbyt dobrze znana, to taką dyrektywą można doprowadzić do konfliktów nazw, uniknięciu których przestrzenie nazw miały przecież służyć.

Rozpatrzmy jeszcze prosty przykład:


P182: nmspc.cpp     Przestrzenie nazw

      1.  #include <iostream>
      2.  
      3.  namespace A {
      4.      const int two = 2;
      5.      const int six = 6;
      6.      void write() { std::cout << "ns-A" << " "; }
      7.  }
      8.  
      9.  namespace B {
     10.      void write() { std::cout << "ns-B" << " "; }
     11.  }
     12.  
     13.  namespace C {
     14.      const int two = 22;
     15.      const int six = 66;
     16.  }
     17.  
     18.  int main() {
     19.      using A::write;
     20.      using namespace C;
     21.  
     22.      write();
     23.      B::write();
     24.      std::cout << six << std::endl;
     25.  }

Mamy tu trzy przestrzenie nazw. W programie głównym deklarujemy (linia 19), że niekwalifikowanej nazwy write będziemy w zakresie funkcji main używać jako synonimu nazwy A::write. Otwierając w linii 20 przestrzeń nazw  C powodujemy, że wszystkie nazwy z tej przestrzeni mogą być używane wewnątrz funkcji main bez kwalifikowania ich nazwą przestrzeni. Dlatego stała six użyta w linii 24 odnosi się do stałej tak nazwanej w przestrzeni  C, a nie z przestrzeni  A. Oczywiście funkcja write z przestrzeni  B jest w dalszym ciągu dostępna, tyle że jej użycie wymaga nazwy kwalifikowanej (linia 23). Program drukuje 'ns-A ns-B 66'.

Zauważmy, że nazwy coutendl w tym programie kwalifikujemy nazwą przestrzeni nazw std (linie 6, 10 i 24). Włączyliśmy bowiem nagłówek iostream, ale nie napisaliśmy zaraz potem sakramentalnego

       using namespace std;
Otóż wszystkie udogodnienia biblioteki standardowej, do których mamy dostęp po włączeniu plików nagłówkowych (między innymi pliku iostream), są umieszczane w przestrzeni nazw std. Wyjątkiem są funkcje operator newoperator delete oraz makra preprocesora. Normalnie, dla uproszczenia, w naszych przykładowych programach otwieraliśmy na samym początku całą tę przestrzeń nazw, właśnie za pomocą powyższej dyrektywy użycia. Dzięki temu nie musieliśmy kwalifikować jej nazwą takich nazw, jak cout czy endl. Teraz natomiast tej przestrzeni nie otworzyliśmy, więc nazwy z niej pochodzące musieliśmy kwalifikować.

Przestrzenie nazw to stosunkowo nowy element języka C++. W C tradycyjnie używa się również plików nagłówkowych, ale wszystkie nazwy w nich deklarowane są jednakowo dostępne bez żadnych kwalifikacji, bo nie ma tam mechanizmu przestrzeni nazw. Trzeba było zatem zapewnić możliwość kompilowania przez kompilatory C++ programów napisanych w C. Osiągnięto to poprzez umowę, że jeśli użyjemy tradycyjnej (pochodzącej z C) nazwy pliku nagłówkowego, to odpowiedni plik jest włączany i zadeklarowane w nim nazwy są dodawane do domyślnej przestrzeni nazw. Jeśli natomiast ten sam plik nagłówkowy włączymy pod „nową” nazwą, to nazwy w nim deklarowane są dodawane do przestrzeni nazw std. Przyjęto przy tym konwencję, że pliki nagłówkowe o tradycyjnej nazwie nazwa.h są w C++ nazywane cnazwa. Tak więc dyrektywa preprocesora

       #include <string.h>
jest równoważna
       #include <cstring>
tyle że w pierwszym przypadku zawartość jest włączana do domyślnej przestrzeni nazw, zaś w drugim do przestrzeni std. To samo dotyczy następujących 18 plików nagłówkowych pochodzących z C:
Tabela: Pliki nagłówkowe z C w C++
C C ++ C C ++ C C ++
assert.h cassert ctype.h cctype errno.h cerrno
float.h cfloat iso646.h ciso646 limits.h climits
locale.h clocale math.h cmath setjmp.h csetjmp
signal.h csignal stdarg.h cstdarg stddef.h cstddef
stdio.h cstdio stdlib.h cstdlib string.h cstring
time.h ctime wchar.h cwchar cwtype.h cwctype

Plik iostream.h jest bardzo często używany zamiast iostream. Zauważmy, że nie pochodzi on w ogóle z C, więc powyżej opisana zasada go nie dotyczy. Nie powinno się go nigdy używać, gdyż nie należy do standardu i w związku z tym może w ogóle nie istnieć! Zawsze należy stosować prawidłową nazwę iostream.

Pozostałe 32 standardowe pliki nagłówkowe nie pochodzą z C i są charakterystyczne tylko dla C++:

Tabela: Pliki nagłówkowe C++
algorithm iomanip list ostream streambuf
bitset ios locale queue string
complex iosfwd map set typeinfo
deque iostream memory sstream utility
exception istream new stack valarray
fstream iterator numeric stdexcept vector
functional limits      

Zakończmy ten rozdział inną wersją przykładu stack.cpp. Teraz nazwy z modułu opisującego stos umieszczamy w przestrzeni nazw mySTACKS, co zapobiega konfliktom między nazwami zadeklarowanymi w tym module a nazwami pochodzącymi z innych przestrzeni nazw. Nagłówek deklarujący abstrakcyjną klasę STACK umieszczamy w pliku mySTACK.h


P183: mySTACK.h     Nagłówek klasy abstrakcyjnej

      1.  #ifndef mySTACK_H
      2.  #define mySTACK_H
      3.  
      4.  namespace mySTACKS {
      5.      class STACK
      6.      {
      7.      public:
      8.          virtual void push(int) = 0;
      9.          virtual int pop()      = 0;
     10.          virtual bool empty()   = 0;
     11.          static STACK* getInstance(int);
     12.          virtual ~STACK() { }
     13.      };
     14.  }
     15.  #endif

Z kolei nagłówek dla implementacji umieszczamy w pliku mySTACKS.h — włącza on mySTACK.h i dodaje do przestrzeni nazw mySTACKS klasy konkretne, które będą implementować klasę abstrakcyjną STACK:

P184: mySTACKS.h     Nagłówek dla klas implementujących

      1.  #ifndef mySTACKS_H
      2.  #define mySTACKS_H
      3.  
      4.  #include "mySTACK.h"
      5.  
      6.  namespace mySTACKS {
      7.  
      8.      class ListStack: public STACK {
      9.          struct Node {
     10.              int   data;
     11.              Node* next;
     12.              Node(int data, Node* next);
     13.          };
     14.          Node* head;
     15.          ListStack();
     16.      public:
     17.          friend STACK* STACK::getInstance(int);
     18.          int pop();
     19.          void push(int data);
     20.          bool empty();
     21.          ~ListStack();
     22.      };
     23.  
     24.      class ArrayStack : public STACK {
     25.          int  top;
     26.          int* arr;
     27.          ArrayStack();
     28.      public:
     29.          friend STACK* STACK::getInstance(int);
     30.          void push(int data);
     31.          int pop();
     32.          bool empty();
     33.          ~ArrayStack();
     34.      };
     35.  }
     36.  #endif

Plik z implementacją, mySTACKSImpl.cpp, włącza nagłówek mySTACKS.h (który z kolei włącza mySTACK.h) i dostarcza implementacji klas konkretnych

P185: mySTACKSImpl.cpp     Implementacja

      1.  #include <iostream>
      2.  #include "mySTACKS.h"
      3.  using namespace mySTACKS;
      4.  
      5.  // ListStack
      6.  
      7.  ListStack::Node::Node(int data, Node* next)
      8.      : data(data), next(next)
      9.  { }
     10.  
     11.  ListStack::ListStack() {
     12.      head = NULL;
     13.      std::cerr << "Creating ListStack" << std::endl;
     14.  }
     15.  
     16.  int ListStack::pop() {
     17.      int   data = head->data;
     18.      Node* temp = head->next;
     19.      delete head;
     20.      head = temp;
     21.      return data;
     22.  }
     23.  
     24.  void ListStack::push(int data) {
     25.      head = new Node(data, head);
     26.  }
     27.  
     28.  bool ListStack::empty() {
     29.      return head == NULL;
     30.  }
     31.  
     32.  ListStack::~ListStack() {
     33.      std::cerr << "Deleting ListStack" << std::endl;
     34.      while (head) {
     35.          Node* node = head;
     36.          head = head->next;
     37.          std::cerr << " node " << node->data << std::endl;
     38.          delete node;
     39.      }
     40.  }
     41.  
     42.  // ArrayStack
     43.  
     44.  ArrayStack::ArrayStack() {
     45.      top = 0;
     46.      arr = new int[100];
     47.      std::cerr << "Creating ArrayStack" << std::endl;
     48.  }
     49.  
     50.  void ArrayStack::push(int data) {
     51.      arr[top++] = data;
     52.  }
     53.  
     54.  int ArrayStack::pop() {
     55.      return arr[--top];
     56.  }
     57.  
     58.  bool ArrayStack::empty() {
     59.      return top == 0;
     60.  }
     61.  
     62.  ArrayStack::~ArrayStack() {
     63.      std::cerr << "Deleting ArrayStack with " << top
     64.                << " elements remaining" << std::endl;
     65.      delete [] arr;
     66.  }
     67.  
     68.  // STACK
     69.  
     70.  STACK* STACK::getInstance(int size) {
     71.      if (size > 100)
     72.          return new ListStack();
     73.      else
     74.          return new ArrayStack();
     75.  }

Możemy teraz skompilować ten plik, aby usyskać plik binarny mySTACKSImpl.o
    cpp> g++ -pedantic-errors -Wall -c mySTACKSImpl.cpp
Tylko plik mySTACKSImpl.o i plik nagłówkowy mySTACK.h jest teraz potrzebny, aby skompilować aplikację wykorzystującą nasz moduł:

P186: stacksApp.cpp     Aplikacja

      1.  #include <iostream>
      2.  #include "mySTACK.h"
      3.  
      4.  int main() {
      5.  
      6.      mySTACKS::STACK* stack;
      7.  
      8.      stack = mySTACKS::STACK::getInstance(120);
      9.      stack->push(1);
     10.      stack->push(2);
     11.      stack->push(3);
     12.      stack->push(4);
     13.      std::cout << stack->pop() << " ";
     14.      std::cout << stack->pop() << std::endl;
     15.      delete stack;
     16.  
     17.      stack = mySTACKS::STACK::getInstance(50);
     18.      stack->push(1);
     19.      stack->push(2);
     20.      stack->push(3);
     21.      stack->push(4);
     22.      std::cout << stack->pop() << " ";
     23.      std::cout << stack->pop() << std::endl;
     24.      delete stack;
     25.  }

Kompilacja:
    cpp> g++ -pedantic-errors -Wall -o stacksApp \
                   stacksApp.cpp mySTACKSImpl.o
tworzy plik wynikowy stacksApp, który po uruchomieniu daje
    cpp> ./stacksApp
    Creating ListStack
    4 3
    Deleting ListStack
     node 2
     node 1
    Creating ArrayStack
    4 3
    Deleting ArrayStack with 2 elements remaining

T.R. Werner, 23 lutego 2019; 23:59