Search code examples
c++g++clang++friendambiguous-call

Disambiguation of friend and member binary operator


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&&.


Solution

  • 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.