2.4. Предотвращение конфликта имен с помощью пространств имен

2.4. Предотвращение конфликта имен с помощью пространств имен

Проблема

В несвязанных между собой модулях обнаружены конфликтующие имена или требуется заранее избежать возможности таких конфликтов, создав логические группы кода.

Решение

Для структурирования кода используйте пространства имен. С помощью пространств имен можно объединять большие группы кода, находящиеся в разных файлах, в единое пространство имен. Для разбиения больших модулей на подмодули можно использовать вложенные пространства имен, и потребители вашего модуля смогут выборочно открывать элементы вашего пространства имен, которые им требуются. Пример 2.5 показывает несколько способов использования пространства имен.

Пример 2.5. Использование пространств имен

// Devices.h

#ifndef DEVICES_H__

#define DEVICES_H__

#include <string>

#include <list>

namespace hardware {

 class Device {

 public:

  Device(): uptime_(0), status_("unknown") {}

  unsigned long getUptime() const;

  std::string getStatus() const;

  void reset();

 private:

  unsigned long uptime_;

  std::string status_;

 };

 class DeviceMgr {

 public:

  void getDeviceIds(std::list<std::string>& ids) const;

  Device getDevice(const std::string& id) const;

  // Other stuff...

 };

}

#endif // DEVICES_H__

// Devices.cpp

#include "Devices.h"

#include <string>

#include <list>

namespace hardware {

 using std::string;

 using std::list;

 unsigned long Device::getUptime() const {

  return(uptime__);

 }

 string Device::getStatus() const {

  return(status_);

 }

 void DeviceMgr::getDeviceIds(list<string>& ids) const {}

 Device DeviceMgr::getDevice(const string& id) const {

  Device d;

  return(d);

 }

}

// DeviceWidget.h

#ifndef DEVICEWIDGET_H__ #define DEVICEWIDGET_H__

#include "Devices.h"

namespace ui {

 class Widget {/*... */ };

 class DeviceWidget : public Widget {

 public:

  DeviceWidget(const hardware::Device& dev) : device_(dev) {}

  // Some other stuff

 protected:

  hardware::Device device_;

 };

}

#endif // DEVICEWIDGET_H__

// main.cpp

#include <iostream>

#include "DeviceWidget.h"

#include "Devices.h"

int main( ) {

 hardware::Device d;

 ui::DeviceWidget myWidget(d);

 // ...

}

Обсуждение

Пример 2.5 более сложен, но давайте разберем его по частям, так как он иллюстрирует несколько ключевых моментов, связанных с пространствами имен. Представьте, что вы пишете управляющее приложение, которое должно взаимодействовать с различным оборудованием. С целью устранения конфликтов имен вы можете разбить это приложение на два или более пространств имен или просто разделить его логически на две части.

Вначале рассмотрим файл Devices.h. Он содержит пару классов, которые управляют элементами оборудования, — Device и DeviceMgr. Однако они не должны находиться в глобальном пространстве имен (что означало бы, что их имена видны в любом месте программы), так что я поместил их в пространство имен hardware.

#ifndef DEVICES_H__ // см. рецепт 2.0

#define DEVICES_H__

#include <string>

#include <list>

namespace hardware {

 class Device {

  // ...

 };

 class DeviceMgr {

  // ...

 };

}

#endif // DEVICES_H__

Этот механизм прост: «оберните» все, что требуется поместить в пространство имен, в блок namespace.

Приведенный выше фрагмент — это объявление Device и DeviceMgr, но нам также требуется подумать об их реализациях, которые находятся в файле Devices.cpp. И снова оберните все в блок namespace — он будет добавлен к уже имеющемуся содержимому этого пространства имен.

#include "Devices.h"

#include <string>

#include <list>

namespace hardware {

 using std::string;

 using std::list;

 // Реализация Device и DeviceMgr

}

В данный момент пространство имен hardware содержит все, что требуется. Все, что осталось, — это где-то его использовать. Для этого имеется несколько способов. Способ, который был использован в примере 2.5, состоит в указании полного имени класса Device, включая пространство имен, как здесь.

#ifndef DEVICEWIDGET_H_

#define DEVICEWIDGET_H_

#include "Devices.h"

namespace ui {

 class Widget { /* ... */ };

 class DeviceWidget : public Widget {

 public:

  DeviceWidget(const hardware::Device& dev) : device_(dev) {}

  // Other stuff...

 protected:

  hardware::Device device_;

 };

}

#endif // DEVICEWIDGET_H__

Также я использую этот способ в main в main.cpp.

int main() {

 hardware::Device d;

 ui::DeviceWidget myWidget(d);

}

Чтобы добавить к одному из пространств имен типы, объявите заголовочный файл и файл реализации так, как показано в примере 2.5. При каждом использовании блока пространства имен, обрамляющего код, этот код добавляется в это пространство имен, так что в пространстве имен может находиться код, который ничего не знает о другом коде этого же пространства имен.

При использовании подхода с указанием полных имен классов, включающих пространство имен, вы быстро устанете вводить код. Имеется пара способов устранить эту проблему. Для полного имени типа с помощью ключевого слова using можно создать псевдоним.

using hardware::Device;

int main() {

 Device d; // Пространство имен не требуется

 ui::DeviceWidget myWidget(d);

}

