Why is ADL not working with Boost.Range?

后端 未结 3 576
误落风尘
误落风尘 2021-01-11 10:43

Considering:

#include 
#include 
#include 

int main() {
    auto range = boost         


        
3条回答
  •  臣服心动
    2021-01-11 11:03

    Historical background

    The underlying reason is discussed in this closed Boost ticket

    With the following code, compiler will complain that no begin/end is found for "range_2" which is integer range. I guess that integer range is missing ADL compatibility ?

    #include 
    
    #include 
    #include 
    
    int main() {
        std::vector 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() and boost::end() are not meant to be found by ADL. In fact, Boost.Range specifically takes precautions to prevent boost::begin() and boost::end() from being found by ADL, by declaring them in the namespace boost::range_adl_barrier and then exporting them into the namespace boost from there. (This technique is called an "ADL barrier").

    In the case of your range_1, the reason unqualified begin() and end() calls work is because ADL looks not only at the namespace a template was declared in, but the namespaces the template arguments were declared in as well. In this case, the type of range_1 is boost::iterator_range::iterator>. The template argument is in namespace std (on most implementations), so ADL finds std::begin() and std::end() (which, unlike boost::begin() and boost::end(), do not use an ADL barrier to prevent being found by ADL).

    To get your code to compile, simply add "using boost::begin;" and "using boost::end;", or explicitly qualify your begin()/end() calls with "boost::".

    Extended code example illustrating the dangers of ADL

    The danger of ADL from unqualified calls to begin and end is two-fold:

    1. the set of associated namespaces can be much larger than one expects. E.g. in begin(x), if x has (possibly defaulted!) template parameters, or hidden base classes in its implementation, the associated namespaces of the template parameters and of its base classes are also considered by ADL. Each of those associated namespace can lead to many overloads of begin and end being pulled in during argument dependent lookup.
    2. unconstrained templates cannot be distinguished during overload resolution. E.g. in namespace std, the begin and end function templates are not separately overloaded for each container, or otherwise constrained on the signature of the container being supplied. When another namespace (such as boost) also supplies similarly unconstrained function templates, overload resolution will consider both an equal match, and an error occurs.

    The following code samples illustrate the above points.

    A small container library

    The first ingredient is to have a container class template, nicely wrapped in its own namespace, with an iterator that derives from std::iterator, and with generic and unconstrained function templates begin and end.

    #include 
    #include 
    
    namespace C {
    
    template
    struct Container
    {
        T data[N];
        using value_type = T;
    
        struct Iterator : public std::iterator
        {
            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
    auto begin(Cont& c) -> decltype(c.begin()) { return c.begin(); }
    
    template
    auto end(Cont& c) -> decltype(c.end()) { return c.end(); }
    
    }   // C
    

    A small range library

    The second ingredient is to have a range library, also wrapped in its own namespace, with another set of unconstrained function templates begin and end.

    namespace R {
    
    template
    struct IteratorRange
    {
        It first, second;
    
        auto begin() { return first; }
        auto end() { return second; }
    };
    
    template
    auto make_range(It first, It last)
        -> IteratorRange
    {
        return { first, last };    
    }
    
    template
    auto begin(Rng& rng) -> decltype(rng.begin()) { return rng.begin(); }
    
    template
    auto end(Rng& rng) -> decltype(rng.end()) { return rng.end(); }
    
    } // R
    

    Overload resolution ambiguity through ADL

    Trouble begins when one tries to make an iterator range into a container, while iterating with unqualified begin and end:

    int main() 
    {
        C::Container 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

    Argument-dependent name lookup on rng will find 3 overloads for both begin and end: from namespace R (because rng lives there), from namespace C (because the rng template parameter Container::Iterator lives there), and from namespace std (because the iterator is derived from std::iterator). Overload resolution will then consider all 3 overloads an equal match and this results in a hard error.

    Boost solves this by putting boost::begin and boost::end in an inner namespace and pulling them into the enclosing boost namespace by using directives. An alternative, and IMO more direct way, would be to ADL-protect the types (not the functions), so in this case, the Container and IteratorRange class templates.

    Live Example With ADL barriers

    Protecting your own code may not be enough

    Funny enough, ADL-protecting Container and IteratorRange would -in this particular case- be enough to let the above code run without error because std::begin and std::end would be called because std::iterator is not ADL-protected. This is very surprising and fragile. E.g. if the implementation of C::Container::Iterator no longer derives from std::iterator, the code would stop compiling. It is therefore preferable to use qualified calls R::begin and R::end on any range from namespace R in order to be protected from such underhanded name-hijacking.

    Note also that the range-for used to have the above semantics (doing ADL with at least std as an associated namespace). This was discussed in N3257 which led to semantic changes in range-for. The current range-for first looks for member functions begin and end, so that std::begin and std::end will not be considered, regardless of ADL-barriers and inheritance from std::iterator.

    int main() 
    {
        C::Container arr = {{ 1, 2, 3, 4 }};
        auto rng = R::make_range(arr.begin(), arr.end());
        for (auto e : rng)
            std::cout << e;
    }
    

    Live Example

提交回复
热议问题