c++c++17overloadingambiguousreverse-iterator

Ambiguous call when overloaded methods take reverse iterators in arguments


I'm trying to write an overloaded method that returns non-const result only when both the object on which it is called is non-const and iterator passed in the argument is non-const. (Think of it like the standard methods begin() and begin() const that additionally take an iterator argument.)

I made a version for normal iterators with no problems. However, for some reason, when I'm trying to do the same for reverse iterators, I get a compilation error about an ambiguous function call.

Here's a minimal example:

#include <vector>

class Foo
{
public:
    void bar(std::vector<int>::iterator x) {}
    void bar(std::vector<int>::const_iterator x) const {}

    void baz(std::vector<int>::reverse_iterator x) {}
    void baz(std::vector<int>::const_reverse_iterator x) const {}
};

int main()
{
    std::vector<int> v;
    Foo foo;
    foo.bar(v.cbegin());  // OK
    foo.baz(v.crbegin()); // ambiguous
}

For some reason it compiles if I remove const from the second method baz. It also works in C++20 but currently I cannot use that version.

live demo

How can I make the function baz work in analogous way to the function bar?


Solution

  • Oh the joys of overloading resolution rules and SFINAE.

    The methods are equivalent to free functions:

    void bbaz(Foo&,std::vector<int>::reverse_iterator){}
    void bbaz(const Foo&,std::vector<int>::const_reverse_iterator){}
    

    and your usage becomes:

    int main()
    {
        std::vector<int> v;
        Foo foo;
        bbaz(foo,v.crbegin());
    }
    

    The arguments do not exactly match either call:

    Cause of ambiguity

    Now, the issue is that std::reverse_iterator's ctor is not SFINAE-friendly until C++20:

    template< class U >
    std::reverse_iterator( const std::reverse_iterator<U>& other );
    

    I.e. there is a viable candidate converting std::reverse_iterator<T> to std::reverse_iterator<U> between any T-U pairs. In this case for T=vector::const_iterator, U=vector::iterator. But of course the template instantiation fails later because it cannot convert const int* to int*.

    Since that happens in the template function's body, not the signature, it is too late for SFINAE and overloading considers it a viable candidate function, hence the ambiguity since both calls require one implicit conversion - although only the second one would compile.

    This is explained in these answers, making this one essentially a duplicate of that question but it would be IMHO cruel to mark it as such without an explanation which I cannot fit into a comment.

    C++20 fixes this omission and SFINAEs that ctor - cppreference:

    This overload participates in overload resolution only if U is not the same type as Iter and std::convertible_to<const U&, Iter> is modeled (since C++20)

    Solution

    As pointed in the comments by @Eljay, forcing const Foo& at the call site is one option, one can use C++17 std::as_const:

    #include <utility>
    std::as_const(foo).baz(v.crbegin());
    

    Fixing this at definition is more tricky, you could use SFINAE to actually force these overloads but that might be a hassle. @fabian 's solution with adding a third overload without const method qualifier seems easiest to me:

    void Foo::baz(std::vector<int>::const_reverse_iterator x) { 
        return std::as_const(*this).baz(x); 
    }
    

    It works because now it is a better (exact) match for non-const Foos than the still considered vector::reverse_iterator which would not compile anyway.