Некоторое время назад я прочитал статью, в которой объяснялись некоторые ловушки поиска, зависящего от аргумента, но я больше не могу ее найти. Речь шла о получении доступа к вещам, к которым у вас не должно быть доступа, или что-то в этом роде. Поэтому я решил спросить здесь: каковы подводные камни 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++.
merge
, который объединяет две вещи, и вы передаете два объекта std::vector
. Вы получите разные результаты в зависимости от того, включили ли вы <algorithm>
(который объявляет std::merge
).
- person James McNellis; 04.02.2011
print
в соответствии с вашим типом unicorn
. Широко используемым приложением этого является swap
: многие стандартные алгоритмы нуждаются в обмене значениями; вы можете предоставить собственную оптимизированную версию swpa
, и она будет выбрана благодаря ADL. Конечно, было бы лучше, если бы вы могли предотвратить это переопределение, когда оно не требуется (так же, как вы не обязаны делать свои функции-члены виртуальными).
- person Luc Touraille; 01.12.2011
print_n()
следует упомянуть, что он вызывает print()
несколько раз (чтобы пользователи знали о проблемах с ADL), либо что print()
следует заключить в namespace detail
. В том примере кода, который вы привели, у print_n()
есть недокументированная точка настройки в виде ADL на print()
.
- person TemplateRex; 13.07.2012
using namespace
(ну, за исключением ваших собственных пространств имен), а не против ADL. Если вы укажете пространство имен, ваш читатель и компилятор поймут вас лучше. Для вызова print из print_n вы можете указать пространство имен или использовать его как точку настройки.
- person Grault; 08.09.2013
print(x)
, когда он хочет вызвать ::utility::print()
? Если я напишу print(x)
, то я намерен вызвать ADL, чтобы найти правильную перегрузку (возможно, и в других пространствах имен). Если бы я не хотел ADL, я бы написал ::utility::print(x)
. Так что я не полностью согласен с этим ответом. Чаще всего это происходит из-за отсутствия базовых знаний об ADL. Вместо этого я бы согласился с @LucTouraille. :-)
- person Nawaz; 09.10.2013
using nested_namespace
в качестве защиты. Тем не менее, я не могу понять, как это будет работать в этом примере. Должен ли я включать вложенное пространство имен в my_stuff
или в utility
?
- person andresgongora; 10.01.2018
Принятый ответ просто неверен - это не ошибка 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.
size
из-за ADL asstd::size
был добавлен в C++17, см.: https://quuxplusone.github.io/blog/2018/06/17/std-size. - person Amir Kirsh   schedule 07.02.2021