Каковы подводные камни ADL?

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

2 ответа

Решение

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

#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++.

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

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

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

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

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

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, Это кажется очень странным, потому что псевдоним объявлен с точно такими же токенами, что и в списке базовых спецификаторов определения класса, и я уверен, что ничего из них не может быть расширено макросом. Так почему же?

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

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

Другие вопросы по тегам