tl;博士:
- 声明成员的类是成员函数指针将绑定到的类。
-
->* 上的 Derived 不能与 Base:: 成员函数指针一起使用,除非您可以访问 Derived 中的 private Base(例如在 Derived 的成员函数中或在声明为朋友的函数中Derived)。
- c 样式转换允许您将
Derived* 转换为 Base* 以及这些类型的成员函数指针,即使 Base 不可访问(这对于任何 c++ 样式转换都是非法的),例如:
Base* b = (Base*)&d;
在您的示例中是合法的。
1。为什么你会得到一个Base:: 成员函数指针
9.9 The using declaration(强调我的)
12 [注 5:为了在重载决议期间形成一组候选者,派生类中由 using-declaration 命名的函数被视为它们的直接成员派生类。特别是,隐式对象参数被视为对派生类的引用,而不是对基类的引用 ([over.match.funcs])。 这对函数的类型没有影响,并且在所有其他方面,该函数仍然是基类的一部分。 — 尾注]
因此using-声明不会为Derived 创建foo 的版本,但编译器需要假装它是Derived:: 成员以实现重载解析。
因此,在这种情况下,相关位是 这对函数的类型没有影响 - 也就是说,如果您获取 @ 的地址,您仍然会得到 Base:: 的函数指针987654354@.
注意:唯一的例外是构造函数
2。为什么不能将->* 与Base:: 成员指针一起使用
7.6.4 Pointer-to-member operators(强调我的)
3 二元运算符->* 将其第二个操作数绑定到它的第一个操作数,该操作数的类型应为“T 成员的指针”键入“指向 U 的指针”其中 U 是 T 或 T 是明确和可访问基类。表达式E1->*E2 被转换为等价形式(*(E1)).*E2。
这里的问题是你使用foo指针Base是不可访问,所以调用不起作用。
3。如何让它工作
实际上要求成员函数可以转换为任何派生类型,只要满足以下几个条件:
7.3.13 Pointer-to-member conversions(强调我的)
2类型为“cv T 类型的 B 成员的指针”类型的纯右值,其中B是一个类类型,可以转换为“指向cvD成员的指针类型的纯右值cv T”,其中 D 是从 B 派生的完整类 ([class.derived])。如果 B 是 inaccessible ([class.access])、ambiguous ([class.member.lookup]) 或 virtual (@ 987654329@) D 的基类,或 D 的虚拟基类的基类,需要进行这种转换的程序格式不正确。
[...]
鉴于Base 不像您的示例那样模棱两可或虚拟,我们需要关注的唯一问题是可访问性部分。
我们呢?
其实标准中有一个小漏洞可以利用:
7.6.3 Explicit type conversion (cast notation)(强调我的)
4 由
执行的转换
可以使用显式类型转换的强制转换表示法来执行。 适用相同的语义限制和行为,但在以下情况下执行static_cast 时,即使基类不可访问,转换也是有效的:: p>
-
(4.6)指向派生类类型对象或派生类类型左值或右值的指针可以分别显式转换为指向明确基类类型的指针或引用;
-
(4.7)指向派生类类型成员的指针可以显式转换为指向明确的非虚拟基类类型成员的指针;
-
(4.8) 指向明确非虚拟基类类型对象的指针,明确非虚拟基类类型的左值,或 指向明确非虚拟基类类型成员的指针虚拟基类类型可以分别显式转换为指针、引用或指向派生类类型成员的指针。
[...]
因此,尽管任何 C++ 转换方法(如 static_cast / reinterpret_cast 等)都不允许从 Base::* 转换为 Derived::*,但允许执行 c 样式转换, 即使基类不可访问。
例如:
int main() {
Derived d;
auto fn = &Derived::foo;
// cast to base (only legal with c-style cast)
// Base* b = static_cast<Base*>(&d); // not legal
// Base* b = reinterpret_cast<Base*>(&d); // not legal
Base* b = (Base*)&d; // legal
(b->*fn)(12);
// cast member function pointer to derived
// (also only legal with c-style cast)
using MemFn = int (Derived::*)(int) const;
// auto fnD = static_cast<MemFn>(fn); // not legal
// auto fnD = reinterpret_cast<MemFn>(fn); // not legal
auto fnD = (MemFn)fn; // legal
(d.*fnD)(12);
// or as a one liner (provided by @KamilCuk in the comments):
// slightly hard to read, but still legal c++:
(d.*((int(decltype(d)::*)(int))&decltype(d)::foo))(12); // legal
}
godbolt example
是有效的 c++。
所以只需将Derived 转换为Base 或将成员函数指针转换为绑定到Derived 的指针。
标准中甚至有一个例子可以做到这一点:11.8.3 Accessibility of base classes and base class members (3)
4。为什么&Derived::foo 不返回Derived::* memfn 指针?
因为标准是这样说的。我不知道他们为什么会这样决定,但我可以推测潜在的原因可能是什么:
-
您可以检查哪个派生最多的类实现了给定的成员函数。如果&Derived::foo 返回Derived::* 指针,这将中断。 (例如,这可以与 CRTP 一起使用来检查给定的Derived 类是否为Base 的给定成员提供了新定义)
例如:
class Base {
public:
int foo(int x) const { return 2*x; }
};
class Derived : private Base {
public:
using Base::foo;
};
template<class T>
struct implementing_class_helper;
template<class T, class R>
struct implementing_class_helper<R T::*> {
typedef T type;
};
template<class T>
struct implementing_class : implementing_class_helper<typename std::remove_cv<T>::type> {
};
template<class T>
using implementing_class_t = implementing_class<T>;
int main() {
static_assert(std::is_same_v<
typename implementing_class<decltype(&Derived::foo)>::type,
Base
>, "Shenanigans!");
}
-
如果要创建存根函数,例如:
class Base {
public:
int foo(int x) const { return 2*x; }
};
class Derived : Base {
public:
// pretending using Base::foo; would result in this:
int foo(int x) { return Bar::foo(x); }
};
编译器现在会出现问题,因为Base::foo 和Derived::foo 是不同的函数,但仍需要比较等于Base::foo,因为这是实际的实现。
因此,编译器需要知道所有编译单元中包含using Base::foo; 的所有类,并确保无论何时将它们的::foo 与Base::foo 进行比较,结果都是true。对于一个奇怪的边缘情况,这听起来像是很多实现工作。