How to call "friends" properly?
开头
有一天看到类似这样的代码(隐藏了业务逻辑,对函数签名进行了简化):
#include <cstdio>
struct Foo {
friend Foo max(Foo a, Foo b);
};
Foo max(Foo a, Foo b) {
printf("friend max\n");
return a;
}
Foo max(Foo a, Foo b, int x = 0) {
printf("global max\n");
return a;
}
int main(int argc, char **argv) {
Foo a;
Foo b;
Foo c = max(a, b); // ambiguous
}
很明显无法编译:
error: call of overloaded 'max(Foo&, Foo&)' is ambiguous
20 | Foo c = max(a, b); // ambiguous
| ~~~^~~~~~
Tip
这个 max(Foo, Foo, int)
看起来有点奇怪,但是如果是 max(Foo, Foo, cudaStream_t)
,你可能就理解了。没理解也没关系,不影响阅读。
用强制类型转换解决函数名字歧义
想要选择第二个重载非常简单,只需要给出第三个参数即可。但是我当时需要选择第一个重载。GPT-4o 建议我完整地给出函数的签名:
#include <cstdio>
struct Foo {
friend Foo max(Foo a, Foo b);
};
Foo max(Foo a, Foo b) {
printf("friend max\n");
return a;
}
Foo max(Foo a, Foo b, int x = 0) {
printf("global max\n");
return a;
}
int main(int argc, char **argv) {
Foo a;
Foo b;
Foo c = ((Foo(*)(Foo, Foo))max)(a, b); // prints "friend max"
}
很好,这成功了。
What if they are hidden?
但是如果友元函数是 hidden friends 呢?
#include <cstdio>
struct Foo {
friend Foo max(Foo a, Foo b) {
printf("friend max\n");
return a;
}
};
Foo max(Foo a, Foo b, int x = 0) {
printf("global max\n");
return a;
}
int main(int argc, char **argv) {
Foo a;
Foo b;
Foo c = ((Foo(*)(Foo, Foo))max)(a, b); // ???
}
很奇怪,虽然能够编译,但结果是 global max
。把 x=0
这个默认参数改成 x
,即不给出默认参数,发现也能编译成功,原来是 C 风格的强制类型转换把外面的 max
转换成了我们想要的类型。可以用 static_cast
来解决这个问题:
#include <cstdio>
struct Foo {
friend Foo max(Foo a, Foo b) {
printf("friend max\n");
return a;
}
};
Foo max(Foo a, Foo b, int x = 0) {
printf("global max\n");
return a;
}
int main(int argc, char **argv) {
Foo a;
Foo b;
Foo c = (static_cast<Foo(*)(Foo, Foo)>(max))(a, b);
}
结果是:
error: invalid 'static_cast' from type 'Foo(Foo, Foo, int)' to type 'Foo (*)(Foo, Foo)'
18 | Foo c = (static_cast<Foo(*)(Foo, Foo)>(max))(a, b);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这是因为在类中声明的友元函数只能被 ADL 查找到,用类型转换就不适用于 ADL 的规则。在类中直接定义友元函数,或在类中声明友元函数但在本翻译单元中不给出其定义,都会导致无法找到友元函数。
更糟糕的是友元函数并不被编译器认为在类中定义,所以不能通过限定性名称查找来指定。事实上,ADL 属于非限定性名称查找!
最后我的想法是不要使用 hidden friends,解除 ADL 的限制:
#include <cstdio>
struct Foo {
friend Foo max(Foo a, Foo b);
};
Foo max(Foo a, Foo b); // May be defined in another translation unit.
/* Other code. */
Foo max(Foo a, Foo b, int x = 0) {
printf("global max\n");
return a;
}
int main(int argc, char **argv) {
Foo a;
Foo b;
Foo c = (static_cast<Foo(*)(Foo, Foo)>(max))(a, b);
}
参考这个 回答,应该可以认为 hidden friend 指的是(对于当前翻译单元来说)只在类中声明过的友元函数。
把 hidden friends 变得不再隐藏其实很简单,就算是已经在类中给出了定义,还是可以声明一下的:
#include <cstdio>
struct Foo {
friend Foo max(Foo a, Foo b) {
printf("friend max\n");
return a;
}
};
Foo max(Foo a, Foo b);
Foo max(Foo a, Foo b, int x = 0) {
printf("global max\n");
return a;
}
int main(int argc, char **argv) {
Foo a;
Foo b;
Foo c = (static_cast<Foo(*)(Foo, Foo)>(max))(a, b);
}
在 Compiler Explorer 中给出以上内容的 完整代码。
为什么在我们工程创建的共享库中就不成功了?
2024 年 11 月 20 日:遇到一个情况,友元函数的定义是在共享库中,而且不在全局名字空间,这个办法不会成功。但不知道是哪个因素导致了问题。修改 Compiler Explorer 上面的代码 来尝试重现这个问题,但是却能正常编译链接,找不到问题所在。
后来发现编译得到的目标文件中符号是这样的(objdump -Ct xxx.o
或者 readelf -WCs xxx.o
,如果用 readelf
则可能需要高于某个版本才能使用 -C
选项,加 -W
是用来完整打印而不按宽度截断),有一个 .hidden
标记:
0000000000000000 *UND* 0000000000000000 .hidden A::max(A::Foo, A::Foo)
这导致链接器希望在本地,而不是共享库中找到这个函数。
但是 Compiler Explorer 上做的实验中, foo 函数的可见性是公开的:
34: 0000000000000000 0 FUNC GLOBAL DEFAULT UND A::max(A::Foo, A::Foo)
然后我就在本地改成如下声明就能成功编译链接了:
namespace A {
__attribute__ ((visibility ("default"))) Foo max(Foo a, Foo b);
}
2024 年 12 月 1 日:我现在总算知道为什么要设置这个选项了,原来其他同学在头文件里面加了一条:
#pragma GCC visibility push(hidden)
不知其用意……
其他参考
https://stackoverflow.com/a/4492524/
Every name first declared in a namespace is a member of that namespace. If a friend declaration in a non-local class first declares a class or function (this implies that the name of the class or function is unqualified) the friend class or function is a member of the innermost enclosing namespace.
(因为 Obsidian 渲染 <>
有问题,不得不把代码块放在引用外。)
// Assume f and g have not yet been defined.
void h(int);
template <class T> void f2(T);
namespace A {
class X {
friend void f(X); // A::f(X) is a friend
class Y {
friend void g(); // A::g is a friend
friend void h(int); // A::h is a friend
// ::h not considered
friend void f2<>(int); // ::f2<>(int) is a friend
};
};
// A::f, A::g and A::h are not visible here
X x;
void g() { f(x); } // definition of A::g
void f(X) { /* ... */} // definition of A::f
void h(int) { /* ... */ } // definition of A::h
// A::f, A::g and A::h are visible here and known to be friends
}