6.2 – Пользовательские пространства имен

Добавлено 11 мая 2021 в 00:41

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

Давайте еще раз рассмотрим пример конфликта имен, а затем покажем, как мы можем разрешить его с помощью пространств имен. В следующем примере foo.cpp и goo.cpp – это исходные файлы, содержащие функции, выполняющие разные действия, но имеющие одинаковое имя и параметры.

foo.cpp:

// Эта doSomething() складывает значения своих параметров
int doSomething(int x, int y)
{
    return x + y;
}

goo.cpp:

// Эта doSomething() вычитает значения своих параметров
int doSomething(int x, int y)
{
    return x - y;
}

main.cpp:

#include <iostream>
 
int doSomething(int x, int y); // предварительное объявление для doSomething
 
int main()
{
    std::cout << doSomething(4, 3) << '\n'; // какую doSomething мы получим?
    return 0;
}

Если этот проект содержит только либо foo.cpp, либо goo.cpp (но не оба сразу), он без проблем будет скомпилирован и запущен. Однако, скомпилировав оба этих файла в одну и ту же программу, мы теперь ввели две разные функции с одинаковыми именами и параметрами в одну и ту же область видимости (глобальную область видимости), что вызывает конфликт имен. В результате компоновщик выдаст ошибку:

goo.cpp:3: multiple definition of `doSomething(int, int)'; foo.cpp:3: first defined here

Обратите внимание, что эта ошибка возникает в момент переопределения, поэтому не имеет значения, вызывается ли когда-либо функция doSomething.

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

Определение ваших собственных пространств имен

C++ позволяет нам определять наши собственные пространства имен с помощью ключевого слова namespace. Пространства имен, которые вы создаете для своих собственных объявлений, называются пользовательскими пространствами имен. Пространства имен, предоставляемые C++ (например, глобальное пространство имен) или библиотеками (например, пространство имен std), не считаются пользовательскими.

Идентификаторы пространства имен обычно пишутся без заглавных букв.

Ниже приведен пример файлов из предыдущего примера, переписанных с использованием пространств имен:

foo.cpp:

namespace foo // определяем пространство имен с именем foo
{
    // Эта doSomething() принадлежит пространству имен foo
    int doSomething(int x, int y)
    {
        return x + y;
    }
}

goo.cpp:

namespace goo // определяем пространство имен goo
{
    // Эта doSomething() принадлежит пространству имен goo
    int doSomething(int x, int y)
    {
        return x - y;
    }
}

Теперь doSomething() внутри foo.cpp находится внутри пространства имен foo, а doSomething() внутри goo.cpp находится внутри пространства имен goo. Посмотрим, что произойдет, когда мы перекомпилируем нашу программу.

main.cpp:

int doSomething(int x, int y); // предварительное объявление для doSomething
 
int main()
{
    std::cout << doSomething(4, 3) << '\n'; // какую doSomething мы получим?
    return 0;
}

А ответ – теперь мы получаем еще одну ошибку!

ConsoleApplication1.obj : error LNK2019: unresolved external symbol "int __cdecl doSomething(int,int)" (?doSomething@@YAHHH@Z) referenced in function _main

В этом случае компилятор был удовлетворен (нашим предварительным объявлением), но компоновщик не смог найти определение для doSomething в глобальном пространстве имен. Это потому, что обе наши версии doSomething больше не находятся в глобальном пространстве имен!

Есть два разных способа сообщить компилятору, какую версию doSomething() использовать, с помощью оператора разрешения области видимости или с помощью инструкций using (которые мы обсудим на следующем уроке этой главы).

Для удобства чтения в последующих примерах мы объединим предыдущие примеры в однофайловый проект.

Доступ к пространству имен с помощью оператора разрешения области видимости (::)

Лучший способ указать компилятору искать идентификатор в определенном пространстве имен – это использовать оператор разрешения области видимости (::). Оператор разрешения области видимости сообщает компилятору, что идентификатор, указанный правым операндом, следует искать в области видимости левого операнда.

Ниже показан пример использования оператора разрешения области видимости, чтобы сообщить компилятору, что мы явно хотим использовать версию doSomething(), которая находится в пространстве имен foo:

#include <iostream>
 
namespace foo // определяем пространство имен с именем foo
{
    // Эта doSomething() принадлежит пространству имен foo
    int doSomething(int x, int y)
    {
        return x + y;
    }
}
 
namespace goo // определяем пространство имен goo
{
    // Эта doSomething() принадлежит пространству имен goo
    int doSomething(int x, int y)
    {
        return x - y;
    }
}
 
int main()
{
    // используем doSomething(), которая находится в пространстве имен foo
    std::cout << foo::doSomething(4, 3) << '\n'; 
    return 0;
}

Это дает ожидаемый результат:

7

Если бы мы хотели вместо этого использовать версию doSomething(), которая находится в goo:

#include <iostream>
 
namespace foo // определяем пространство имен с именем foo
{
    // Эта doSomething() принадлежит пространству имен foo
    int doSomething(int x, int y)
    {
        return x + y;
    }
}
 
namespace goo // определяем пространство имен goo
{
    // Эта doSomething() принадлежит пространству имен goo
    int doSomething(int x, int y)
    {
        return x - y;
    }
}
 
int main()
{
    // используем doSomething(), которая находится в пространстве имен goo
    std::cout << goo::doSomething(4, 3) << '\n'; 
    return 0;
}

Эта программа дает следующий результат:

1

Оператор разрешения области видимости хорош, потому что он позволяет нам явно выбирать, в каком пространстве имен мы хотим искать, и поэтому нет потенциальной неоднозначности. Мы даже можем сделать следующее:

#include <iostream>
 
namespace foo // определяем пространство имен с именем foo
{
    // Эта doSomething() принадлежит пространству имен foo
    int doSomething(int x, int y)
    {
        return x + y;
    }
}
 
namespace goo // определяем пространство имен goo
{
    // Эта doSomething() принадлежит пространству имен goo
    int doSomething(int x, int y)
    {
        return x - y;
    }
}
 
int main()
{
    // используем doSomething(), которая находится в пространстве имен foo
    std::cout << foo::doSomething(4, 3) << '\n'; 
    // используем doSomething(), которая находится в пространстве имен goo
    std::cout << goo::doSomething(4, 3) << '\n'; 
    return 0;
}

Эта программа дает следующий результат:

7
1

Использование оператора разрешения области видимости без префикса имени

Оператор разрешения области видимости также может использоваться перед идентификатором без указания имени пространства имен (например, ::doSomething). В таком случае идентификатор (например, doSomething) ищется в глобальном пространстве имен.

#include <iostream>
 
void print() // эта функция print находится в глобальном пространстве имен
{
	std::cout << " there\n";
}
 
namespace foo
{
	void print() // эта функция print находится в пространстве имен foo
	{
		std::cout << "Hello";
	}
}
 
int main()
{
	foo::print(); // вызываем print() из пространства имен foo
	::print();    // вызываем print() из глобального пространства имен 
                  // (в этом случае то же самое, что и просто вызов print())
 
	return 0;
}

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

Разрешение идентификатора из пространства имен

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

#include <iostream>
 
void print() // эта функция print находится в глобальном пространстве имен
{
	std::cout << " there\n";
}
 
namespace foo
{
	void print() // эта функция print эта функция в пространстве имен foo
	{
		std::cout << "Hello";
	}
 
	void printHelloThere()
	{
		print();   // вызывает print() из пространства имен foo
		::print(); // вызывает print() из глобального пространства имен
	}
}
 
int main()
{
    foo::printHelloThere();
	return 0;
}

Эта программа напечатает:

Hello there

В приведенном выше примере print() вызывается без разрешения области видимости. Поскольку это использование print() находится внутри пространства имен foo, компилятор сначала ищет, можно ли найти объявление для foo::print(). Поскольку оно существует, вызывается foo::print().

Если бы foo::print() не была найден, компилятор проверил бы содержащее его пространство имен (в данном случае глобальное пространство имен), чтобы увидеть, можно ли найти print() там.

Обратите внимание, что мы также используем оператор разрешения области видимости без пространства имен (::print()) для явного вызова глобальной версии print().

Допускается несколько блоков пространства имен

Допускается объявлять блоки пространства имен в нескольких местах (либо в нескольких файлах, либо в нескольких местах в одном файле). Все объявления в пространстве имен считаются частью одного пространства имен.

circle.h:

#ifndef CIRCLE_H
#define CIRCLE_H
 
namespace basicMath
{
    inline constexpr double pi{ 3.14 };
}
 
#endif

growth.h:

#ifndef GROWTH_H
#define GROWTH_H
 
namespace basicMath
{
    // константа e также является частью пространства имен basicMath
    inline constexpr double e{ 2.7 };
}
 
#endif

main.cpp:

#include "circle.h" // for basicMath::pi
#include "growth.h" // for basicMath::e
 
#include <iostream>
 
int main()
{
    std::cout << basicMath::pi << '\n';
    std::cout << basicMath::e << '\n';
 
    return 0;
}

Это работает именно так, как вы ожидаете:

3.14
2.7

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

Обратите внимание, что эта возможность также означает, что вы можете добавить свои собственные функции в пространство имен std. Это в большинстве случаев приводит к неопределенному поведению, потому что пространство имен std имеет специальное правило, запрещающее расширение из пользовательского кода.

Предупреждение


Не добавляйте пользовательские функции в пространство имен std.

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

add.h

#ifndef ADD_H
#define ADD_H
 
namespace basicMath
{
    // функция add() является частью пространства имен basicMath
    int add(int x, int y);
}
 
#endif

add.cpp

#include "add.h"
 
namespace basicMath
{
    // определяем функцию add()
    int add(int x, int y)
    {
        return x + y;
    }
}

main.cpp

#include "add.h" // для basicMath::add()
 
#include <iostream>
 
int main()
{
    std::cout << basicMath::add(4, 3) << '\n';
 
    return 0;
}

Если пространство имен пропущено в исходном файле, компоновщик не найдет определение basicMath::add, поскольку исходный файл будет определять только add (глобальное пространство имен). Если пространство имен опущено в заголовочном файле, main.cpp не сможет использовать basicMath::add, потому что он видит объявление только для add (глобальное пространство имен).

Вложенные пространства имен

Пространства имен могут быть вложены в другие пространства имен. Например:

#include <iostream>
 
namespace foo
{
    namespace goo // goo - это пространство имен внутри пространства имен foo
    {
        int add(int x, int y)
        {
            return x + y;
        }
    }
}
 
int main()
{
    std::cout << foo::goo::add(1, 2) << '\n';
    return 0;
}

Обратите внимание: поскольку пространство имен goo находится внутри пространства имен foo, мы получаем доступ к add как foo::goo::add.

Начиная с C++17, вложенные пространства имен также могут быть объявлены следующим образом:

#include <iostream>
 
namespace foo::goo // goo - это пространство имен внутри пространства имен foo (стиль C++17)
{
  int add(int x, int y)
  {
    return x + y;
  }
}
 
int main()
{
    std::cout << foo::goo::add(1, 2) << '\n';
    return 0;
}

Псевдонимы пространств имен

Поскольку ввод полного имени переменной или функции во вложенном пространстве имен может быть болезненным, C++ позволяет создавать псевдонимы пространств имен, которые позволяют нам временно сократить длинную последовательность пространств имен до чего-то более короткого:

#include <iostream>
 
namespace foo::goo
{
    int add(int x, int y)
    {
        return x + y;
    }
}
 
int main()
{
    namespace boo = foo::goo; // boo теперь ссылается на foo::goo
 
    std::cout << boo::add(1, 2) << '\n'; // На самом деле это foo::goo::add()
 
    return 0;
} // Здесь прекращается действие псевдонима boo

Одно приятное преимущество псевдонимов пространства имен: если вы когда-нибудь захотите переместить функциональность foo::goo в другое место, вы можете просто обновить псевдоним boo, чтобы отразить новое место назначения, вместо того, чтобы искать/заменять каждый экземпляр foo::goo.

Стоит отметить, что пространства имен в C++ изначально были разработаны не как способ реализации информационной иерархии – они были разработаны, в первую очередь, как механизм предотвращения конфликтов имен. В качестве доказательства этого обратите внимание, что вся стандартная библиотека находится в одном пространстве имен std:: (с некоторыми вложенными пространствами имен, используемыми для новых функций библиотеки). Некоторые новые языки (например, C#) в этом отношении отличаются от C++.

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

Когда следует использовать пространства имен

В приложениях пространства имен могут использоваться для отделения кода, специфичного для приложения, от кода, который может быть повторно использован позже (например, математические функции). Например, физические и математические функции могут входить в одно пространство имен (например, math::). Функции языка и локализации – в другое пространство имен (например, lang::).

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

Теги

C++ / CppLearnCppnamespaceДля начинающихОбучениеПрограммированиеПространство имен

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.