Перейти к содержимому

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

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

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

Я вовсю использовал пространство имен std в примерах. std — пространство имен из стандартной библиотеки, но и свое создать несложно.

#include <iostream>
// Определяем пространство имен Demo.
namespace Demo
{
// Помещаем функцию внутрь Demo.
void f()
{
std::cout << "Hello from Demo::f" << std::endl;
}
}
int main()
{
// Доступ к функции можно получить через оператор разрешения области видимости.
// Это аналогично использованию `.` для доступа к полям структуры.
Demo::f();
return 0;
}

Заметьте, это практически то же самое, что статическая функция в объявлении типа. Так получается потому, что объявления типов могут работать как пространства имен.

#include <iostream>
struct Demo
{
static void f()
{
std::cout << "Hello from Demo::f" << std::endl;
}
};
int main()
{
Demo::f();
return 0;
}

Пространства имен можно вкладывать друг в друга.

namespace Demo
{
namespace Demo1
{
void f()
{
}
}
}
int main()
{
Demo::Demo1::f();
return 0;
}

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

namespace Impl1
{
void f()
{
std::cout << "Hello";
}
}
namespace Impl2
{
// Несмотря на то же имя, ошибки линковки не будет.
void f()
{
std::cout << "World";
}
}
int main()
{
Impl1::f(); // Hello
Impl2::f(); // World
return 0;
}

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

namespace Demo
{
void f()
{
}
}
namespace Dima
{
void g()
{
}
}
// Обозначает то же пространство имен, что и Demo выше.
namespace Demo
{
// Это пространство имен — не то же самое, что Dima выше.
// Это пространство имен на самом деле называется Demo::Dima.
namespace Dima
{
// Полностью допустимо, ошибки времени компиляции нет
void g()
{
}
}
// Тоже допустимо, ошибки времени компиляции нет.
void g()
{
}
// Дублирующееся определение, ошибка времени компиляции.
// Функция `f()` уже есть выше.
void f()
{
}
}

А значит, определять пространство имен std в своем коде можно совершенно спокойно. Для некоторых вещей это даже требуется, например для определения хеш-функции для типа.

namespace std
{
void f(){}
}
int main()
{
std::f();
return 0;
}

🤓 технически дописывать в std произвольные сущности — неопределенное поведение, так делать не стоит. Правило защищает стандартную библиотеку: реализация вправе считать, что контролирует все содержимое std, и объявлять там что-то свое. Если ваша std::f когда-нибудь столкнется с внутренней f из заголовков библиотеки, компилятор не обязан даже предупредить. (Определять то, что уже существует, — и так очевидная ошибка.) Пример выше считайте упрощением.

Заметьте, main должен находиться вне любых пространств имен, чтобы играть особую роль точки входа.

Одно из явно разрешенных стандартом исключений — специализация std::hash для вашего собственного типа: без нее ваш тип нельзя положить в std::unordered_set или использовать как ключ std::unordered_map.

#include <cstddef>
#include <functional>
#include <iostream>
#include <string>
struct Person
{
std::string name;
int age;
};
namespace std
{
// Разрешенное стандартом расширение:
// специализация `std::hash` для вашего собственного типа.
template <>
struct hash<Person>
{
size_t operator()(const Person& person) const
{
return std::hash<std::string>{}(person.name)
^ (std::hash<int>{}(person.age) << 1);
}
};
}
int main()
{
Person person{"John", 20};
std::cout << std::hash<Person>{}(person) << std::endl;
return 0;
}

Заметьте, main должен находиться вне любых пространств имен, чтобы играть особую роль точки входа.

Пространства имен позволяют использовать сущности без уточнения

Заголовок раздела «Пространства имен позволяют использовать сущности без уточнения»
namespace Demo
{
void f()
{
}
void g()
{
// Можно использовать `f` без уточнения.
f();
// При желании можно было бы использовать `Demo::f`.
Demo::f();
}
}

Это аналогично тому, что внутри тела метода не нужно писать this-> при обращении к полям или методам.

Кстати, то же самое относится к областям имен, создаваемым struct или class.

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

namespace Outer
{
void f()
{
}
void g()
{
}
namespace Inner
{
// Переопределить `g` во вложенном пространстве имен можно.
void g()
{
// Вызывает `Outer::f()`
f();
}
}
}

