Каковы подводные камни 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 так быстро.