Каковы подводные камни ADL? - PullRequest
48 голосов
/ 02 июня 2010

Некоторое время назад я прочитал статью, в которой объяснялось несколько ловушек, зависящих от аргументов, но я больше не могу ее найти. Речь шла о получении доступа к вещам, к которым у вас не должно быть доступа, или к чему-то в этом роде. Поэтому я решил спросить здесь: каковы подводные камни ADL?

Ответы [ 2 ]

65 голосов
/ 22 ноября 2010

Существует огромная проблема с аргументно-зависимым поиском. Рассмотрим, например, следующую утилиту:

#include <iostream>

namespace utility
{
    template <typename T>
    void print(T x)
    {
        std::cout << x << std::endl;
    }

    template <typename T>
    void print_n(T x, unsigned n)
    {
        for (unsigned i = 0; i < n; ++i)
            print(x);
    }
}

Это достаточно просто, верно? Мы можем вызвать print_n() и передать ему любой объект, и он вызовет print, чтобы напечатать объект n раз.

На самом деле, получается, что если мы посмотрим только на этот код, мы абсолютно не знаем , какую функцию будет вызывать print_n. Это может быть приведенный здесь шаблон функции print, но это не так. Зачем? Поиск, зависящий от аргумента.

В качестве примера предположим, что вы написали класс для представления единорога. По какой-то причине вы также определили функцию с именем print (что за совпадение!), Которая просто вызывает сбой программы при записи в нулевой указатель с разыменованной ссылкой (кто знает, почему вы это сделали; это не важно):

namespace my_stuff
{
    struct unicorn { /* unicorn stuff goes here */ };

    std::ostream& operator<<(std::ostream& os, unicorn x) { return os; }

    // Don't ever call this!  It just crashes!  I don't know why I wrote it!
    void print(unicorn) { *(int*)0 = 42; }
}

Затем вы пишете небольшую программу, которая создает единорога и печатает его четыре раза:

int main()
{
    my_stuff::unicorn x;
    utility::print_n(x, 4);
}

Вы компилируете эту программу, запускаете ее и ... она падает. «Что ?! Ни за что», - говорите вы: «Я только что позвонил print_n, который вызывает функцию print для печати единорога четыре раза!» Да, это правда, но он не вызвал функцию print, которую вы ожидали. Это называется my_stuff::print.

Почему выбран my_stuff::print? Во время поиска имени компилятор видит, что аргумент для вызова print имеет тип unicorn, который является типом класса, который объявлен в пространстве имен my_stuff.

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

(Если вы не верите этому, вы можете скомпилировать код в этом вопросе как есть и увидеть ADL в действии.)

Да, зависимый от аргумента поиск является важной особенностью C ++. По сути, это необходимо для достижения желаемого поведения некоторых языковых функций, таких как перегруженные операторы (рассмотрим библиотеку потоков). Тем не менее, это также очень, очень некорректно и может привести к действительно ужасным проблемам. Было несколько предложений по исправлению зависимого от аргумента поиска, но ни одно из них не было принято комитетом по стандартам C ++.

2 голосов
/ 16 июля 2018

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

Короче говоря, если вы используете неквалифицированное имя в postfix-expression вызова функции, вы должны признать, что вы предоставили возможность переопределения функции в другом месте (да, это разновидность статического полиморфизма). Таким образом, написание неквалифицированного имени функции в C ++ является точно частью интерфейса .

В случае принятого ответа, если print_n действительно нужен ADL print (т. Е. Допускается его переопределение), он должен был быть задокументирован с использованием неквалифицированного print в качестве явного уведомления, таким образом клиенты получат контракт о том, что print должен быть тщательно объявлен, а за ненадлежащее поведение будет отвечать my_stuff. В противном случае это ошибка print_n. Исправление простое: квалификация print с префиксом utility::. Это действительно ошибка print_n, но вряд ли это ошибка правил ADL в языке.

Тем не менее, do существует нежелательные вещи в спецификации языка, и технически, не только один . Они реализуются более 10 лет, но ничего в языке еще не исправлено. Они пропущены принятым ответом (за исключением того, что последний абзац до сих пор является правильным). Подробнее см. бумагу .

