То, что написано ниже, для многих квалифицированных C++ разработчиков будет прекрасно известным и очевидным, но тем не менее, я периодически встречаю using namespace std; в коде различных проектов, а недавно в нашумевшей статье про впечатления от высшего образования было упомянуто, что студентов так учат писать код в вузах, что и сподвигло меня написать эту заметку.
Итак... многие слышали, что using namespace std; в начале файла в C++ считается плохой практикой и нередко даже явно запрещен в принятых во многих проектах стандартах кодирования. Касательно недопустимости использования using namespace в header-файлах вопросов обычно не возникает, если мы хоть немного понимаем, как работает препроцессор компилятора: .hpp-файлы при использовании директивы #include вставляются в код "как есть", и соответственно using автоматически распространится на все затронутые .hpp- и .cpp-файлы, если файл с ним был заинклюден хоть в одном звене цепочки (на одном из сайтов это метко обозвали "заболеванием передающимся половым путем"). Но вот про .cpp-файлы все не так очевидно, так что давайте еще раз разберем, что же именно здесь не так.
Для чего вообще придумали пространства имен в C++? Когда какие-то две сущности (типы, функции, и т.д.) имеют идентификаторы, которые могут конфликтовать друг с другом при совместном использовании, C++ позволяет объявлять пространства с помощью ключевого слова namespace. Всё, что объявлено внутри пространства имен, принадлежит только этому пространству имен (а не глобальному). Используя using мы вытаскиваем сущности какого-либо пространства имен в глобальный контекст.
А теперь посмотрим, к чему это может привести.
Допустим, вы используете две библиотеки под названием Foo и Bar и написали в начале файла что-то типа
using namespace foo;
using namespace bar;
...таким образом вытащив всё, что есть в foo:: и в bar:: в глобальное пространство имен.
Все работает нормально, и вы можете без проблем вызвать Blah() из Foo и Quux() из Bar. Но однажды вы обновляете библиотеку Foo до новой версии Foo 2.0, которая теперь еще имеет в себе функцию Quux().
Теперь у вас конфликт: и Foo 2.0, и Bar импортируют Quux() в ваше глобальное пространство имен. В лучшем случае это вызовет ошибку на этапе компиляции, и исправление этого потребует усилий и времени.
А вот если бы вы явно указывали в коде метод с его пространством имен, а именно, foo::Blah() и bar::Quux(), то добавление foo::Quux() не было бы проблемой.
Но всё может быть даже хуже!
В библиотеку Foo 2.0 могла быть добавлена функция foo::Quux(), про которую компилятор по ряду причин посчитает, что она однозначно лучше подходит для некоторых ваших вызовов Quux(), чем bar::Quux(), вызывавшаяся в вашем коде на протяжении многих лет. Тогда ваш код все равно скомпилируется, но будет молча вызывать неправильную функцию и делать бог весть что. И это может привести к куче неожиданных и сложноотлаживающихся ошибок.
Имейте в виду, что пространство имен std:: имеет множество идентификаторов, многие из которых являются очень распространенными (list, sort, string, iterator, swap), которые, скорее всего, могут появиться и в другом коде, либо наоборот, в следущей версии стандарта C++ в std добавят что-то, что совпадет с каким-то из идентификаторов в вашем существующем коде.
Если вы считаете это маловероятным, то посмотрим на реальные примеры со stackoverflow:
Вот тут был задан вопрос о том, почему код возвращает совершенно не те результаты, что от него ожидает разработчик. По факту там происходит именно описанное выше: разработчик передает в функцию аргументы неправильного типа, но это не вызывает ошибку компиляции, а компилятор просто молча использует вместо объявленной выше функции distance() библиотечную функцию std::distance() из std:: ставшего глобальным неймспейсом.
Второй пример на ту же тему: вместо функции swap() используется std::swap(). Опять же, никакой ошибки компиляции, а просто неправильный результат работы.
Так что подобное происходит гораздо чаще, чем кажется.
P.S. В комментариях еще была упомянута такая штука, как Argument Dependent Lookup, она же Koenig lookup. Почитать подробнее можно на Википедии, но в итоге лекарство от этой проблемы такое же: явное указание пространства имен перед вызовом функций везде, где только можно.
Комментарии (38)
am_i_dead
27.09.2021 16:36ну я не говорил, что нас учат так писать, но сказал, что мы задавали вопрос по этому поводу. и нам ответили "это пространство имен, подробнее объяснят позже". а при решении задач используем, да.
ну а статья полезная, спасибо.
MainEditor0
27.09.2021 19:48Вроде какой-то лектор, кажется Хирьянов, говорил, что для решения олипиадных задач, да и в принципе задач использовать можно, но не в чем-то ином. Кажется так
F0iL Автор
27.09.2021 19:49+10Олимпиадное программирование -- это отдельный мир, и там write-only-код (который один раз написали и больше не будут читать, расширять и поддерживать) встречается на каждом шагу.
Bronsky
28.09.2021 10:34Хирьянов в своих лекциях про c++ и алгоритмы строго-настрого запретил объявлять пространства имён в начале файла, только внутри функций.
0xd34df00d
27.09.2021 17:54+4В библиотеку Foo 2.0 могла быть добавлена функция foo::Quux(), про которую компилятор по ряду причин посчитает, что она однозначно лучше подходит для некоторых ваших вызовов Quux(), чем bar::Quux(), вызывавшаяся в вашем коде на протяжении многих лет. Тогда ваш код все равно компилируется, но он молча вызывает неправильную функцию и делает бог весть что. И это может привести к куче неожиданных и сложноотлаживающихся ошибок.
Интересно, что в C++20 есть изменение (связанное с синтаксисом инициализации и SFINAE), которое может привести ровно к такому же веселью в рантайме.
sergegers
28.09.2021 01:08+3Что имеется ввиду?
0xd34df00d
28.09.2021 19:32+2struct S { int a; int b; }; template<typename T, typename = decltype(T(int {}))> bool isCpp20Impl(int) { return true; } template<typename T> bool isCpp20Impl(...) { return false; } bool isCpp20() { return isCpp20Impl<S>(0); }
isCpp20
вернётtrue
при компиляции с-std=c++20
(или соответствуюшим флагом) иfalse
при более раннем (по крайней мере, с gcc 11, в clang это ещё не запилили). Это следствие вот этого пропозала (который, что иронично, призван чинить другую проблему, связанную с невозможностью сделатьmake_unique
/make_shared
агрегату).
cr0nk
27.09.2021 20:23+3Каждый раз читая документацию по boost невольно начинаешь плевать в монитор из за множественных using и попытками разобраться что к чему относиться
ncr
28.09.2021 02:18+2Но всё может быть даже хуже!
В библиотеку Foo 2.0 могла быть добавлена функция foo::Quux(), про которую компилятор по ряду причин посчитает, что она однозначно лучше подходит для некоторых ваших вызовов Quux(), чем bar::Quux(), вызывавшаяся в вашем коде на протяжении многих лет. Тогда ваш код все равно скомпилируется, но будет молча вызывать неправильную функцию и делать бог весть что. И это может привести к куче неожиданных и сложноотлаживающихся ошибок.
Это было бы прекрасным аргументом, если бы все не было еще хуже: у вас может быть своя собственная функция, принимающая, скажем, std::string, а потом в очередном стандарте в std добавят функцию с таким же именем, и она, внезапно, может подойти лучше вашей и тихо и незаметно вызываться вместо нее.
Поэтому расслабьтесь и пишите так, как вам удобно. Ваш код все равно где-то неизбежно сломается с очередным обновлением компилятора и библиотек.elektroschwein
28.09.2021 09:14+2Немного не понял, как может вызваться функция из std:: вместо моей из анонимного неймспейса или вместо метода самого объекта, если я не декларировал выше using namespace std?
mk2
28.09.2021 09:30+3Вместо метода самого объекта не может, да. А вот вместо твоей из анонимного неймспейса может.
Как кандидата её найдёт благодаря Argument Dependent Lookup (ищет кандидатов в неймспейсе аргумента). А дальше или ошибка компиляции, если функции одинаково подходят, или она может подойти лучше и тихо начать вызываться. Например, если твоя функция на самом деле принимала что-то кастующеется из std::string, а функция std принимает именно string.elektroschwein
28.09.2021 09:54+1А, понял , речь про Koenig lookup. Ну то есть тогда напрашивается тот же вывод: не хочешь проблем -- избегай анонимных неймспейсов, для своих функций тоже сделай неймспейс, и указывай неймспейсы при вызове функций всегда.
pdragon
28.09.2021 09:35+8Читая все это постепенно приходит понимание что c++ это язык страданий и боли.
slonopotamus
28.09.2021 09:39+15Так и есть. В C++ про каждую фичу есть список причин почему ей не надо пользоваться.
elektroschwein
28.09.2021 10:13Прекрасный пример, демонстрирующий что "разработка комитетом" тоже не приводит ни к чему хорошему.
Pipandrya
28.09.2021 12:39Студент первого курса. На очередном уроке языков программирования, написал std::cout, преподаватель же сказал, что я написал неправильно и лучше писать using namespace std, и потом уже cout. Я же переварил эту информацию в голове и подумал, ок буду писать как преподаватель сказал, но сам же буду использовать std:: .
mbait
30.09.2021 23:04Преподавать как-то аргументировал своё мнение? Или "потому что я преподаватель"?
tabfor
28.09.2021 12:39Вообще ноги растут от разных изысков типа шаблонов, которые разрушают стройность объектного программирования с одним из основополагающих его принципов - инкапсуляцией. Я понимаю, что все это должно оптимизировать код по объему и времени выполнения, но чаще всего оно того не стоит из-за элементарной потери читаемости кода.
Kakadout
28.09.2021 16:03А обсуждаемая проблема специфичная для С++? По-моему, нет, так открывать локально пространства имен можно много где: С#, Java, Haskell, OCaml…
DistortNeo
28.09.2021 16:18Да, специфичная для C++ из-за его особенностей по включению заголовочных файлов.
Во всех остальных языках действие аналоговusing namespace
ограничивается самим файлом.elektroschwein
28.09.2021 20:54Вопрос ещё в том, делают ли в других языках компиляторы так же как в C++: при возникновении неопределенности не падают с ошибкой и даже не кидают ворнинг, а молча выбирают то что им больше нравится.
KGeist
29.09.2021 07:27Тем временем в новой версии C# решили, что автоподстановка неявного using System в каждом файле (а ещё System.Http, System.Thread и несколько других неймспейсов) - "отличная" идея
SNVMK
29.09.2021 08:48(можете забить тапками за пайтон под c++ постом, но оставлю это здесь)
В некоторых примерах видел:
from foolib import *
То же самое что using namespace foolib; Часто бесит
Guul
29.09.2021 12:26Использую using namespace в cpp для своих namespace. Чужие - в своих функциях по мере надобности. Предпочитаю использовать using fx = foo::x для посторонних неймспейсов.
Бросьте в меня камень и ссылку на github issue тот кто в реальности столкнулся с больными фантазиями автора(фция исчезла и стала вызываться другая из другой библиотеки) .
- - - - - - -
Ещё знаете как может случиться - библиотека Foo может обновиться и ввести функцию которая подходит лучше чем ваше 10 летнее использование (у Foo::x(int) появился char принимающий брат, например).
Поэтому с сегодняшнего дня каждый раз когда используем функции внешней библиотеки, делаем это создавая на неё явные ссылки
int (&dfoo)(double) = foo;
int (&ifoo)(int) = foo;
Раз уж бороться с мельницами, то по полной!11
F0iL Автор
29.09.2021 13:09Бросьте в меня камень и ссылку на github issue тот кто в реальности столкнулся
Я лично сталкивался в одном из старых проектов, именно поэтому, увидев упоминание на Хабре, и написал эту заметку.
А так, в статье есть как минимум две ссылки на посты от реальных людей, кто тоже с этим столкнулись на практике.
Так что это не фантазия, а объективная реальность, компиляторы действительно делают так, а если лично вы с чем-то не сталкивались, то это не значит, что такого не бывает :)
F0iL Автор
29.09.2021 19:08+1фция исчезла и стала вызываться другая из другой библиотеки
Кстати, вы невнимательно читали. Речь не про "исчезла", а про "добавилась ещё одна с тем же названием".
yeputons
30.09.2021 00:37+2Можно даже при обновлении стандартной библиотеки огрести. Пусть мы пишем свой НОК "в олимпиадном стиле", чтобы работал с `long long`:
#include <bits/stdc++.h> using namespace std; int gcd(int a, int b) { return b ? gcd(b, a % b) : a; } long long lcm(long long a, long long b) { return a / gcd(a, b) * b; } int main() { std::cout << lcm(1'000'000'000, 1'000'000'001) << "\n"; }
Тогда до C++14 включительно эта программа работает: выводит число порядка 10^18. А вот начиная с C++17 случается UB, потому что появилась стандартная функция std::lcm, которая принимает два
int
и по перегрузкам подходит строго лучше, чем наша. Но возвращает-то онаint
, а не long long => получаем переполнение.
DX168B
Не использую конструкцию using namespace. Вообще. Действительно, лучше написать namespace::entity и сразу видно, откуда эта entity взялась.
gudvinr
А потом код состоит из кучи паровозов вроде
std::chrono::system_clock::now()
. Ничего криминального вusing namespace
нет, если серьёзно ограничивать scope, в котором это делается.0xd34df00d
Никто не мешает сделать алиас
namespace chrono = std::chrono;
(правда, он тут не очень поможет) или вообщеusing std::chrono::system_clock
в одной конкретной функции.DistortNeo
Ничто не мешает ввести конструкцию
#using namespace
, область действия которой будет — один файл, но комитет на такое не пойдёт.F0iL Автор
В статье как раз разбираются случаи, когда using namespace имеет область действия только в пределах одного .cpp-файла, и это все равно приводит к проблемам. Так что новая конструкция тут не поможет.
syrslava
Это ещё короткий поезд; с тем же std нередко в одном выражении нужно сослаться на несколько стандартных объектов. Или сделать это в нескольких строках подряд. Вот там рябит в глазах — мало не покажется.
F0iL Автор
Собственно, в статье было уточнено, что речь идет про using namespace в начале файла.
В принципе да, если scope очень узкий (например, не больше одного метода, а сам метод не больше одного экрана), то можно. Хотя предложенный выше вариант с алиасами или вытаскиванием только нужной функции/типа будет и красивее, и безопаснее.