В последующем коде вместо ввода полного имени можно просто сослаться на этот псевдоним. Или можно с помощью using импортировать все содержимое пространства имен, а не только один содержащийся в нем тип.

using namespace hardware;

int main() {

 Device d:

 ui::DeviceWidget myWidget(d);

}

Этот вариант вы, вероятно, уже использовали, или, по крайней мере, видели в примерах, при использовании стандартной библиотеки (эту методику используют многие примеры в этой книге). Вся стандартная библиотека находится в пространстве имен std, так что очень часто вы увидите такое:

using namespace std;

Импорт всего пространства имен часто является плохой идеей и обычно рассматривается как плохой стиль. В примерах в этой книге мы импортируем полное содержимое пространства имен std только с целью повышения ясности кода и обычно рекомендуем не делать этого в реальных программах.

При импорте всего пространства имен или даже нескольких пространств имен их полезность значительно снижается. Одной из причин существования пространств имен является снижение конфликтов имен. При импорте нескольких различных пространств имен вероятность конфликтов имен увеличивается. В данный момент код может прекрасно компилироваться, но в будущем в одно из пространств имен может быть что-то добавлено, и при последующей сборке кода возникнет конфликт.

Чтобы разделить содержимое пространства имен на более мелкие группы, можно использовать вложенные пространства имен. Например, пространство имен hardware, определенное в примере 2.5, может на самом деле содержать большое количество сетевых классов и еще больше классов устройств, так что его можно было бы разделить, вложив еще несколько описательных имен.

namespace hardware {

 namespace net {

  // сетевые классы

 }

 namespace devices {

  // классы устройств

 }

}

Теперь доступ к элементам, содержащимся в пространстве имен, стал несколько более сложным.

// В каком-либо другом файле...

using hardware::devices::Device;

Пространства имен довольно удобны. Есть несколько интересных вещей, связанных с пространствами имен, облегчающих жизнь: псевдонимы пространств имен, автоматический поиск имен в пространствах имен параметров функций и подстановка имен перегрузок функций в объявлениях using. Последние два длинны по названиям, но просты.

Псевдоним пространства имен — это то, что означает его название: имя (возможно, короткое), которое используется для замены имени (возможно, длинного) пространства имен. Если вы не хотите использовать выражение using, но также не хотите вводить при каждом использовании класса огромное полное имя, создайте для него псевдоним.

using dev = hardware::devices;

// ...

dev::Device d;

Затем этот псевдоним используется при ссылке на элементы соответствующего пространства имен.

C++ также предоставляет автоматический поиск в пространствах имен, к которым относятся параметры функций. Так, например, следующий код описывает аргументы в пространстве имен (dev — это пространство имен, в котором объявлен Device):

void f(dev::Device& d) {

 register(d); // на самом деле это dev::register

}

При передаче функции параметра, принадлежащего пространству имен, компилятор включает это пространство имен при поиске имен функций, вызываемых в теле этой функции. Это, может быть, не самая часто используемая особенность, но когда она используется, то экономит значительное время набора кода и позволяет избежать лишних директив using. В основе этого лежит идея о том, что функции, которые оперируют с каким-либо типом, часто определяются в том же пространстве имен, что и этот тип. Кстати, вообще всегда, когда возможно, следует помещать функции, оперирующие определенными типами, в то же пространство имен, в котором находятся эти типы.

Последним интересным моментом, связанным с пространствами имен, является подстановка имен перегрузок в объявлениях using. Рассмотрим такой пример.

namespace mylib {

 void foo(mt);

 void foo(double);

 void foo(std::string);

 // Другие перегрузки foo( )...

}

// В каком-то другом файле...

using mylib::foo; // Какой вариант используется?

Объявление using соответствует всем перегрузкам foo, так что писать отдельную директиву для каждой перегрузки не требуется. Другим преимуществом этой записи является то, что если добавляется еще одна перегрузка foo, то весь код, содержащий объявление вида mylib::foo, видит ее автоматически (конечно, при компиляции кода, содержащего это объявление), так как объявление using включает и ее.

Конечно, использовать пространства имен следует обдуманно, а иначе у вас или тех, кто будет их использовать, появятся неожиданные ошибки компиляции. Вот несколько популярных советов по использованию пространств имен.

Как можно реже используйте using namespace xxx

Как я объяснял ранее, импорт всего пространства имен увеличивает вероятность конфликта имен — либо сразу, либо в будущем (в используемое вами пространство имен может быть добавлено что-то, что приведет к конфликту). Это также снижает степень модульности, предоставляемую пространствами имен.

Не используйте оператор using в заголовочных файлах

Заголовочные файлы включаются большим количеством других файлов, так что использование пространства имен или чего-либо из пространства имен в заголовочном файле открывает его файлам, включающим этот заголовочный файл. Решение этой проблемы заключается в указании в заголовочных файлах полных имен.

Не помещайте объявления using или определения перед директивами #include

Если это сделать, тогда то, что указано в директиве using, будет открыто для кода заголовочного файла, что, вероятно, не входило в намерения автора этого заголовочного файла.

При выполнении этих правил использование пространств имен в новом проекте или добавление их в существующий проект должно быть относительно просто.