为什么ADL不能使用Boost.Range?

Sho*_*hoe 12 c++ boost namespaces argument-dependent-lookup 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));
}
Run Code Online (Sandbox Code Playgroud)

Live Clang demo Live GCC demo

这给了:

main.cpp:8:37:错误:使用未声明的标识符'end'

考虑到如果你写using boost::end;工作得很好,这意味着它boost::end是可见的:

为什么ADL不工作并且boost::end在表达中找到end(range)?如果它是故意的,它背后的理由是什么?


需要说明的是,预期结果与本例中使用std::find_if和不合格的结果类似end(vec).

Tem*_*Rex 11

历史背景

在这个封闭的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;
}
Run Code Online (Sandbox Code Playgroud)

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也会考虑模板参数及其基类的关联命名空间.这些关联的命名空间中的每一个都可能导致在参数依赖查找期间的许多重载beginend被拉入.
  2. 在重载解析期间无法区分无约束模板.例如namespace std,对于每个容器,beginend函数模板不是单独重载的,或者以受限制的方式约束在所提供的容器的签名上.当另一个命名空间(例如boost)也提供类似的无约束函数模板时,重载解析将同时考虑相等匹配,并发生错误.

以下代码示例说明了以上几点.

一个小容器库

第一个成分是有一个容器类模板,很好地包裹在自己的命名空间,与从派生的迭代器std::iterator,并与通用的无约束函数模板beginend.

#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
Run Code Online (Sandbox Code Playgroud)

一个小范围的图书馆

第二个要素是有一个范围库,也包含在自己的命名空间中,还有另一组无约束的函数模板beginend.

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
Run Code Online (Sandbox Code Playgroud)

通过ADL过载分辨率模糊

当一个人试图将迭代器范围变成一个容器时,麻烦就开始了,同时用不合格的迭代beginend:

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;
}
Run Code Online (Sandbox Code Playgroud)

实例

依赖于参数的名称查找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命名空间来解决这个问题.替代方法和IMO更直接的方式是ADL保护类型(而不是函数),因此在本例中是ContainerIteratorRange类模板.

实例与ADL障碍

保护自己的代码可能还不够

有趣的是,ADL保护Container并且IteratorRange在这种特殊情况下 - 足以让上面的代码运行而没有错误因为std::begin并且std::end因为std::iterator不受ADL保护而被调用.这非常令人惊讶和脆弱.例如,如果实现C::Container::Iterator不再来自std::iterator,代码将停止编译.因此,最好使用合格的呼叫,R::beginR::end在任何范围内namespace R进行保护,以防止这种不正当的名称劫持.

还要注意,range-for用于具有上述语义(至少std将ADL 作为关联的命名空间).这在N3257中讨论过,导致范围内的语义变化.目前的区间为首先查找成员函数beginend,使std::beginstd::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;
}
Run Code Online (Sandbox Code Playgroud)

实例

  • 非常好的文章.哎呀.答案:)样本非常重要. (3认同)

Yak*_*ont 7

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{} );
}
Run Code Online (Sandbox Code Playgroud)

实例.双方foo::beginbar::begin具有同等法律效力调用的函数的begin( bar::bar_type{} )在这方面.

这可能就是他们所说的.他们boost::beginstd::begin可能在你有一个背景下同样有效using std::begin从一个类型boost.通过将其置于子命名空间中boost,std::begin可以调用(并自然地在范围上工作).

如果begin命名空间中的boost泛型不太通用,那么它将是首选,但这不是他们编写它的方式.


Ven*_*Ven 5

那是因为boost::end位于ADL屏障内部,然后boost文件末尾拉入.

但是,从ADL的cppreference页面(抱歉,我没有方便的C++草案):

1)忽略关联命名空间中的using-directive

这可以防止它被包含在ADL中.