Пространства имен (namespace)

Практический курс по C/C++: https://stepik.org/course/193691

На этом занятии мы с вами познакомимся с новой концепцией языка С++ пространством имен. Из курса по языку Си мы знаем, что в глобальной области видимости можно объявлять функции, переменные, структуры и так далее:

void foo()
{
    std::cout << "function: foo()" << std::endl;
}
 
int global_a = 5;
 
struct point {
    double x, y;
};

Так вот, в языке С++ все подобные объявления попадают в так называемое глобальное пространство имен. В частности, это означает, что внутри функции main() мы можем обращаться к этим определениям следующим образом:

int main()
{
    point pt {};
 
    foo();
    std::cout << global_a << std::endl;
 
    return 0;
}

или так:

int main()
{
    ::point pt {};
 
    ::foo();
    std::cout << ::global_a << std::endl;
 
    return 0;
}

Обратите внимание на два двоеточия (программисты еще иногда их называют «четвероточием»). Формально, это символ раскрытия области видимости. Если перед ним (слева) не указана какая-либо область видимости, то подразумевается глобальная область (глобальное пространство имен).

Что нам дает этот символ? Смотрите, если, к примеру, внутри функции main() объявить еще одну переменную с именем global_a:

int global_a = 10;

то мы можем их различать с помощью «четвероточия»:

std::cout << ::global_a << " " << global_a << std::endl;

Увидим значения 5 и 10. Поэтому, если мы хотим быть уверены, что обращаемся к глобальной переменной, то перед ее именем достаточно прописать два двоеточия. В ряде случаев это бывает очень полезно.

Далее, из курса по Си мы знаем, что если в глобальном пространстве имен определить два определения с одинаковыми именами, например, две функции с именем foo:

void foo() ...
void foo() ...

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

Или же мы написали свою библиотеку функций или классов, подключаем ее к проекту, где используются другие стандартные библиотеки, и видим тот же конфликт имен. Хотя при разработке своего отдельного модуля мы с этим никак не сталкивались. И подобных ситуаций может быть множество. Как же этого избежать? Для в С++ существует возможность создавать свои пространства имен, которые позволяют разделять один набор определений от другого набора. Для этого используется ключевое слово namespace, после которого указывается название нового или существующего пространства. Например, так:

namespace firstSpace {
    void foo()
    {
        std::cout << "function from firstSpace: foo()" << std::endl;
    }
}
 
void foo()
{
    std::cout << "function: foo()" << std::endl;
}

Здесь название firstSpace мы придумываем сами подобно именам переменных и функций, а, затем, в фигурных скобках следует содержимое этого пространства. В нашем примере там располагается функция с именем foo. Теперь программа будет компилироваться без проблем, так как функции foo() находятся в разных пространствах имен. Но давайте вторую функцию тоже поместим в отдельное пространство с именем secondSpace:

namespace secondSpace {
    void foo()
    {
        std::cout << "function from secondSpace: foo()" << std::endl;
    }
}

Отлично, но как теперь следует вызывать эти функции? Делается это следующим образом:

firstSpace::foo();

То есть, сначала указывается название пространства имен, а затем, через два двоеточия элемент этого пространства. В данном случае вызывается функция foo().

Аналогично можно вызвать функцию foo() из другого пространства:

secondSpace::foo();

После запуска программы видим, что были вызваны две разные функции foo() из соответствующих пространств имен. Как видите, все достаточно просто.

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

namespace firstSpace {
    void foo()
    {
        std::cout << "function from firstSpace: foo()" << std::endl;
    }
}
 
namespace firstSpace {
    struct point {
        double x, y;
    };
}

В результате будет создано пространство firstSpace с функцией foo и структурой point. Это эквивалентно тому, если бы мы их прописали в одном определении firstSpace:

namespace firstSpace {
    void foo()
    {
        std::cout << "function from firstSpace: foo()" << std::endl;
    }
 
    struct point {
        double x, y;
    };
}

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

Следующее важное замечание касается вложенных пространств имен. Например, так:

namespace secondSpace {
    namespace functions {
        void foo()
        {
            std::cout << "function from secondSpace: foo()" << std::endl;
        }
    }
 
    namespace params {
        int global_a = 5;
    }
}

Соответственно, обращение к элементам такого пространства будет выглядеть следующим образом:

secondSpace::functions::foo();
std::cout << secondSpace::params::global_a << std::endl;

И последний штрих. Если перед именем вложенного пространства прописать ключевое слово inline, например, так:

namespace secondSpace {
    inline namespace functions {
        void foo()
        {
            std::cout << "function from secondSpace: foo()" << std::endl;
        }
    }
    ...
}

то доступ к функции foo() можно делать следующим образом:

secondSpace::functions::foo();
secondSpace::foo();

То есть, имя inline-пространства указывать не обязательно.

На этом мы с вами завершим текущее занятие, а на следующем рассмотрим оператор using, который позволяет включать все или отдельные определения одного пространства имен в другое.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме