朋友和成员二元运算符的消歧
Disambiguation of friend and member binary operator
考虑以下带有二元运算符的 class(我使用 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
然后当我尝试在两边使用 A
(a + 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 一样,我希望更喜欢成员函数,但在这里它更喜欢朋友函数 and 发出警告。
奇怪的事 #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.1
和 g++ (GCC) 8.1.1 20180712
。根据 Francis Cugler 的说法,MSVS 2017 CE 给出了不同的行为。
我找到了一个解决方法,可以为 clang 和 gcc(对于 MSVS?)做正确的事情(打印 'member' for a+a
case),但是它需要很多样板和一个人工基地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;}
};
但是,如果我将 BB const&
替换为 BB&&
,它仍然会发出模棱两可的调用。
我去 运行 你在 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 试过了,得到了相同的结果。
作为额外的测试,我在上面的模板 class 之后添加了这个:
/* 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
作为输出并且成员优先于朋友超载。现在关于运算符优先级的事实,我不知道 GCC
和 Clang
是否与 Visual Studio
不同,因为每个编译器的工作方式不同,我不是真的熟悉它们,但至于语言本身,您的编译器在不知道要使用哪种 <type>
时不知道如何处理 A::+()
。但是,当您有 A<int>::+()
或 A<char>::+()
或 A<C>::+()
...
时,它知道该怎么做
这些都是模棱两可的。在对成员和非成员进行排序时,GCC 中存在已知的部分排序错误,例如https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66914.
如果 BB
是 A
的特化,就限制你的朋友不参与重载决议。
如果 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 未解析时永远不会解析。
另一个提示是,如果 BB 可转换为 A(且不等于 A),如果您想解析成员函数,您可能需要为模板提供默认类型:
template <typename BB = A>
void operator++(BB const&) const {/*...*/}
只有当您向 class 提供其他运算符++时,这才是真正有用的,但值得注意。
考虑以下带有二元运算符的 class(我使用 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
然后当我尝试在两边使用 A
(a + 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 一样,我希望更喜欢成员函数,但在这里它更喜欢朋友函数 and 发出警告。
奇怪的事 #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.1
和 g++ (GCC) 8.1.1 20180712
。根据 Francis Cugler 的说法,MSVS 2017 CE 给出了不同的行为。
我找到了一个解决方法,可以为 clang 和 gcc(对于 MSVS?)做正确的事情(打印 'member' for a+a
case),但是它需要很多样板和一个人工基地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;}
};
但是,如果我将 BB const&
替换为 BB&&
,它仍然会发出模棱两可的调用。
我去 运行 你在 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 试过了,得到了相同的结果。
作为额外的测试,我在上面的模板 class 之后添加了这个:
/* 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
作为输出并且成员优先于朋友超载。现在关于运算符优先级的事实,我不知道 GCC
和 Clang
是否与 Visual Studio
不同,因为每个编译器的工作方式不同,我不是真的熟悉它们,但至于语言本身,您的编译器在不知道要使用哪种 <type>
时不知道如何处理 A::+()
。但是,当您有 A<int>::+()
或 A<char>::+()
或 A<C>::+()
...
这些都是模棱两可的。在对成员和非成员进行排序时,GCC 中存在已知的部分排序错误,例如https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66914.
如果 BB
是 A
的特化,就限制你的朋友不参与重载决议。
如果 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 未解析时永远不会解析。
另一个提示是,如果 BB 可转换为 A(且不等于 A),如果您想解析成员函数,您可能需要为模板提供默认类型:
template <typename BB = A>
void operator++(BB const&) const {/*...*/}
只有当您向 class 提供其他运算符++时,这才是真正有用的,但值得注意。