Подтвердить что ты не робот

Почему 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).

4b9b3361

Ответ 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