消除朋友和成员二进制运算符的歧义

时间:2018-10-13 12:58:56

标签: c++11 g++ friend clang++ ambiguous-call

使用二进制运算符考虑以下类(我以operator+为例)。

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;}
};

我可以用两种不同的类型来调用此二进制运算符:

A<int> a;
B b;
a + b; // member
b + a; // friend

然后,当我尝试在两面都使用Aa + a)时,会发生很多奇怪的事情。三个编译器对同一代码给出不同的答案。

某些上下文:我不想定义void operator+(A const&),因为如果某些语法不起作用,我需要一个模板来使用SFINAE函数。另外,我也不需要template<class BB, class AA> friend void operator(BB const&, AA const&)。因为A是模板,所以不同的实例化将产生同一模板的多个定义。

继续原始代码:

奇怪的事情1:在gcc中,朋友优先:

a + a; // prints friend in gcc

我希望该成员优先,有一种方法让该成员优先gcc?

奇怪的事情2:在clang中,该代码无法编译:

a + a; // use of overload is ambiguous

这已经指出了gcc和clang之间的不一致,谁是正确的? 对clang来说,使其像gcc一样工作的解决方法是什么?

如果我尝试在参数上更加贪婪,例如要进行一些优化,我可以使用转发引用:

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;}
};

奇怪的事情#3:使用转发引用会在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:"

但是仍然可以编译,如何在gcc或变通方法中使此警告静音?与情况1一样,我希望使用成员函数,但在这里它希望使用朋友函数发出警告。

奇怪的事情#4:使用转发引用会导致clang错误。

a + a; // error: use of overloaded operator '+' is ambiguous (with operand types 'A' and 'A')

谁又指出了gcc和clang之间的不一致,在这种情况下谁是正确的?

总而言之,我正在尝试使此代码始终如一地工作。我真的希望该功能被注入朋友功能(不是免费的朋友功能)。我不想用相同的非模板参数定义一个函数,因为不同的实例化将产生相同函数的重复声明。


这是要使用的完整代码:

#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
}

注意:我正在使用clang version 6.0.1g++ (GCC) 8.1.1 20180712。根据弗朗西斯·库格勒(Francis Cugler)的说法,MSVS 2017 CE的行为有所不同。


我发现了一种变通办法,可以对clang和gcc(对于MSVS?)都做正确的事情(在a+a情况下打印'member'),但是对于样板和人工基类则需要很多工作:

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;}
};

但是,如果我将BB const&替换为BB&&,它仍然会发出歧义。

3 个答案:

答案 0 :(得分:1)

我去了,并在Visual Studio 2017 CE中以以下方式运行您的代码:

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
}

Visual Studio编译时没有错误,并且在没有警告的情况下成功运行,并且当程序返回时,其代码为(0),并显示以下输出:

member
friend
member

我已经尝试过使用float,double,char并获得相同的结果。

即使是一项额外的测试,我也在上面的模板类之后添加了它:

/* your code here */

struct C {};

int main() {
    A<C> a;
    B b;
    a + b;
    b + a;
    a + a;

    return 0;
}

仍然得到相同的结果。


对于与Strange thing #1, #2, #3, #4:gcc都属于clang的问题的后半部分

我在Visual Studio中没有这个问题,因为a + a给我一个member的输出,并且该成员优先于朋友重载。现在,关于运算符优先级的事实,我不知道GCCClang是否会与Visual Studio不同,因为每个编译器的工作方式都不一样,我不是真的熟悉它们,但至于语言本身,编译器在不知道要使用哪种A::+()的情况下也不知道如何处理<type>。但是,如果您拥有A<int>::+()A<char>::+()A<C>::+() ...

,它就会知道该怎么办。

答案 1 :(得分:1)

这些都是模棱两可的。在订购成员和非成员时,例如在GCC中,GCC中存在部分已知的订购错误。 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66914

如果BBA的特长,请约束您的朋友不要参加重载解决方案。

答案 2 :(得分:1)

如果BB可转换为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;}

注意:您需要使用std::enable_if作为类型,以便SFINAE无法解析时函数 declaration 永远不会解析。

另一个提示是,如果BB可转换为A(且不等于A),并且要解析为成员函数,则可能需要为模板提供默认类型:

template <typename BB = A>
void operator++(BB const&) const {/*...*/}

只有在为该类提供其他opterator ++时,这才真正有用,但是值得注意。