Поиск имен ( Name lookup )

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

Представим, что есть большой проект, который пишут много людей. И пусть в рамках этого проекта два программиста написали классы с одинаковым именем, например Stack. Один класс Stack будет в одном модуле, другой - в другом модуле. Это могло произойти, так как слов в английском языке конечное число, а различных терминов, использующихся в программировании не так уж и много.

Что произойдет дальше? Если эти классы Stack включены в один файл, то возникнет ошибка на стадии компиляции. Иначе, при линковке проекта возникнет другая проблема: конфликт имен, вследствии нарушения правила одного определения (One Definition Rule, ODR). ODR, в частности, говорит, что у одного имени, внутри программы, не может быть более одного определения.

Даже если у двух классов с одинаклвыми именами все их методы не перекрываются по названиям, все равно есть функции, названия которых совпадают. Это конструктор и деструктор. Если в классе нет явным образом определенных конструкторов и деструктора, то компилятор использует конструктор ( по умолчанию ) и деструктор , определенные неявным способом и в этом случае эти функции являются inline. Если функция inline, то будет только одна копия этой функции на всю программу. То есть, когда линкер встречает две одинаковые функции, он одну из них выбрасывает. И это приводит к тому, что для объекта одного класса вызывается деструктор другого класса.

Эта ошибка происходит при раскрутке стека вызова функций или при завершении программы.

Что же делать? Действительно, разным программистам может понадобиться написать одинаковые классы или использовать функции с одинаковыми названиями. Для решения этой проблемы можно использовать пространства имен ( namespace ).

Программист Вася опишет свой Stack в одном пространстве имен:

namespace vasya {
    class Stack {
        ...
    };
}
    

а программист Петя - в другом:

namespace petya {
    class Stack {
        ...
    };
}
    

Теперь полное имя Васиного стека будет: vasya::Stack;

Соответственно, полное имя Петиного стека будет: petya::Stack;

Использовать namespace vasya удобнее, чем, например, класс с именем Stack_vasya, потому что тогда Васе всегда придется писать Stack_vasya. Тогда как, если Вася будет работать внутри namespace vasya, он сможет обрашаться к стеку просто как Stack.

Замечание: В языке С нет пространств имен, поэтому C-программитсам приодиться использовать имена с различнами префиксами. Например, wxExecute и wxLogTrace из библиотеки wxWidgets.

Пространство имен может быть разделенным ( можно в одном файле создать namespace vasya{}
и в другом файле создать namespace vasya{} )

Пространства имен могут быть вложенными, например:  ru::spb::academy::edward

namespace ru{
    namespace spb{
        namespace academy{
            namespace edward{
                ...
            }
        }
    }
}    
    

Из одного пространства имен можно обратиться к другому. Например, находясь в пространстве имен academy, можно обращаться к пространству имен edward, используя относительное имя: edward или полное имя ru::spb::academy::edward.

Пусть есть еще один namespace edward, находящийся в корне ( т.е вне namespace ru ). Чтобы отличить namespace edward, вложенный в namespace academy, от корневого namespace edward существует обращение к глобальному пространству имен ::. Теперь, если написать ::edward, то произойдет обращение к внешнему пространству имен edward. Та же проблема возникнет, если внутри namespace academy будет создан namespace ru. Теперь, если написать ru::spb::academy::edward, то это будет воспринято как обращение к внутреннему пространству имен ru. Решение: написать ::ru::spb::academy::edward.

Пусть есть еще один набор вложенных пространств имен ru::msk::yandex и мы хотим в namespace edward работать с объектами из пространства имен ru::msk::yandex. Как это сделать? Можно обращаться к каждому, используя полное имя ( например: ru::msk::yandex::Stack ), но это не очень удобно, так как приходится писать длинные обращения. Чтобы избежать необходимость использовать длинные приставки можно воспользоваться директивой using:

using ru::msk::yandex;

Но это тоже не очень хороший способ, потому что таким образом мы загружаем в наш локальный namespace edward все имена из заданного namespace yandex, хотя, в действительности, хотим работать с парой классов. И тот, кто будет дальше использовать наш код, не сможет создавать классы с теми именами, которые есть в namespace yandex.

Тем не менее, директиву using можно использовать для обращения к отдельному имени в пространстве имен. Т.е. можно написать:

using ru::msk::yandex::Stack;

Замечание: Существует мнемоническое правило: не использовать using в заголовочных файлах!

Чтобы не писать все время ru::msk::yandex::Stack;, можно создать синоним ( alias ) для пространства имен:

namespace ya = ru::msk::yandex;

Теперь можно использовать ya::Heap для того, чтобы обращаться к куче, написанной в namespace yandex.

Замечание:

2) Поск имен ( Name lookup )

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

Другой вопрос, что такое имя? Именем может быть:

  1. функция

  2. поле, имя переменной

  3. тип ( класс, структура, typedef, union, enum )

  4. namespace

Общий принцип поиска имен такой:

Допустим, мы вызываем функцию, находясь в некотором пространстве имен. Сначало проверятся, есть ли определение этой функции в данном прострастве имен. Если его нету, происходит переход на уровень выше (в "опоясывающий" namespace). И так далее. Если определение функции найдено, процесс остонавливается и в другие пространства имен переходов не происходит.

Пример 1:

namespace ru{
    namespace spb{
        void f( int ){}
        namespace academy{
            void f( double ){}   
            void g(){ f(3); } // какая функция здесь вызовется?
        }
    }
}    
    

