Почему ADL не работает с Boost.Range?
Учитывая:
#include <cassert>
#include <boost/range/irange.hpp>
#include <boost/range/algorithm.hpp>
int main() {
auto range = boost::irange(1, 4);
assert(boost::find(range, 4) == end(range));
}
Live Clang demo
Live GCC demo
это дает:
main.cpp: 8: 37: ошибка: использование незаявленного идентификатора "конец"
Учитывая, что если вы пишете using boost::end;
, то отлично работает, что означает, что boost::end
видно:
Почему ADL не работает и находит boost::end
в выражении end(range)
? И если это преднамеренно, что за это стоит?
Чтобы быть ясным, ожидаемый результат был бы аналогичен тому, что происходит в этом примере, используя std::find_if
и неквалифицированный end(vec)
.
Ответы
Ответ 1
В boost/range/end.hpp
они явно блокируют ADL, помещая end
в пространство имен range_adl_barrier
, затем using namespace range_adl_barrier;
, чтобы привести его в пространство имен boost
.
Как end
на самом деле не от ::boost
, а скорее от ::boost::range_adl_barrier
, он не найден ADL.
Их рассуждения описаны в boost/range/begin.hpp
:
//Используйте барьер пространства имен ADL, чтобы избежать двусмысленности с другими неквалифицированными // вызывает. Это особенно важно с поддержкой С++ 0x // неквалифицированные вызовы для начала/конца.
нет примеров, где это вызывает проблему, поэтому я могу только теоретизировать то, о чем они говорят.
Вот пример, который я придумал, как ADL может вызвать неоднозначность:
namespace foo {
template<class T>
void begin(T const&) {}
}
namespace bar {
template<class T>
void begin(T const&) {}
struct bar_type {};
}
int main() {
using foo::begin;
begin( bar::bar_type{} );
}
живой пример. Оба foo::begin
и bar::begin
являются одинаково допустимыми функциями для вызова begin( bar::bar_type{} )
в этом контексте.
Это может быть то, о чем они говорят. Их boost::begin
и std::begin
могут быть одинаково действительными в контексте, где у вас есть using std::begin
для типа из boost
. Помещая его в пространство под-имен boost
, std::begin
получает вызов (и работает на диапазонах, естественно).
Если begin
в пространстве имен boost
был менее общим, было бы предпочтительным, но это не то, как они его написали.
Ответ 2
Историческая справка
Основная причина обсуждается в этом закрытом билете Boost
В следующем коде компилятор будет жаловаться, что начало/конец не начинается найденный для "range_2
", который является целым числом. Я думаю, что целочисленный диапазон отсутствует совместимость с ADL?
#include <vector>
#include <boost/range/iterator_range.hpp>
#include <boost/range/irange.hpp>
int main() {
std::vector<int> v;
auto range_1 = boost::make_iterator_range(v);
auto range_2 = boost::irange(0, 1);
begin(range_1); // found by ADL
end(range_1); // found by ADL
begin(range_2); // not found by ADL
end(range_2); // not found by ADL
return 0;
}
boost::begin()
и boost::end()
не должны быть найдены ADL. В факт, Boost.Range специально принимает меры предосторожности для предотвращения boost::begin()
и boost::end()
из найденного ADL, объявив их в namespace boost::range_adl_barrier
, а затем экспортировать их в namespace boost
оттуда. (Этот метод называется "ADL барьер" ).
В случае вашего range_1
причина неквалифицирована begin()
и end()
вызывает работу, потому что ADL ищет не только пространство имен шаблона было объявлено в, но пространства имен аргументы шаблона были заявлено также. В этом случае тип range_1
равен boost::iterator_range<std::vector<int>::iterator>
. Шаблон аргумент находится в namespace std
(в большинстве реализаций), поэтому ADL находит std::begin()
и std::end()
(что, в отличие от boost::begin()
и boost::end()
, не используйте барьер ADL, чтобы предотвратить обнаружение ADL).
Чтобы получить код для компиляции, просто добавьте "using boost::begin;
" и "using boost::end;
" или явно квалифицировать ваши вызовы begin()/end()
с "boost::
".
Пример расширенного кода, иллюстрирующий опасности ADL
Опасность ADL от неквалифицированных вызовов до begin
и end
в два раза:
- набор связанных пространств имен может быть намного больше, чем ожидается. Например. в
begin(x)
, если x
имеет (возможно, по умолчанию!) параметры шаблона или скрытые базовые классы в его реализации, связанные пространства имен параметров шаблона и его базовых классов также рассматриваются ADL. Каждое из этих связанных пространств имен может привести к перегрузкам из begin
и end
во время поиска зависимого от аргумента файла.
- неограниченные шаблоны не могут различаться при разрешении перегрузки. Например. в
namespace std
шаблоны функций begin
и end
не перегружаются отдельно для каждого контейнера или иным образом ограничены сигнатурой поставляемого контейнера. Когда другое пространство имен (например, boost
) также предоставляет аналогичные шаблоны без ограничений, разрешение перегрузки будет учитывать как равное совпадение, так и ошибку.
Следующие примеры кода иллюстрируют вышеуказанные моменты.
Маленькая библиотека контейнеров
Первым компонентом является шаблон шаблона контейнера, красиво завернутый в собственное пространство имен, с итератором, который происходит от std::iterator
, и с шаблонами шаблонов begin
и end
.
#include <iostream>
#include <iterator>
namespace C {
template<class T, int N>
struct Container
{
T data[N];
using value_type = T;
struct Iterator : public std::iterator<std::forward_iterator_tag, T>
{
T* value;
Iterator(T* v) : value{v} {}
operator T*() { return value; }
auto& operator++() { ++value; return *this; }
};
auto begin() { return Iterator{data}; }
auto end() { return Iterator{data+N}; }
};
template<class Cont>
auto begin(Cont& c) -> decltype(c.begin()) { return c.begin(); }
template<class Cont>
auto end(Cont& c) -> decltype(c.end()) { return c.end(); }
} // C
Библиотека небольших диапазонов
Второй компонент состоит в том, чтобы иметь библиотеку диапазонов, также завернутую в собственное пространство имен, с другим набором шаблонов без ограничений begin
и end
.
namespace R {
template<class It>
struct IteratorRange
{
It first, second;
auto begin() { return first; }
auto end() { return second; }
};
template<class It>
auto make_range(It first, It last)
-> IteratorRange<It>
{
return { first, last };
}
template<class Rng>
auto begin(Rng& rng) -> decltype(rng.begin()) { return rng.begin(); }
template<class Rng>
auto end(Rng& rng) -> decltype(rng.end()) { return rng.end(); }
} // R
Неопределенность разрешения перегрузки через ADL
Неисправность начинается, когда вы пытаетесь сделать диапазон итераторов в контейнере, итерации с помощью неквалифицированных begin
и end
:
int main()
{
C::Container<int, 4> arr = {{ 1, 2, 3, 4 }};
auto rng = R::make_range(arr.begin(), arr.end());
for (auto it = begin(rng), e = end(rng); it != e; ++it)
std::cout << *it;
}
Пример Live
Поиск зависимого от аргумента имени rng
найдет 3 перегрузки для begin
и end
: от namespace R
(потому что rng
живет там), от namespace C
(поскольку там существует параметр шаблона rng
Container<int, 4>::Iterator
) и из namespace std
(потому что итератор получен из std::iterator
). Разрешение перегрузки будет учитывать все 3 перегрузки равным образом, и это приведет к жесткой ошибке.
Boost решает это, помещая boost::begin
и boost::end
во внутреннее пространство имен и потянув их в пространство имен boost
, используя директивы. Альтернативный вариант и более эффективный IMO был бы для ADL-защиты типов (а не функций), поэтому в этом случае шаблоны классов Container
и IteratorRange
.
Живой пример с барьерами ADL
Защита собственного кода может быть недостаточно
Забавно, что ADL-защита Container
и IteratorRange
будет в этом конкретном случае - достаточно, чтобы приведенный выше код работал без ошибок, потому что std::begin
и std::end
будут вызваны, потому что std::iterator
не является ADL-защита. Это очень удивительно и хрупко, Например. если реализация C::Container::Iterator
больше не выводится из std::iterator
, код прекратит компиляцию. Поэтому предпочтительнее использовать квалифицированные вызовы R::begin
и R::end
в любом диапазоне от namespace R
, чтобы быть защищенным от такого хитрого захвата имен.
Обратите внимание также, что диапазон - используется для использования вышеуказанной семантики (выполнение ADL с не менее чем std
как связанное пространство имен). Это обсуждалось в N3257, что привело к семантическим изменениям в диапазоне. Текущий диапазон - сначала ищет функции-члены begin
и end
, поэтому std::begin
и std::end
не будут учитываться, независимо от ADL-барьеров и наследования от std::iterator
.
int main()
{
C::Container<int, 4> arr = {{ 1, 2, 3, 4 }};
auto rng = R::make_range(arr.begin(), arr.end());
for (auto e : rng)
std::cout << e;
}
Пример Live
Ответ 3
Это потому, что boost::end
внутри барьера ADL, который затем вытащил boost
в конце файла.
Однако, из страницы cppreference в ADL (извините, у меня нет возможности для С++):
1) использование директив в связанных пространствах имен игнорируется
Это предотвращает его включение в ADL.