2.8 – Конфликты имен и пространства имен

Добавлено 11 апреля 2021 в 23:51

Допустим, вы впервые едете к другу, и вам дан адрес: Фронт-стрит, 245 в Милл-Сити. Достигнув Милл-Сити, вы открываете свою карту и обнаруживаете, что на самом деле в Милл-Сити есть две разные улицы Фронт-стрит, расположенные на разных концах города! Куда бы вы поехали? Если нет дополнительных подсказок, которые помогли бы вам принять решение (например, вы помните, что его дом находится у реки), вам придется позвонить своему другу и попросить дополнительную информацию. Поскольку это сбивает с толку и неэффективно (особенно для почтальона), в большинстве стран все названия улиц и адреса домов в городе должны быть уникальными.

Точно так же C++ требует, чтобы все идентификаторы были однозначными. Если два идентичных идентификатора вводятся в одну и ту же программу таким образом, что компилятор или компоновщик не может их различить, компилятор или компоновщик выдаст ошибку. Эта ошибка обычно называется конфликтом имен (или коллизией имен).

Пример коллизии имен

a.cpp:

#include <iostream>
 
void myFcn(int x)
{
    std::cout << x;
}

main.cpp:

#include <iostream>
 
void myFcn(int x)
{
    std::cout << 2 * x;
}
 
int main()
{
    return 0;
}

Когда компилятор компилирует эту программу, он независимо компилирует a.cpp и main.cpp, и каждый файл компилируется без проблем.

Однако, когда работает компоновщик, он слинкует все определения в a.cpp и main.cpp вместе и обнаружит конфликтующие определения для функции myFcn. После этого компоновщик прервет работу, выдав ошибку. Обратите внимание, что эта ошибка возникает, даже если myFcn никогда не вызывается!