Я могу добавить один реальный случай против поиска имени. Я реализовывал is_nothrow_swappable, где __cplusplus < 201703L. Я обнаружил, что невозможно полагаться на ADL при реализации такой функции, когда у меня есть объявленный шаблон функции swap в моем пространстве имен. Такой swap всегда будет найден вместе с std::swap, введенным идиоматическим using std::swap; для использования ADL в соответствии с правилами ADL, и тогда возникнет неоднозначность swap, где шаблон swap (который будет создавать экземпляр is_nothrow_swappable чтобы получить правильный noexcept-specification) называется. В сочетании с правилами двухфазного поиска порядок объявлений не учитывается после включения заголовка библиотеки, содержащего шаблон swap. Таким образом, если я не перегружу все типы моей библиотеки специализированной функцией swap (для подавления любых подходящих шаблонных шаблонов swap, сопоставляемых с перегрузкой разрешения после ADL), я не могу объявить шаблон. По иронии судьбы, шаблон swap, объявленный в моем пространстве имен, предназначен именно для использования ADL (рассмотрим boost::swap), и это один из наиболее значимых прямых клиентов is_nothrow_swappable в моей библиотеке (кстати, boost::swap не учитывает исключение Спецификация). Это прекрасно побило мою цель, вздох ...

#include <type_traits>
#include <utility>
#include <memory>
#include <iterator>

namespace my
{

#define USE_MY_SWAP_TEMPLATE true
#define HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE false

namespace details
{

using ::std::swap;

template<typename T>
struct is_nothrow_swappable
    : std::integral_constant<bool, noexcept(swap(::std::declval<T&>(), ::std::declval<T&>()))>
{};

} // namespace details

using details::is_nothrow_swappable;

#if USE_MY_SWAP_TEMPLATE
template<typename T>
void
swap(T& x, T& y) noexcept(is_nothrow_swappable<T>::value)
{
    // XXX: Nasty but clever hack?
    std::iter_swap(std::addressof(x), std::addressof(y));
}
#endif

class C
{};

// Why I declared 'swap' above if I can accept to declare 'swap' for EVERY type in my library?
#if !USE_MY_SWAP_TEMPLATE || HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE
void
swap(C&, C&) noexcept
{}
#endif

} // namespace my

int
main()
{
    my::C a, b;
#if USE_MY_SWAP_TEMPLATE

    my::swap(a, b); // Even no ADL here...
#else
    using std::swap; // This merely works, but repeating this EVERYWHERE is not attractive at all... and error-prone.

    swap(a, b); // ADL rocks?
#endif
}

Попробуйте https://wandbox.org/permlink/4pcqdx0yYnhhrASi и поверните USE_MY_SWAP_TEMPLATE на true, чтобы увидеть неоднозначность.

Обновление 2018-11-05:

Ага, сегодня утром меня снова укусила ADL. На этот раз это даже не имеет отношения к вызовам функций!

Сегодня я заканчиваю работу по переносу ISO C ++ 17 std::polymorphic_allocator на мою кодовую базу. Поскольку некоторые шаблоны классов контейнеров были введены в моем коде давно (например, this ), на этот раз я просто заменяю объявления шаблонами псевдонимов, такими как:

namespace pmr = ystdex::pmr;
template<typename _tKey, typename _tMapped, typename _fComp
    = ystdex::less<_tKey>, class _tAlloc
    = pmr::polymorphic_allocator<std::pair<const _tKey, _tMapped>>>
using multimap = std::multimap<_tKey, _tMapped, _fComp, _tAlloc>;

... поэтому он может использовать мою реализацию polymorphic_allocator по умолчанию. (Отказ от ответственности: в нем есть некоторые известные ошибки. Исправления ошибок будут совершены через несколько дней.)

Но это вдруг не работает, с сотнями строк загадочных сообщений об ошибках ...

Ошибка начинается с этой строки . Он грубо жалуется на то, что заявленное BaseType не является основой класса включения MessageQueue. Это кажется очень странным, потому что псевдоним объявляется с точно такими же токенами, что и в base-specier-list-list определения класса, и я уверен, что ничего из них не может быть расширено макросом. Так почему?

Ответ ... ADL отстой. Строка, представляющая BaseType, жестко запрограммирована с именем std в качестве аргумента шаблона, поэтому шаблон будет найден в соответствии с правилами ADL в области видимости класса . Таким образом, он находит std::multimap, который отличается от результата поиска как фактический базовый класс, объявленный во включающей области имен . Поскольку std::multimap использует std::allocator экземпляр в качестве аргумента шаблона по умолчанию, BaseType отличается от фактического базового класса, который имеет экземпляр polymorphic_allocator, даже multimap, объявленный в окружающем пространстве имен, перенаправляется на std::multimap. При добавлении включающей квалификации в качестве префикса справа к = ошибка исправлена.

Я признаю, что мне повезло. Сообщения об ошибках направляют проблему на эту строку. Есть только 2 аналогичные проблемы, и другой без каких-либо явных std (где string - мой собственный , адаптируемый к изменению string_view ISO C ++ 17, не std один в режимах до C ++ 17). Я бы не понял, ошибка в ADL так быстро.

...