Consider the following class with a binary operator (I use operator+
just as an example).
struct B{};
template<class>
struct A{
template<class BB>
void operator+(BB const&) const{std::cout<<"member"<<std::endl;}
template<class BB>
friend void operator+(BB const&, A const&){std::cout<<"friend"<<std::endl;}
};
I can call this binary operator with two different types:
A<int> a;
B b;
a + b; // member
b + a; // friend
Then when I try to use A
on both sides (a + a
) a lot of strange things happen. Three compilers give different answer to the same code.
Some context: I don't want to define void operator+(A const&)
because I need a template to SFINAE functions away if some syntax doesn't work. Also I don't want a template<class BB, class AA> friend void operator(BB const&, AA const&)
. Because since A
is a template, different instantiations will produce multiple definitions of the same template.
Continuing with the original code:
Strange thing # 1: In gcc, the friend takes precedence:
a + a; // prints friend in gcc
I expect the member to take precedence, is there a way for the member to take precedence gcc?
Strange thing # 2: In clang, this code doesn't compile:
a + a; // use of overload is ambiguous
This already points out at an inconsistency between gcc and clang, who is right? What would be a workaround for clang that makes it work like gcc?
If I try to be more greedy in the arguments, e.g. to apply some optimization I could use forwarding references:
struct A{
template<class BB>
void operator+(BB&&) const{std::cout<<"member"<<std::endl;}
template<class BB>
friend void operator+(BB&&, A const&){std::cout<<"friend"<<std::endl;}
};
Strange thing # 3: Using forwarding reference gives a warning in gcc,
a + a; // print "friend", but gives "warning: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second:"
But still compiles, How can I silence this warning in gcc or workaround? Just as in case # 1, I expect to prefer the member function but here it prefers the friend function and gives a warning.
Strange thing # 4: Using forwarding reference gives an error in clang.
a + a; // error: use of overloaded operator '+' is ambiguous (with operand types 'A' and 'A')
Which again points at an inconsistency between gcc and clang, who is right in this case?
In summary, I am trying to make this code work consistently. I really want the function to be injected friend function (not free friend functions). I don't want to define a function with equal non-template arguments because different instantiation will produce duplicated declarations of the same functions.
Here is the full code to play with:
#include<iostream>
using std::cout;
struct B{};
template<class>
struct A{
template<class BB>
void operator+(BB const& /*or BB&&*/) const{cout<<"member\n";}
template<class BB>
friend void operator+(BB const& /*or BB const&*/, A const&){cout<<"friend\n";}
};
int main(){
A<int> a; //previos version of the question had a typo here: A a;
B b;
a + b; // calls member
b + a; // class friend
a + a; // surprising result (friend) or warning in gcc, hard error in clang, MSVC gives `member` (see below)
A<double> a2; // just to instantiate another template
}
Note: I am using clang version 6.0.1
and g++ (GCC) 8.1.1 20180712
. According to Francis Cugler MSVS 2017 CE give yet a different behavior.
I found a workaround that does the correct thing (prints 'member' for a+a
case) for both clang and gcc (for MSVS?), but it requires a lot for boiler plate and an artificial base class:
template<class T>
struct A_base{
template<class BB>
friend void operator+(BB const&, A_base<T> const&){std::cout<<"friend"<<std::endl;}
};
template<class T>
struct A : A_base<T>{
template<class BB>
void operator+(BB const&) const{std::cout<<"member"<<std::endl;}
};
However it still give an ambiguous call if I replace BB const&
with BB&&
.
These are all ambiguous. There are known partial ordering bugs in GCC when ordering a member and a non-member, e.g. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66914.
Just constrain your friend to not participate in overload resolution if BB
is a specialization of A
.
You want to disable the friend if BB is convertible to A:
template<
class BB,
std::enable_if<
!std::is_convertible<B const&, A>::value, int>::type = 0>
friend void operator+(BB const&, A const&){std::cout<<"friend"<<std::endl;}
Note: you need to use the std::enable_if
as the type to make it so that the function declaration never resolves if the SFINAE doesn't resolve.
Another hint is if you want to resolve to the member function if BB is merly castable to A (and not equal to A) you may want to provide a default type for the template:
template <typename BB = A>
void operator++(BB const&) const {/*...*/}
That is only really useful if you provide other opterator++'s to the class, but it is worth noting.
I went and ran your code in Visual Studio 2017 CE as:
int main(){
A<int> a;
B b;
a + b; // calls member
b + a; // class friend
a + a; // surprising result or warning in gcc, hard error in clang
}
And Visual Studio compiled without errors, and ran successfully without warnings and when the program returned it exited with a code of (0)
with this output:
member
friend
member
I've tried this with float, double, char and got the same results.
Even as an extra test I added this after the template class above:
/* your code here */
struct C {};
int main() {
A<C> a;
B b;
a + b;
b + a;
a + a;
return 0;
}
And still got the same results.
As for the later part of your question that pertains to Strange thing #1, #2, #3, #4:
that pertains to both gcc
& clang
I did not have this issue with Visual Studio as a + a
was giving me a member
for the output and the member was taking precedence over the friend overload. Now as for the fact of operator precedence I don't know off the top of my head if GCC
and Clang
will differ from Visual Studio
as each compiler works differently and I'm not real familiar with them but as for the language itself your compiler doesn't know what to do with A::+()
when it doesn't know what kind of <type>
to be using. However it knows what to do when you have A<int>::+()
or A<char>::+()
or A<C>::+()
...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With