Не обязательно вкладывать каждое пространство имен по отдельности. В определении пространства имен можно использовать ::. Следующий код:

namespace Outer
{
namespace Inner
{
void f()
{
}
}
}

эквивалентен:

namespace Outer::Inner
{
void f()
{
}
}

Глобальная область видимости — это область вне любых пространств имен. У нее нет имени. Она помогает устранять неоднозначность в некоторых нишевых ситуациях.

namespace Demo
{
void f()
{
std::cout << "Outer";
}
namespace Dima
{
namespace Demo
{
void f()
{
std::cout << "Inner";
}
}
void g()
{
// Мы хотим вызвать `Demo::f` из глобальной области видимости.
// Однако простой вызов `Demo::f` вызовет `f` из внутреннего `Demo`.
Demo::f(); // Inner
// Чтобы вызвать внешний `Demo::f`, придется воспользоваться глобальной областью видимости.
::Demo::f(); // Outer
// Внутренний вызов можно записать так:
::Demo::Dima::Demo::f(); // Inner
}
}
}

using namespace — довольно крутая штука. Она втягивает все содержимое пространства имен в текущую область видимости. «Втягивает» — значит делает все члены пространства имен видимыми, то есть доступными без уточнения. Самое классное: это можно делать почти в любой области видимости — в отдельных функциях, во вложенных обычных областях, в пространствах имен. Исключение — области видимости классов: внутри класса using-директива запрещена, там используйте квалифицированные имена или using-объявления.

#include <iostream>
int main()
{
{
// Делаем всех членов std видимыми здесь без уточнения.
using namespace std;
cout << "Hello" << endl;
}
{
// Но не здесь.
cout << "World" << endl; // ошибка времени компиляции
}
}

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

Покажу это на примере поспокойнее, чтобы вы уловили идею.

#include <iostream>
namespace Demo
{
void f(int a)
{
std::cout << "Demo::f -- " << a << std::endl;
}
}
// Мы спокойно объявляем вторую функцию, принимающую вместо этого float,
// и ожидаем, что вызовется она, ведь о функции в Demo мы не знаем.
void f(float a)
{
std::cout << "Happy life" << std::endl;
}
int main()
{
using namespace Demo;
// Угадайте, какая функция вызовется здесь?
f(5);
return 0;
}

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

Применить using namespace к типам нельзя.

#include <iostream>
struct T
{
int value;
static void f()
{
std::cout << "T::f" << std::endl;
}
};
int main()
{
// Не компилируется
using namespace T;
f();
return 0;
}

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

namespace A
{
void f()
{
std::cout << "A";
}
}
namespace B
{
void f()
{
std::cout << "B";
}
}
int main()
{
using namespace A;
using namespace B;
// Какую вызывать, брат? A::f или B::f?
f();
// Придется устранить неоднозначность, уточнив имя.
A::f(); // A
B::f(); // B
}

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

namespace A
{
void f()
{
std::cout << "Hello";
}
}
void f()
{
std::cout << "World";
}
int main()
{
using namespace A;
f(); // бессмыслица!
A::f(); // Hello
::f(); // World
return 0;
}

Еще одно очень полезное применение using — дать типу другое имя. Это во многом эквивалентно typedef, но using чуть интуитивнее, поэтому рекомендуется использовать using вместо typedef.

С шаблонами typedef не работает, а using — да.

namespace Long::Namespace::Name
{
struct Outer
{
struct Inner
{
struct Target
{
int x;
}
}
}
}
int main()
{
using T = Long::Namespace::Name::Outer::Inner::Target;
// то же, что
// typedef Long::Namespace::Name::Outer::Inner::Target T;
T t{15};
std::cout << t.x; // 15
return 0;
}
enum class Color
{
Red = 'r',
Green = 'g',
Blue = 'b',
};
int main()
{
using C = Color;
C color = C::Red;
switch (color)
{
case C::Red:
std::cout << "Red";
break;
case C::Green:
std::cout << "Green";
break;
case C::Blue:
std::cout << "Blue";
break;
}
return 0;
}

Для пространств имен для того же есть синтаксис namespace X =.

namespace Hello::World::Long::Namespace::Very::Long
{
void func()
{
}
}
int main()
{
namespace NS = Hello::World::Long::Namespace::Very::Long;
NS::func();
}