Ответ: функция void f( double ) из namespace academy, т.к. сначала поиск осуществляется в пространстве имен academy, а там такая функция f определена. После этого поиск заканчивается.

Замечание: если бы в namespace academy вместо функции void f( double ) была определена функция f( double, double ) или f была бы переменной, то возникла бы ошибка компиляции, т.к. компилятор сначала ищет только имя ( в данном случае f ). И только после того, как найдены все имена, запускается механизм перегрузки, т.е. подбираетя функция, определение которой подходит лучше всего.

Пример 2:

struct A{
    void f( int a ) {}
};  
struct B : A {
    void f( double b );
};  

B b;
b.f(3); // какая функция f вызовется?
    

Замечание: Ситуация в этом примере аналогична примеру 1, т.к. с каждой структурой и с каждым классом связан одноименный namespace. Классу B соответствует одноименный namespace B. При наследовании получается, что namespace B вложен в namespace A.

Ответ: Т. о. мы находим в пространстве имен B функцию f(double) и дальше не ищем.

Чтобы добавить в namespace B фунцию f(int) используем директиву using:

struct B : A {
    using A::f; 
    void f( double b );
};  
    

В таком случае мы в наш namespace B добавим все имена f, которые находились в namespace A.
Теперь при вызове функции f:    b.f(3);    вызовется f(int), т.к. теперь она подходит лучше ( без преобразования типа ).

Можно вызвать f(int) явно, без использования using:    b.A::f(3);

Замечание: Отдельные пространства имен появились не так уж давно ( в середине 90-х годов ), и пока их не было, использовали пространства имен, связанные с классами и структурами. Поэтому, например, при программировании с использованием технологии CORBA, вместо пространств имен используются структуры.

Замечание: Вообще говоря, можно заменить любой namespace на struct со статическими функциями. Вместо синонима для namespace можно будет использовать typedef.

Какие проблемы порождают namespace и такой поиск имен?

Пример 3:

#include <string>

namespace geom{
    struct Point3{}; // пользовательский тип - трехмерная точка
    Point3 operator+ ( Point3 const& a, Point3 const& b ); // оператор сложения для двух точек
}

using namespace std;
int main(){
    geom::Point3 a,b;
    a = a + b;
    return 0;
}
    

В этом примере для двух точек вызывается фунция operator+ . Как компилятор будет ее искать? Сначала он будет искать определение этого опреатора в функции main, ничего не найдет и выйдет наружу в глобадьный namesoace. Но в глобальное пространство имен уже добавлен std::string и все опреаторы, которые в нем есть, в том числе и operator+. Т. о. компилятор найдет этот оператор и остановится. А нужный нам опрератор находится в namespace geom, и найдется он или нет непонятно. Можно конечно написать:
a = geom::operator+(a, b)   , но это вряд ли то, что мы хотели.

Хорошая новость состоит в том, что этот код работает правильно ( использутеся нужный оператор ), несмотря на то что в нашем пространстве имен есть другие операторы сложения, а оператор сложения для точек находится в пространстве имен geom, которое в main не включено.

Работает это благодаря механизму ADL (Argument Dependent Lookup) или Koenig lookup:
Поиск определения функции, которая вызывается в программе, ведется не только в пространстве имен, где происходит вызов этой функции, но и в том пространстве имен, в котором находятся ее аргументы.

В рассматриваемом примере, поиск имен ведется не только в нашем пространстве имен, но и в пространстве имен geom, потому что a и b из namespace geom.

Т. о. можно сформулировать правило:
Oператоры должны быть объявлены в том же пространстве имен, в котором объявлены их аргументы.

Замечание: Для того чтобы функция была локальной в модуле нужно использовать ключевое слово static. Статические функции имеют внутреннюю линковку ( internal linkage ). Это означает, что когда компоновщик разбирает имена, он устанавливает уникальность функции только внутри модуля. При внешней ликовке функций их имена разбираются на уровне всех модулей. Ключевое слово с можно также применять и к переменным. Если в модуле будет статическая переменная static int i, то эта переменная будет доступна только внутри этого модуля и линковщик не будет проверять, что в другом модуле есть такая переменная. А что делать с классами? Ключевое слово static к классу не напишешь. Тем не менее может возникнуть ситуация, когда в двух разных модулях будут написаны одинаковые классы. Это немного решается тем, что мы пишем в разных пространствах имен, но это каксается не только "больших" и "содержательных" классов, вроде класса Stack или класса Queue, но и "маленьких" классов, таких как функторы. Почти все они имеют одинаковые названия, типа test или functor и заводить на каждый функтор свой namespace очень расточительно.
Для решения этой проблемы придумана такая вещь, как безымянное пространство имен.

namespace{
    class test{
        ...
    };    
}    
    

Это означает, что мы не даем имя пространству имен сами, а его придумывает компилятор. И компилятор придумывает его так, чтобы в двух разных модулях этот namespace назывался по-разному.
После этого можно общаться с функторами, обявленными в безымянном пространстве имен, не указывая это пространство имен.
Безымянное пространство имен - это такой C++ -style замены для ключевого слова static, т. е. функция, объявленная в безымянном namespace имеет те же свойства, что и static-функция. Она индивидуальна для конкретного модуля. То же самое относится к переменным и классам. Разница состоит только в том, что все имена, обявленные в безымянном пространстве имен имеют внешнюю линковку, но так как имени пространства имен никто не знает, то и перекрыться эти имена не могут.