То, что написано ниже, для многих квалифицированных 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)


  1. DX168B
    27.09.2021 16:22
    +3

    Не использую конструкцию using namespace. Вообще. Действительно, лучше написать namespace::entity и сразу видно, откуда эта entity взялась.


    1. gudvinr
      27.09.2021 18:36
      +19

      А потом код состоит из кучи паровозов вроде std::chrono::system_clock::now(). Ничего криминального в using namespace нет, если серьёзно ограничивать scope, в котором это делается.


      1. 0xd34df00d
        27.09.2021 18:44
        +16

        Никто не мешает сделать алиас namespace chrono = std::chrono; (правда, он тут не очень поможет) или вообще using std::chrono::system_clock в одной конкретной функции.


        1. DistortNeo
          28.09.2021 12:20

          Ничто не мешает ввести конструкцию #using namespace, область действия которой будет — один файл, но комитет на такое не пойдёт.


          1. F0iL Автор
            29.09.2021 11:06
            +1

            В статье как раз разбираются случаи, когда using namespace имеет область действия только в пределах одного .cpp-файла, и это все равно приводит к проблемам. Так что новая конструкция тут не поможет.


      1. syrslava
        27.09.2021 18:46
        +16

        Это ещё короткий поезд; с тем же std нередко в одном выражении нужно сослаться на несколько стандартных объектов. Или сделать это в нескольких строках подряд. Вот там рябит в глазах — мало не покажется.


      1. F0iL Автор
        27.09.2021 19:48
        +1

        Собственно, в статье было уточнено, что речь идет про using namespace в начале файла.
        В принципе да, если scope очень узкий (например, не больше одного метода, а сам метод не больше одного экрана), то можно. Хотя предложенный выше вариант с алиасами или вытаскиванием только нужной функции/типа будет и красивее, и безопаснее.


  1. am_i_dead
    27.09.2021 16:36

    ну я не говорил, что нас учат так писать, но сказал, что мы задавали вопрос по этому поводу. и нам ответили "это пространство имен, подробнее объяснят позже". а при решении задач используем, да.

    ну а статья полезная, спасибо.


    1. MainEditor0
      27.09.2021 19:48

      Вроде какой-то лектор, кажется Хирьянов, говорил, что для решения олипиадных задач, да и в принципе задач использовать можно, но не в чем-то ином. Кажется так


      1. F0iL Автор
        27.09.2021 19:49
        +10

        Олимпиадное программирование -- это отдельный мир, и там write-only-код (который один раз написали и больше не будут читать, расширять и поддерживать) встречается на каждом шагу.


      1. Bronsky
        28.09.2021 10:34

        Хирьянов в своих лекциях про c++ и алгоритмы строго-настрого запретил объявлять пространства имён в начале файла, только внутри функций.


  1. 0xd34df00d
    27.09.2021 17:54
    +4

    В библиотеку Foo 2.0 могла быть добавлена функция foo::Quux(), про которую компилятор по ряду причин посчитает, что она однозначно лучше подходит для некоторых ваших вызовов Quux(), чем bar::Quux(), вызывавшаяся в вашем коде на протяжении многих лет. Тогда ваш код все равно компилируется, но он молча вызывает неправильную функцию и делает бог весть что. И это может привести к куче неожиданных и сложноотлаживающихся ошибок.

    Интересно, что в C++20 есть изменение (связанное с синтаксисом инициализации и SFINAE), которое может привести ровно к такому же веселью в рантайме.


    1. sergegers
      28.09.2021 01:08
      +3

      Что имеется ввиду?


      1. 0xd34df00d
        28.09.2021 19:32
        +2

        struct 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 агрегату).


  1. cr0nk
    27.09.2021 20:23
    +3

    Каждый раз читая документацию по boost невольно начинаешь плевать в монитор из за множественных using и попытками разобраться что к чему относиться


  1. ncr
    28.09.2021 02:18
    +2

    Но всё может быть даже хуже!
    В библиотеку Foo 2.0 могла быть добавлена функция foo::Quux(), про которую компилятор по ряду причин посчитает, что она однозначно лучше подходит для некоторых ваших вызовов Quux(), чем bar::Quux(), вызывавшаяся в вашем коде на протяжении многих лет. Тогда ваш код все равно скомпилируется, но будет молча вызывать неправильную функцию и делать бог весть что. И это может привести к куче неожиданных и сложноотлаживающихся ошибок.

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

    Поэтому расслабьтесь и пишите так, как вам удобно. Ваш код все равно где-то неизбежно сломается с очередным обновлением компилятора и библиотек.


    1. elektroschwein
      28.09.2021 09:14
      +2

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


      1. mk2
        28.09.2021 09:30
        +3

        Вместо метода самого объекта не может, да. А вот вместо твоей из анонимного неймспейса может.
        Как кандидата её найдёт благодаря Argument Dependent Lookup (ищет кандидатов в неймспейсе аргумента). А дальше или ошибка компиляции, если функции одинаково подходят, или она может подойти лучше и тихо начать вызываться. Например, если твоя функция на самом деле принимала что-то кастующеется из std::string, а функция std принимает именно string.


        1. elektroschwein
          28.09.2021 09:54
          +1

          А, понял , речь про Koenig lookup. Ну то есть тогда напрашивается тот же вывод: не хочешь проблем -- избегай анонимных неймспейсов, для своих функций тоже сделай неймспейс, и указывай неймспейсы при вызове функций всегда.


  1. pdragon
    28.09.2021 09:35
    +8

    Читая все это постепенно приходит понимание что c++ это язык страданий и боли.


    1. slonopotamus
      28.09.2021 09:39
      +15

      Так и есть. В C++ про каждую фичу есть список причин почему ей не надо пользоваться.


    1. elektroschwein
      28.09.2021 10:13

      Прекрасный пример, демонстрирующий что "разработка комитетом" тоже не приводит ни к чему хорошему.


  1. teodorneotov
    28.09.2021 10:34

    Нас в вузе наоборот учили, что использовать namespace std - плохо.


    1. teodorneotov
      29.09.2021 04:42

      *использовать using в начале


  1. Pipandrya
    28.09.2021 12:39

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


    1. mbait
      30.09.2021 23:04

      Преподавать как-то аргументировал своё мнение? Или "потому что я преподаватель"?


      1. DistortNeo
        30.09.2021 23:16
        +1

        Потому что так написано в методичке же.


  1. tabfor
    28.09.2021 12:39

    Вообще ноги растут от разных изысков типа шаблонов, которые разрушают стройность объектного программирования с одним из основополагающих его принципов - инкапсуляцией. Я понимаю, что все это должно оптимизировать код по объему и времени выполнения, но чаще всего оно того не стоит из-за элементарной потери читаемости кода.


  1. Vesh
    28.09.2021 12:39

    У меня код с КДПВ не компилируется


  1. Kakadout
    28.09.2021 16:03

    А обсуждаемая проблема специфичная для С++? По-моему, нет, так открывать локально пространства имен можно много где: С#, Java, Haskell, OCaml…


    1. DistortNeo
      28.09.2021 16:18

      Да, специфичная для C++ из-за его особенностей по включению заголовочных файлов.
      Во всех остальных языках действие аналогов using namespace ограничивается самим файлом.


      1. elektroschwein
        28.09.2021 20:54

        Вопрос ещё в том, делают ли в других языках компиляторы так же как в C++: при возникновении неопределенности не падают с ошибкой и даже не кидают ворнинг, а молча выбирают то что им больше нравится.


  1. KGeist
    29.09.2021 07:27

    Тем временем в новой версии C# решили, что автоподстановка неявного using System в каждом файле (а ещё System.Http, System.Thread и несколько других неймспейсов) - "отличная" идея


  1. SNVMK
    29.09.2021 08:48

    (можете забить тапками за пайтон под c++ постом, но оставлю это здесь)

    В некоторых примерах видел:

    from foolib import *

    То же самое что using namespace foolib; Часто бесит


  1. 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


    1. F0iL Автор
      29.09.2021 13:09

      Бросьте в меня камень и ссылку на github issue тот кто в реальности столкнулся

      Я лично сталкивался в одном из старых проектов, именно поэтому, увидев упоминание на Хабре, и написал эту заметку.

      А так, в статье есть как минимум две ссылки на посты от реальных людей, кто тоже с этим столкнулись на практике.

      Так что это не фантазия, а объективная реальность, компиляторы действительно делают так, а если лично вы с чем-то не сталкивались, то это не значит, что такого не бывает :)


    1. F0iL Автор
      29.09.2021 19:08
      +1

      фция исчезла и стала вызываться другая из другой библиотеки

      Кстати, вы невнимательно читали. Речь не про "исчезла", а про "добавилась ещё одна с тем же названием".


  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 => получаем переполнение.