6.12 – Объявления using и директивы using
Вы, наверное, видели эту программу во многих учебниках и учебных руководствах:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello world!";
return 0;
}
Некоторые старые компиляторы также начинают новые проекты с аналогичной программы.
Если вы это видите, бегите. Возможно, ваш учебник, руководство или компилятор устарели. В этом уроке мы выясним, почему.
Краткий урок истории
Еще до того, как C++ получил поддержку пространств имен, все имена, которые сейчас присутствуют в пространстве имен std
, были в глобальном пространстве имен. Это вызывало конфликты имен между идентификаторами программ и идентификаторами стандартной библиотеки. Программы, которые работали с одной версией C++, могли получить конфликт имен с более новой версией C++.
В 1995 году пространства имен были стандартизированы, и все функции стандартной библиотеки были перенесены из глобального пространства имен в пространство имен std
. Это изменение сломало старый код, в котором всё еще использовались имена без std::
.
Любой, кто работал с большой кодовой базой, знает, что любое изменение кодовой базы (каким бы тривиальным оно ни было) может нарушить работу программы. Обновление каждого имени, которое теперь было перемещено в пространство имен std
для использования префикса std::
, было огромным риском. Было запрошено решение.
Перенесемся в сегодняшний день – если вы много используете стандартную библиотеку, набирание std::
перед всем, что вы используете из стандартной библиотеки, может стать повторяющейся рутиной, а в некоторых случаях может затруднить чтение вашего кода.
C++ предлагает решение обеих этих проблем в виде инструкций using
.
Но сначала давайте определим два термина.
Полные и неполные имена
Имя может быть полным или неполным.
Полное имя – это имя, которое включает в себя связанную область видимости. Чаще всего имена дополняются пространством имен с помощью оператора разрешения области видимости (::
). Например:
std::cout // идентификатор cout уточняется с помощью пространства имен std
::foo // идентификатор foo уточняется с помощью глобального пространства имен
Для продвинутых читателей
Имя также может быть уточнено именем класса с помощью оператора разрешения области (::
) или объектом класса с помощью операторов выбора члена (.
или ->
). Например:
class C; // какой-то класс
C::s_member; // s_member уточняется с помощью класса C
obj.x; // x уточняется с помощью объекта класса obj
ptr->y; // y уточняется с помощью указателя на объект класса ptr
Неполное имя – это имя, которое не включает в себя квалификатор области видимости. Например, cout
и x
являются неполными именами, поскольку они не включают связанную область видимости.
Объявления using
Один из способов уменьшить повторение ввода std::
снова и снова – использовать инструкцию объявления using
. Объявление using
позволяет нам использовать неполное имя (без области видимости) в качестве псевдонима для полного имени.
Вот наша базовая программа Hello world, в которой используется объявление using
в строке 5:
#include <iostream>
int main()
{
using std::cout; // это объявление using сообщает компилятору,
// что cout должен вычисляться как std::cout
cout << "Hello world!"; // поэтому префикс std:: здесь не нужен!
return 0;
} // здесь истекает срок действия объявления using
Объявление using
с использованием std::cout
; сообщает компилятору, что мы собираемся использовать объект cout
из пространства имен std
. Поэтому всякий раз, когда он видит cout
, он предполагает, что мы имеем в виду std::cout
. Если существует конфликт имен между std::cout
и каким-либо другим использованием cout
, предпочтительнее использовать std::cout
. Поэтому в строке 6 мы можем ввести cout
вместо std::cout
.
В этом тривиальном примере это не сэкономит много усилий, но если вы много раз используете cout
внутри функции, объявление using
может сделать ваш код более читабельным. Обратите внимание, что вам потребуется отдельное объявление using
для каждого имени (например, одно для std::cout
, одно для std::cin
и т.д.).
Хотя этот метод менее явный, чем использование префикса std::
, он обычно считается безопасным и приемлемым (при использовании внутри функции).
Директивы using
Другой способ упростить эту задачу – использовать директиву using
. Директива using
импортирует все идентификаторы из пространства имен в область видимости директивы using
.
Вот снова наша программа Hello World с директивой using
в строке 5:
#include <iostream>
int main()
{
using namespace std; // эта директива using указывает компилятору импортировать
// все без уточнения имена из пространства имен std в
// текущее пространство имен
cout << "Hello world!"; // поэтому префикс std:: здесь не нужен
return 0;
}
Директива using namespace std;
указывает компилятору импортировать все имена из пространства имен std
в текущую область видимости (в данном случае в функцию main()
). Затем, когда мы используем неполный идентификатор cout
, он будет преобразован в импортированный std::cout
.
Директивы using
– это решение, которое было предоставлено для баз старого кода до пространства имен, которые использовали неполные имена для функций стандартной библиотеки. Вместо того, чтобы вручную обновлять каждое неполное имя до полного имени (что было рискованно), можно было бы разместить одну директиву using
(т.е. using namespace std;
) в верхней части каждого файла, и все имена, которые были перемещены в пространство имен std
всё еще можно было использовать неполными.
Проблемы с директивами using
Однако для современного кода C++ директивы using
дают немного выгоды (экономия на вводе текста) по сравнению с риском.
Поскольку директивы using
импортируют все имена из пространства имен (потенциально включая множество имен, которые вы никогда не будете использовать), вероятность возникновения конфликтов имен значительно возрастает (особенно, если вы импортируете пространство имен std
).
Для наглядности рассмотрим пример, в котором директивы using
вызывают неоднозначность:
#include <iostream>
namespace a
{
int x{ 10 };
}
namespace b
{
int x{ 20 };
}
int main()
{
using namespace a;
using namespace b;
std::cout << x << '\n';
return 0;
}
В приведенном выше примере компилятор не может определить, относится ли x
в main
к a::x
или b::x
. В этом случае код не будет скомпилирован из-за ошибки «ambiguous symbol» (неоднозначный символ). Мы могли бы решить эту проблему, удалив одну из инструкций using
, используя вместо нее объявление using
или определив x
с явным квалификатором области видимости (a::
или b::
).
Вот еще один более коварный пример:
#include <iostream> // импортирует объявление std::cout
int cout() // объявляет нашу собственную функцию "cout"
{
return 5;
}
int main()
{
using namespace std; // делает std::cout доступным как "cout"
cout << "Hello, world!"; // упс! Какой cout нам нужен здесь?
// Тот, который находится в пространстве имен std или тот,
// который мы определили выше?
return 0;
}
В приведенном выше примере компилятор не может определить, наше использование cout
означает std::cout
или функцию cout
, которую мы определили, и снова не сможет выполнить компиляцию из-за ошибки «неоднозначного символа». Хотя этот пример тривиален, если бы у нас был явный префикс std::cout
, например:
std::cout << "Hello, world!"; // сообщаем компилятору, что мы имеем в виду std::cout
или использовалось объявление using
вместо директивы using
:
using std::cout; // сообщаем компилятору, что cout означает std::cout
cout << "Hello, world!"; // поэтому это означает std::cout
тогда наша программа вообще не имела бы никаких проблем.
Даже если директивы using
не вызывают конфликтов имен сегодня, они делают код более уязвимым для будущих конфликтов. Например, если ваш код включает директиву using
для какой-либо библиотеки, которая затем обновляется, все новые имена, представленные в обновленной библиотеке, теперь являются кандидатами на конфликты имен с вашим существующим кодом.
Может возникнуть и более коварная проблема. В обновленной библиотеке может появиться функция, которая не только имеет то же имя, но и лучше подходит для некоторых вызовов функций. В таком случае компилятор может решить отдать предпочтение новой функции, и поведение вашей программы неожиданно изменится.
Рассмотрим следующую программу:
foolib.h:
namespace Foo
{
// притворимся, что здесь есть полезный код
}
main.cpp:
#include <iostream>
#include <foolib.h>
int someFcn(double)
{
return 1;
}
int main()
{
using namespace Foo; // Потому что мы ленивы и хотим получить доступ к именам
// с квалификатором Foo:: без ввода префикса Foo::
std::cout << someFcn(0); // Литерал 0 должен быть 0.0, но эту ошибку легко допустить
return 0;
}
Эта программа запускается и печатает 1.
Теперь предположим, что мы обновляем библиотеку foolib, которая включает обновленный foolib.h. Наша программа теперь выглядит так:
foolib.h:
namespace Foo
{
// добавленная недавно функция
int someFcn(int)
{
return 2;
}
// притворимся, что здесь есть полезный код
}
main.cpp:
#include <iostream>
#include <foolib.h>
int someFcn(double)
{
return 1;
}
int main()
{
using namespace Foo; // Потому что мы ленивы и хотим получить доступ к именам
// с квалификатором Foo:: без ввода префикса Foo::
std::cout << someFcn(0); // Литерал 0 должен быть 0.0, но эту ошибку легко допустить
return 0;
}
Наш файл main.cpp вообще не изменился, но теперь эта программа запускается и выводит 2!
Когда компилятор встречает вызов функции, он должен определить, с каким определением функции он должен сопоставить этот вызов. При выборе функции из набора потенциально совпадающих функций он предпочтет ту функцию, которая не требует преобразования аргументов, а не ту, которая требует преобразования аргументов. Поскольку литерал 0 принадлежит целочисленному типу, C++ предпочтет сопоставить someFcn(0)
с недавно добавленной someFcn(int)
(без преобразований), а не с someFcn(double)
(требуется преобразование из int в double). Это вызывает неожиданное изменение результатов работы нашей программы.
Этого бы не произошло, если бы мы использовали объявление using
или явный квалификатор области видимости.
Область видимости объявлений и директив using
Если объявление using
или директива using
используется в блоке, имена применимы только в этом блоке (они следуют обычным правилам области видимости блока). Это хорошо, поскольку снижает вероятность возникновения конфликтов имен внутри этого блока.
Если объявление using
или директива using
используются в глобальном пространстве имен, имена применимы ко всему остальному файлу (они имеют область видимости файла).
Отмена или замена инструкции using
После объявления инструкции using
ее невозможно отменить или заменить ее другой инструкцией using
в той области видимости, в которой она была объявлена.
int main()
{
using namespace Foo;
// здесь нет способа отменить "using namespace Foo"!
// также нет возможности заменить "using namespace Foo" другой инструкцией using
return 0;
} // "using namespace Foo" здесь прекращает свое действие
Лучшее, что вы можете сделать, – это намеренно с самого начала ограничить область видимости инструкции using
с помощью правил области видимости блока.
int main()
{
{
using namespace Foo;
// здесь вызовы функций из Foo::
} // "using namespace Foo" прекращает свое действие
{
using namespace Goo;
// здесь вызовы функций из Goo::
} // "using namespace Goo" прекращает свое действие
return 0;
}
Конечно, всей этой головной боли можно избежать, в первую очередь, используя явным образом оператор разрешения области видимости (::
).
Лучшие практики для инструкций using
В современном C++ на самом деле нет места для использования директив. Они увеличивают вероятность коллизий имен сейчас и в будущем и могут вызывать более коварные проблемы. Хотя во многих учебниках и руководствах они широко используются, использования директив лучше вообще избегать.
Объявления using
обычно считаются безопасными для использования внутри блоков. Ограничьте их использование в глобальном пространстве имен файлов исходного кода и никогда не используйте их в глобальном пространстве имен заголовочных файлов.
Лучшая практика
Предпочитайте явное указание пространств имен вместо инструкций using
. Полностью избегайте директив using
. Внутри блоков допускается использование объявлений using
.