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

时间:2015-11-03 16:08:37

标签: 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));
}

Live Clang demo Live GCC demo

这给出了:

  

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

考虑到如果你写using boost::end; works just fine,这意味着boost::end可见:

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

为清楚起见,预期结果与使用std::find_if和不合格end(vec)的{​​{3}}中的结果类似。

3 个答案:

答案 0 :(得分:10)

历史背景

这个closed Boost ticket

中讨论了潜在的原因
  

使用以下代码,编译器会抱怨没有开始/结束   找到&#34; range_2&#34;这是整数范围。我想整数范围   缺少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;
}
  ADL无意找到

boost::begin()boost::end()。在   事实上,Boost.Range特别采取预防措施来防范   通过声明,ADL找到boost::begin()boost::end()   它们在namespace boost::range_adl_barrier中然后导出它们   从那里进入namespace boost。 (这种技术称为&#34; ADL   屏障&#34;。)

     

对于您的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()不同   using boost::begin;,不要使用ADL屏障来防止被发现   ADL)。

     

要编译代码,只需添加&#34; using boost::end;&#34;和   &#34; begin()/end()&#34;,或明确限定您的boost::来电   与&#34; begin&#34;。

说明ADL危险的扩展代码示例

对于endbegin(x)的无限制通话,ADL的危险是双重的:

  1. 相关命名空间的集合可能比预期的要大得多。例如。在x中,如果begin具有(可能是默认的!)模板参数,或者在其实现中隐藏了基类,则ADL也会考虑模板参数及其基类的关联命名空间。这些关联的命名空间中的每一个都可能导致在参数依赖查找期间引入endnamespace std的许多重载。
  2. 在重载解析期间无法区分无约束模板。例如。在begin中,endboost函数模板不会针对每个容器单独重载,或以其他方式约束在所提供的容器的签名上。当另一个命名空间(例如std::iterator)也提供类似的无约束函数模板时,重载解析将认为两者都是相等匹配,并且发生错误。
  3. 以下代码示例说明了以上几点。

    小型容器库

    第一个要素是拥有一个容器类模板,它很好地包装在它自己的命名空间中,带有一个派生自begin的迭代器,以及一般和无约束的函数模板end和{{1} }。

    #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
    

    小范围图书馆

    第二个要素是使用另一组无约束的函数模板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
    

    通过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;
    }
    

    Live Example

    rng上依赖于参数的名称查找会发现beginend 的3次重载:来自namespace R(因为{{1}从rng开始(因为namespace C模板参数rng生活在那里)和Container<int, 4>::Iterator(因为迭代器来自namespace std )。 过载分辨率会将所有3次重载视为相等匹配,这会导致硬错误。

    Boost通过将std::iteratorboost::begin放在内部命名空间中并使用指令将它们拉入封闭的boost::end命名空间来解决这个问题。替代方案和IMO更直接的方式是来ADL保护类型(而不是函数),因此在这种情况下,boostContainer类模板。

    Live Example With ADL barriers

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

    有趣的是,ADL保护IteratorRangeContainer - 在这种特殊情况下 - 足以让上述代码无误地运行,因为IteratorRangestd::begin会被调用,因为std::end不受ADL保护。 这非常令人惊讶和脆弱。例如。如果std::iterator的实现不再来自C::Container::Iterator,则代码将停止编译。因此,最好在std::iterator的任何范围内使用合格的来电R::beginR::end,以免受此类拙劣的名称劫持。

    另请注意,range-for曾经具有上述语义(将ADL至少namespace R作为关联的命名空间)。这在N3257中讨论过,导致范围内的语义变化。当前范围 - 首先查找成员函数stdbegin,这样就不会考虑endstd::begin,无论ADL障碍和std::end的继承如何1}}。

    std::iterator

    Live Example

答案 1 :(得分:7)

boost/range/end.hpp中,他们通过将end置于range_adl_barrier命名空间,然后using namespace range_adl_barrier;将其置于boost命名空间中来明确阻止ADL。

由于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{} );
}

live examplefoo::beginbar::begin都是同样有效的函数,可以在该上下文中调用begin( bar::bar_type{} )

这可能就是他们所说的。他们boost::beginstd::begin在您using std::begin类型boost的情况下可能同样有效。通过将其放在boost的子命名空间中,std::begin被调用(并且自然地在范围上工作)。

如果命名空间begin中的boost不那么通用,那么它会更受欢迎,但这不是他们编写它的方式。

答案 2 :(得分:5)

那是因为boost::endinside an ADL barrier,然后是pulled in boost at the end of the file

但是,从cppreference's page on ADL开始(抱歉,我没有方便的C ++草稿):

  

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

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