Поиск Кёнига
Авторы: Dyzzet, Данил Диль

Поиск Кёнига в языке C++ — это набор правил для поиска неквалифицированных (без оператора разрешения области видимости ::) имён функций и операторов. По-другому он называется поиском, зависящим от аргументов, по-английски — argument-dependent lookup (ADL), Koenig lookup.

ADL в упрощённой формулировке означает, что при поиске имени функции компилятор будет рассматривать не только то пространство имён, в котором она находится, но ещё и пространства имён, которые содержат типы аргументов этой функции. В отличие от перегрузки функций, ADL — особенность вызова функции, а не её объявления [Дьюхерст07, 96—97].

Причины появления

Герб Саттер пишет [Саттер05, 118]:

«Причина [...] заключается в том, чтобы обеспечить коду, использующему объект x типа X, возможность работать с частью его интерфейса, состоящей из функций, не являющихся членами (например, инструкция cout << x использует оператор operator<<, который не является членом класса X) так же легко, как и функции-члены (например, вызов x.f() не требует выполнения специального поиска, поскольку очевидно, что поиск f выполняется в области видимости X)».

Попробуем разобраться. Далее представлен исчерпывающий пример [Саттер03, 243] [Дьюхерст07, 97]:

#include <iostream>
#include <string>   // Здесь объявлена функция
 
int main()
{
    std::string hello = "Hello, world";
    std::cout << hello; // Вызывается std::operator<<
}

Если бы поиск Кёнига не работал, пришлось бы писать std::operator<<(std::cout, hello) (теряется смысл в перегрузке оператора) либо using std::operator<<, либо using namespace std.

Другой полезный пример использования ADL — прочие инфиксные операторы (типа x + x, что эквивалентно вызову operator+(x, x)) [Дьюхерст07, 97].

На примере

Есть пространство имён Cats, в нём класс Cat.

namespace Cats
{
    class Cat
    {
        std::string name;
        Breed breed;
        int weight;
    };
//

И функция pet() находится внутри пространства имён Cats. Погладим кота.

void pet(Cat& cat)
{
    std::cout << __FUNCTION__ << std::endl;
}

В функции main() пишем следующие строчки. И… магия!

Cats::Cat cat;
pet(cat);

Для функции pet() явно не указано пространство имён. Но почему это работает? В функцию передаётся аргумент, пространство имён которого уже известно. По этому пространству имён ADL ищет функцию pet().

Добавим в пространство имён Cats структуру Food со вложенным перечислением и структурой.

struct Food
{
    enum class Type
    {
        Meat,
        Fish,
        Milk,
        VitaminsAndMinerals
    } type;
 
    struct NutritionFacts
    {
        int calories;
        int protein;
        int carbohydrates;
        int fat;
    } nutritionFacts;
};

Добавим оператор в это же пространство имён.

void operator+=(Cat& cat, Food& food)
{
    std::cout << __FUNCTION__ << std::endl;
}

Попробуем использовать его. Покормим кота.

Cats::Food food;
cat += food;

Всё отработает без ошибок: food и cat оба относятся к одному пространству имён. ADL ищет оператор в нём. Это самый важный случай использования ADL.

ADL срабатывает и в том случае, когда аргумент имеет тип перечисления (enum).

void breedInfo(Breed breed)
{
    std::cout << __FUNCTION__ << std::endl;
}
breedInfo(Cats::Breed::Birman); // Cats::breedInfo

То же справедливо для массивов, указателей (подобно ссылкам в примере выше), объединений (union), структур, классов, охватываемых другими классами.

void nutritionInfo(Food::NutritionFacts& nutritionFacts)
{
    std::cout << __FUNCTION__ << std::endl;
}
Cats::Food::NutritionFacts nutritionFacts;
nutritionInfo(nutritionFacts); // Cats::nutritionInfo

Советы

В книге «Стандарты программирования на C++» Герб Саттер и Андрей Александреску [Саттер05, 118, 120, 136] дают три рекомендации, которые касаются ADL. В первой рекомендации они призывают использовать этот механизм.

«Функции, не являющиеся членами и разработанные как часть интерфейса класса X (в особенности операторы и вспомогательные функции), должны быть определены в том же пространстве имён, что и X, что обеспечивает их корректный вызов».

В то же самое время авторы предостерегают от случайного эффекта.

«Оберегайте ваши типы от непреднамеренного поиска, зависящего от аргументов; однако преднамеренный поиск должен завершаться успешно. Этого можно добиться путём размещения типов в своих собственных пространствах имён (вместе с непосредственно связанными с ними свободными функциями). Избегайте помещения типов в те же пространства имён, что и шаблоны функций и операторов».

namespace Cats
{
    template <typename T>
    intoperator+(T, unsigned)
    {
        std::cout << __FUNCTION__ << std::endl;
    }
}
 
int main()
{
    std::vector<Cats::Cat> cats(5);
    cats[0];
}

Данная практика чревата появлением ошибок, которые трудно обнаружить. Почему? В стандартной библиотеке оператор [] может внутри содержать v.begin() + n, а может быть устроен по-другому, всё зависит от реализации. Реализация vector<T>::iterator может дойти до пространства имен Cats, а может и не дойти (то есть перегруженный оператор может быть вызван, а может и нет). В итоге получается непредсказуемый результат.

Авторы говорят [Саттер05, 121]: «Да, стандартная библиотека C++ помещает алгоритмы и другие шаблоны функций [...] в то же пространство имён, что и множество типов [...]. К счастью, теперь у нас больше опыта и мы знаем, как следует поступать. Не делайте так, как сделано в стандартной библиотеке».

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

* * *

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

В заключение нужно сказать, что имя Эндрю Кёнига не раз упоминается в книге создателя языка Бьерна Страуструпа «Дизайн и эволюция C++» [Страуструп06, 56, 92, 112, 136, 139, 155, 178, 215, 234, 236, 246, 251, 258, 260, 269, 309, 328, 333, 345, 349, 357, 373, 387, 390, 398, 400, 401, 404], он повлиял на многие аспекты языка и сам написал большое количество работ по языку C++.

Ссылки

Документация на сайте CppReference.com.

Приложение: исходный код.

Список литературы

[Саттер03] Саттер Г. Решение сложных задач на C++. Серия C++ In-Depth, т. 4.: Пер. с англ. — М.: Издательский дом «Вильямс», 2003. — 400 с.: ил. Парал. тит. англ. ISBN 5-8459-0352-1

[Саттер05] Саттер Г., Александреску А. Стандарты программирования на C++: Пер. с англ. — М.: Издательский дом «Вильямс», 2005. — 224 с.: ил. — Парал. тит. англ. ISBN 5-8459-0859-0

[Дьюхерст07] Дьюхерст С. C++. Священные знания. — Пер. с англ. — СПб.: Символ-Плюс, 2007. — 240 с., ил. ISBN-13: 978-5-93286-095-3, ISBN-10: 5-93286-095-2

[Страуструп06] Страуструп Б. Дизайн и эволюция C++: Пер. с англ. — М.: ДМК Пресс; СПб: Питер, 2006. — 448 с.: ил. ISBN 5-469-01217-4

19 июля
© MMXIMMXX. RSS
Светлая тема / тёмная тема