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.
How can I make the function baz
work in analogous way to the function bar
?
CodePudding user response:
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:
foo
isFoo&
, notconst Foo&
v.crbegin()
returnvector::const_reverse_iterator
which is just a different instantiation of the samestd::reverse_iterator
template asvector::reverse_iterator
.reverse_iterator
->std::reverse_iterator<vector::iterator>
const_reverse_iterator
->std::reverse_iterator<vector::const_iterator>
Explanation
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
andstd::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
Foo
s than the still considered vector::reverse_iterator
which would not compile anyway.