Почему 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 Текущая демонстрация GCC

это дает:

main.cpp:8:37: ошибка: использование необъявленного идентификатора «конец»

Учитывая, что если вы пишете using boost::end;, он работает просто отлично, что означает, что boost::end виден:

Почему не работает ADL и не находит boost::end в выражении end(range)? А если намеренно, то в чем причина?


Чтобы было ясно, ожидаемый результат будет аналогичен тому, что происходит в этом примере с использованием std::find_if и неквалифицированного end(vec).


person Shoe    schedule 03.11.2015    source источник
comment
Может быть, разместите контрастное сообщение: coliru.stacked-crooked.com/a/a18af912213ca7b8, чтобы люди понять, почему существует ожидание   -  person sehe    schedule 03.11.2015
comment
Обратите внимание, что как в вашем коде, так и в примере от @sehe вы также можете использовать неквалифицированные find(range, 4) и find(begin(v), end(v), 4) соответственно. Однако это не рекомендуется, если только вы не собираетесь использовать настраиваемый алгоритм find в зависимости от переданного ему типа.   -  person TemplateRex    schedule 06.11.2015
comment
@sehe, но есть (any_of_equal равно any_of с проверкой предикатов на равенство значению)   -  person TemplateRex    schedule 06.11.2015
comment
Давайте продолжим это обсуждение в чате.   -  person sehe    schedule 06.11.2015
comment
@TemplateRex Резюме: я не знал, что B::Algorithms поставляется с поддержкой Range :) Это довольно мило: coliru.stacked-crooked.com/a/f46da76f94aa2934   -  person sehe    schedule 06.11.2015
comment
@sehe, в этом примере есть ошибка: 4 не простое число! Вместо max(2, n/2) правильной границей цикла деления является floor(sqrt(n)) + 1. (ошибка заключается в том, что any_of в пустом диапазоне возвращает true) Также: !any_of == none_of: см. этот пример   -  person TemplateRex    schedule 06.11.2015
comment
@TemplateRex ЛОЛ! Я неправильно прочитал утверждение - не видя, что оно было перевернуто. Фу. Ну, смысл заключался в том, чтобы сделать any_of, а не тестирование на простоту :) Тем не менее, спасибо за продуманную исправленную версию!   -  person sehe    schedule 06.11.2015


Ответы (3)


В 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, чтобы избежать двусмысленности с другими // неквалифицированными вызовами
. Это особенно важно, поскольку C++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 было менее общим, оно было бы предпочтительнее, но они написали его иначе.

person Yakk - Adam Nevraumont    schedule 03.11.2015
comment
@Ven, потому что причина пространства имен, блокирующего ADL (которое идентично для обоих), находится только в begin.hpp, а не в end.hpp. - person Yakk - Adam Nevraumont; 03.11.2015
comment
Я не уверен, что полностью понимаю аргументацию. Какую двусмысленность он пытается избежать? - person Shoe; 03.11.2015
comment
@Jeffrey Может быть, это? - person Yakk - Adam Nevraumont; 03.11.2015
comment
@Yakk смотрите мой ответ о помехах диапазона/контейнера из-за неквалифицированного начала/конца - person TemplateRex; 07.11.2015

Историческое прошлое

Основная причина обсуждается в этом закрытом тикете 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_1boost::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 двояка:

  1. набор связанных пространств имен может быть намного больше, чем можно ожидать. Например. в begin(x), если x имеет (возможно, по умолчанию!) параметры шаблона или скрытые базовые классы в своей реализации, связанные пространства имен параметров шаблона и его базовых классов также рассматриваются ADL. Каждое из этих связанных пространств имен может привести ко многим перегрузкам begin и end во время поиска, зависящего от аргумента.
  2. неограниченные шаблоны нельзя различить при разрешении перегрузки. Например. в 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;
}

Живой пример

Поиск имени в зависимости от аргумента для rng найдет 3 перегрузки для begin и end: from namespace R (потому что rng живет там), from namespace C (потому что там живет параметр шаблона rng Container<int, 4>::Iterator) и from namespace std ( потому что итератор получен из std::iterator). При разрешении перегрузки все 3 перегрузки будут считаться равными, что приведет к серьезной ошибке.

Boost решает эту проблему, помещая boost::begin и boost::end во внутреннее пространство имен и перетаскивая их во внешнее пространство имен boost с помощью директив. Альтернативой и, по-моему, более прямым способом будет защита ADL типов (а не функций), поэтому в данном случае шаблоны классов Container и IteratorRange.

Живой пример с барьерами ADL

Защиты собственного кода может быть недостаточно

Забавно, но защиты Container и IteratorRange с помощью ADL в данном конкретном случае будет достаточно, чтобы приведенный выше код выполнялся без ошибок, потому что std::begin и std::end будут вызываться, потому что std::iterator не защищен с помощью ADL. Это очень удивительно и хрупко. Например. если реализация C::Container::Iterator больше не является производной от std::iterator, код перестанет компилироваться. Поэтому предпочтительнее использовать квалифицированные вызовы R::begin и R::end в любом диапазоне от namespace R, чтобы защититься от такого закулисного перехвата имен.

Также обратите внимание, что range-for раньше имел вышеуказанную семантику (выполнение ADL по крайней мере с std в качестве связанного пространства имен). Это обсуждалось в N3257, что привело к семантическим изменениям в range-for. Текущий диапазон for сначала ищет функции-члены 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;
}

Живой пример

person TemplateRex    schedule 06.11.2015
comment
Очень хорошая статья. Упс. Ответ :) Образцы очень на месте. - person sehe; 07.11.2015
comment
@sehe спасибо за щедрую награду! - person TemplateRex; 12.11.2015

Это потому, что boost::end находится внутри барьера ADL, который затем вытягивается boost в конец файла.

Однако на странице cppreference в ADL (извините, у меня нет C++ черновик под рукой):

1) директивы использования в связанных пространствах имен игнорируются

Это предотвращает его включение в ADL.

person Ven    schedule 03.11.2015