Argument-dependent lookup в C++

Как-то со всеми этими новыми модными штучками типа концептов и constexpr стали забываться базовые вещи из C++ и вся та жесть, которая может быть с ними связана. Вот сейчас нарвался на баг, связанный с со старым добрым Argument-dependent lookup, и задумался о том, а как вообще писать безопасный с точки зрения ADL код в библиотеках.

Допустим вы реализуете какую-то библиотеку lib1, в которой содержатся шаблонные функции для работы с произвольными определёнными пользователем типами данных. Среди них в реализации есть какая-нибудь мелкая функция, например calc_size:

namespace lib1 {
    template <typename T>
    int calc_size(const T & v) {
        // calculating and returning size using methods of v
    }

    template <typename T>
    void my_public_func(const T & v) {
        // ...
        int sz = calc_size(v);
        // ...
    }
}

Казалось бы, всё находится в отдельном пространстве имен lib1, и никаких проблем быть не должно. Но не тут то было. Теперь представим, что у вас есть библиотека lib2, в которой определён класс cls, и в заголовочном файле в реализации библиотеки есть вспомогательная функция с тем же самым именем:

namespace lib2 {
    class cls {
        // ...
    };

    inline int calc_size(const cls & v) {
        // ...
    }
}

Теперь если передать в функцию lib1::my_public_func параметр типа cls, то вызовется не lib1::calc_size, а lib2::calc_size. Всё, можно идти и отлаживать баг часами. Повезёт, если функция lib2::calc_size будет тоже шаблонной, тогда компилятор выдаст ошибку о том, что вызов calc_size неоднозначен.

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

namespace lib2 {
    class cls;

    namespace impl {
        // этот calc_size больше не будет использоваться в ADL для класса cls.
        inline int calc_size(const cls & v) {
            // ...
        }
    }
}

А что делать, если функция calc_size должна быть доступна для пользователей библиотеки lib2? Можно использовать директиву using namespace impl; внутри пространства имён lib2, или вместо этого использовать безымянное пространство имён.

namespace lib2 {
    namespace 
    class cls;

    namespace {
        // calc_size не будет использоваться в ADL для класса cls
        inline int calc_size(const cls & v) {
            // ...
        }
    }

    namespace {
        void some_other_func(const cls & v) {
            calc_size(v);       // OK
        }
    }
}

cls c;
::lib2::calc_size(c);           // OK

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

comments powered by Disqus