Большинство конфликтов имен возникают в двух случаях:

  1. Два (или более) определения функции (или глобальной переменной) вводятся в отдельные файлы, которые компилируются в одну программу. Это приведет к ошибке компоновщика, как показано выше.
  2. Два (или более) определения функции (или глобальной переменной) вводятся в один и тот же файл (часто через #include). Это приведет к ошибке компилятора.

По мере того, как программы становятся больше и используют больше идентификаторов, вероятность возникновения конфликта имен значительно возрастает. Хорошей новостью является то, что C++ предоставляет множество механизмов для предотвращения конфликтов имен. Одним из таких механизмов является локальная область видимости, которая не позволяет локальным переменным, определенным внутри функций, конфликтовать друг с другом. Но локальная область видимости не работает для имен функций. Так как же нам уберечь имена функций от конфликта друг с другом?

Что такое пространство имен?

Вернемся на мгновение к нашей аналогии с адресом: наличие двух Фронт-стрит было проблемой только потому, что эти улицы существовали в одном городе. С другой стороны, если бы вам пришлось доставлять почту по двум адресам: один на Фронт-стрит, 209 в Милл-Сити, а другой – на Фронт-стрит, 417 в Джонсвилле, путаницы в том, куда идти, не возникнет. Другими словами, города предоставляют группы, которые позволяют нам устранять неоднозначность адресов, которые в противном случае могли бы конфликтовать друг с другом. Пространства имен действуют так же, как города в этой аналогии.

Пространство имен – это область, которая позволяет вам объявлять имена внутри него с целью устранения неоднозначности. Пространство имен обеспечивает область видимости (называемую областью пространства имен) для имен, объявленных внутри него, что просто означает, что любое имя, объявленное внутри пространства имен, не будет ошибочно принято за идентичные имена в других областях видимости.

Ключевой момент


Имя, объявленное в пространстве имен, не будет ошибочно принято за идентичное имя, объявленное в другой области видимости.

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

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

На следующем уроке мы поговорим о том, как создавать собственные пространства имен.

Глобальное пространство имен

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

В примере в начале урока функции main() и обе версии myFcn() определены внутри глобального пространства имен. Конфликт имен, обнаруженный в этом примере, происходит из-за того, что обе версии myFcn() попадают в глобальное пространство имен, что нарушает правило, согласно которому все имена в пространстве имен должны быть уникальными.

Пространство имен std

Когда C++ был только разработан, все идентификаторы в стандартной библиотеке C++ (включая std::cin и std::cout) были доступны для использования без префикса std:: (они были частью глобального пространства имен). Однако это означало, что любой идентификатор в стандартной библиотеке потенциально может конфликтовать с любым именем, которое вы выбрали для своих собственных идентификаторов (также определенных в глобальном пространстве имен). Код, который работал, мог внезапно получить конфликт имен, когда вы включили через #include новый файл из стандартной библиотеки. Или, что еще хуже, программы, которые будут компилироваться под одной версией C++, могут не компилироваться под будущей версией C++, поскольку новые идентификаторы, введенные в стандартную библиотеку, могут иметь конфликт имен с уже написанным кодом. Таким образом, C++ переместил все функции стандартной библиотеки в пространство имен с именем std (сокращение от «standard», «стандарт»).

Оказывается, имя std::cout на самом деле не std::cout. На самом деле это просто cout, а std – это имя пространства имен, частью которого является идентификатор cout. Поскольку cout определен в пространстве имен std, имя cout не будет конфликтовать с любыми объектами или функциями с именем cout, которые мы создаем в глобальном пространстве имен.

Точно так же при доступе к идентификатору, который определен в пространстве имен (например, std::cout), вам необходимо сообщить компилятору, что мы ищем идентификатор, определенный внутри пространства имен (std).

Ключевой момент


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

Есть несколько способов сделать это.

Явный квалификатор пространства имен std::

Самый простой способ сообщить компилятору, что мы хотим использовать cout из пространства имен std, – это явно использовать префикс std::. Например:

#include <iostream>
 
int main()
{
    // когда мы говорим cout, мы имеем в виду cout, определенный в пространстве имен std
    std::cout << "Hello world!";
    return 0;
}

Символы :: – это оператор, называемый оператором разрешения области видимости. Идентификатор слева от символов :: определяет пространство имен, в котором содержится имя справа от символов ::. Если идентификатор слева от символа :: не указан, предполагается глобальное пространство имен.

Поэтому, когда мы говорим std::cout, мы говорим «cout, который находится в пространстве имен std».

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

Лучшая практика


Используйте явные префиксы пространств имен для доступа к идентификаторам, определенным в этих пространствах имен.

using namespace std (и почему его следует избегать)

Другой способ получить доступ к идентификаторам внутри пространства имен – использовать инструкцию директивы using. Вот наша исходная программа HelloWorld с директивой using:

#include <iostream>

// это директива using, указывающая компилятору проверять пространство имен std
// при разрешении идентификаторов без префикса 
using namespace std;
 
int main()
{
    // cout не имеет префикса, поэтому компилятор проверит, 
    // определен ли cout локально или в пространстве имен std
    cout << "Hello world!";
    return 0;
}

Директива using указывает компилятору проверять указанное пространство имен при попытке разрешить идентификатор, не имеющий префикса пространства имен. Итак, в приведенном выше примере, когда компилятор определяет, что такое идентификатор cout, он проверяет как локально (где он не определен), так и в пространстве имен std (где он будет соответствовать std::cout).

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

Рассмотрим следующую программу:

#include <iostream> // импортирует объявление std::cout
 
using namespace std; // делает std::cout доступным как "cout"
 
int cout() // объявляет нашу собственную функцию "cout"
{
    return 5;
}
 
int main()
{
    cout << "Hello, world!"; // Ошибка компиляции! Какой cout нам здесь нужен? 
                             // Тот, который находится в пространстве имен std или тот,
                             // который мы определили выше?
 
    return 0;
}

Приведенная выше программа не компилируется, потому что теперь компилятор не может определить, нужна нам функция cout, которую мы определили, или cout, которая определена внутри пространства имен std.

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

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


Избегайте использования директив (таких как using namespace std;) в верхней части вашей программы. Они нарушают причину, по которой изначально были добавлены пространства имен.

Подробнее об инструкциях using (и ответственно о том, как их использовать) мы поговорим в уроке «6.12 – Инструкции using».

Теги

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

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

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