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

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


person fredoverflow    schedule 02.06.2010    source источник
comment
Отличный актуальный пост Артура О'Дуайера в блоге о проблемах неоднозначности при вызовах size из-за ADL as std::size был добавлен в C++17, см.: https://quuxplusone.github.io/blog/2018/06/17/std-size.   -  person Amir Kirsh    schedule 07.02.2021


Ответы (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++.

person James McNellis    schedule 22.11.2010
comment
Я, вероятно, должен отметить, что этот пример был вдохновлен презентацией по этому вопросу, которую сделал Бартош Милевский; У меня нет слайдов из той презентации, и это не совсем то же самое, но близко. - person James McNellis; 22.11.2010
comment
Это ловушка ADL или ловушка небрежного использования ADL? - person Chubsdad; 22.11.2010
comment
@Chubsdad: это огромная ловушка ADL. Проблема в том, что вы можете написать две полностью независимые библиотеки и случайно столкнуться с этой проблемой, даже не подозревая, что у вас возникнут проблемы. Никакая осторожность не может полностью защитить вас от этого. - person James McNellis; 22.11.2010
comment
Нет, у двух независимых библиотек никогда не будет такой проблемы. Вы должны написать явный оператор, который смешивает их, как в примере выше. И даже здесь вы видите, что библиотеки предпочитают не смешивать: mystuff-print используется с mystuff-unicorn. - person MSalters; 04.02.2011
comment
@MSalters: Ну, проблема в том, что явный оператор, который смешивает библиотеки, не всегда такой явный. Рассмотрим, например, если вы пишете шаблон функции области видимости пространства имен с именем merge, который объединяет две вещи, и вы передаете два объекта std::vector. Вы получите разные результаты в зависимости от того, включили ли вы <algorithm> (который объявляет std::merge). - person James McNellis; 04.02.2011
comment
Как будет выглядеть обходной путь? Неявно использовать текущую область и использовать ADL только в случае явного запроса и/или отсутствия соответствия в текущей области? - person Konrad Rudolph; 18.11.2011
comment
Я бы сказал, что это не ловушка, а фича: она позволяет переопределить поведение библиотеки, предоставив реализацию, специализированную для ваших типов. Без ADL вы не сможете изменить поведение print в соответствии с вашим типом unicorn. Широко используемым приложением этого является swap: многие стандартные алгоритмы нуждаются в обмене значениями; вы можете предоставить собственную оптимизированную версию swpa, и она будет выбрана благодаря ADL. Конечно, было бы лучше, если бы вы могли предотвратить это переопределение, когда оно не требуется (так же, как вы не обязаны делать свои функции-члены виртуальными). - person Luc Touraille; 01.12.2011
comment
Но я согласен с тем, что иногда это может привести к неожиданному поведению. Пользователь библиотеки должен очень хорошо знать библиотеку, которую он использует, чтобы избежать случайной перегрузки функции. - person Luc Touraille; 01.12.2011
comment
@LucTouraille он позволяет вам переопределить поведение библиотеки, предоставляя реализацию, специализированную для ваших типов. тогда каждая функция является точкой настройки. Разумное значение по умолчанию состоит в том, что функции не являются виртуальными, если они явно не помечены как виртуальные. многие стандартные алгоритмы нуждаются в обмене значениями; вы можете предоставить свою собственную оптимизированную версию, но синтаксис вызова swap уродлив. - person curiousguy; 24.12.2011
comment
Я думаю, что либо в документации print_n() следует упомянуть, что он вызывает print() несколько раз (чтобы пользователи знали о проблемах с ADL), либо что print() следует заключить в namespace detail. В том примере кода, который вы привели, у print_n() есть недокументированная точка настройки в виде ADL на print(). - person TemplateRex; 13.07.2012
comment
+1. Но если бы он не вызывал код сбоя, я бы серьезно разозлился. - person ; 19.10.2012
comment
Я думаю, что это аргумент в пользу того, чтобы избегать using namespace (ну, за исключением ваших собственных пространств имен), а не против ADL. Если вы укажете пространство имен, ваш читатель и компилятор поймут вас лучше. Для вызова print из print_n вы можете указать пространство имен или использовать его как точку настройки. - person Grault; 08.09.2013
comment
Вопрос в том, зачем программисту писать print(x), когда он хочет вызвать ::utility::print()? Если я напишу print(x), то я намерен вызвать ADL, чтобы найти правильную перегрузку (возможно, и в других пространствах имен). Если бы я не хотел ADL, я бы написал ::utility::print(x). Так что я не полностью согласен с этим ответом. Чаще всего это происходит из-за отсутствия базовых знаний об ADL. Вместо этого я бы согласился с @LucTouraille. :-) - person Nawaz; 09.10.2013
comment
Не согласен. Этот ответ вводит в заблуждение. Это превращает саму причину существования ADL в проблему. Обратите внимание, что print(x) — это не просто функция. Это шаблон, который принимает что угодно. И дополнительно: использование в print_n безоговорочно. Если кто-то действительно пишет такие загадочные вещи, он (она) должен знать правила и играть по правилам. И, пожалуйста, не жалуйтесь на единорогов. Почему вы загружаете единорогов в свой шаблон print_n, не проверяя тех, кто создан для такого обращения?! - person Ichthyo; 30.11.2017
comment
Очевидно, что ADL может вызвать серьезные проблемы, но как мы можем предотвратить это? Я где-то читал пример, в котором автор использует вложенное пространство имен и включает его в родительское пространство имен с using nested_namespace в качестве защиты. Тем не менее, я не могу понять, как это будет работать в этом примере. Должен ли я включать вложенное пространство имен в my_stuff или в utility? - person andresgongora; 10.01.2018
comment
Избегайте АДЛ. Используйте внедрение пространства имен там, где это позволяет пространство имен библиотеки. - person chila; 01.05.2020

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

Ответ таков... 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.

person FrankHB    schedule 16.07.2018
comment
Я думаю, что большинство людей сегодня согласны с тем, что правила ADL в том виде, в каком они были определены, были ошибкой (а не ADL сами по себе). Квалифицировать все вызовы функций для символов в вашем собственном пространстве имен (большинство из них в коде приложения) — рутинная работа. Это также вредит читабельности. По умолчанию должно было быть наоборот: явно указывать, какие вызовы предназначены для выполнения ADL. - person Acorn; 04.11.2018
comment
@Acorn Это может быть лучше в смысле POLA, но если бы это было правдой, я подозреваю, что был бы отличительный синтаксис, разработанный специально для ADL. Хотя могут быть и другие варианты. В любом случае, ADL отменяет обычные правила поиска имени во время перевода, так почему бы не использовать некоторые более общие средства метапрограммирования, позволяющие его программировать (например, гигиенические макросы)? Но, к сожалению, это не было учтено при разработке. - person FrankHB; 05.11.2018