ant*_*npp 19 c++ language-lawyer
考虑这段代码:
class Base {
public:
int foo(int x) const { return 2*x; }
};
class Derived : Base {
public:
using Base::foo;
};
Run Code Online (Sandbox Code Playgroud)
现在Derived
有一个公共方法 foo 并且可以调用它
Derived d;
d.foo(2); // compiles (as it should)
Run Code Online (Sandbox Code Playgroud)
但是,如果我通过指针使用该方法,我将无法执行任何操作:
Derived d;
(d.*&Derived::foo)(2); // does not compile because `Derived::foo` expects a pointer to `Base` and `Derived` cannot be casted to its private base class (without a C-style cast).
Run Code Online (Sandbox Code Playgroud)
对于这种行为是否有任何合乎逻辑的解释,或者可能是标准中的疏忽?
Tur*_*ght 13
->*
on aDerived
不能与Base::
成员函数指针一起使用,除非您可以访问private Base
in Derived
(例如,在 的成员函数中Derived
或在声明为 的友元的函数中Derived
)。Derived*
为Base*
这些类型的成员函数指针,即使Base
不可访问(这对于任何 c++ 风格的强制转换都是非法的),例如:\nBase* b = (Base*)&d;\n
Run Code Online (Sandbox Code Playgroud)\n在你的例子中是合法的。Base::
成员函数指针9.9using
声明(强调我的)
\n\n12 [注 5:为了在重载决策期间形成一组候选者,派生类中使用声明命名的函数将被视为派生类的直接成员。特别是,隐式对象参数被视为对派生类的引用,而不是对基类的引用 ( [over.match.funcs] )。这对函数的类型没有影响,并且在所有其他方面该函数仍然是基类的一部分。\xe2\x80\x94 尾注]
\n
因此using
- 声明不会创建foo
for的版本Derived
,但编译器需要假装它是一个Derived::
成员,以便进行重载决策。
因此,这种情况下的相关位是这对函数的类型没有影响Base::
- 即,如果您获取 的地址,您仍然会获得函数指针foo
。
注意:唯一的例外是构造函数
\n->*
与Base::
成员指针一起使用7.6.4 指向成员的指针运算符(重点是我的)
\n\n\n3二元运算符
\n->*
将其第二个操作数(类型为 \xe2\x80\x9c 指向T \xe2\x80\x9d 成员的指针)绑定到其第一个操作数(类型为 \xe2\x80\x9c 指向U \xe2 的指针) \x80\x9d其中U是T或其中T是明确且可访问的基类的类。该表达式E1->*E2
被转换为等价形式(*(E1)).*E2
。
这里的问题是,在您使用指针时foo
不可Base
访问,因此调用不起作用。
实际上,只要满足一些条件,成员函数就需要可转换为任何派生类型:
\n7.3.13 指针到成员的转换(重点是我的)
\n\n\n2 类型为 \xe2\x80\x9c 的纯右值,指向cv T \xe2\x80\x9d类型的B 成员,其中B是类类型,可以转换为类型为 \xe2\x80\x9c 的纯右值,指向 cv T \xe2\x80\x9d 的成员D类型为cv T \xe2\x80\x9d,其中D是从B派生的完整类 ( [class.driven] ) 。如果B是D 的不可访问( [ class.access] )、不明确( [class.member.lookup] ) 或虚拟( [class.mi] ) 基类,或者D的虚拟基类的基类,需要这种转换的程序格式不正确。\n[...]
\n
鉴于这Base
既不像您的示例中那样含糊或虚拟,我们需要关注的唯一问题是可访问性部分。
或者我们呢?
\n该标准实际上有一个我们可以利用的小漏洞:
\n7.6.3 显式类型转换(强制转换符号)(强调我的)
\n\n\n4执行的转换
\n\n
\n- (4.1) a
\nconst_\xc2\xadcast
( [expr.const.cast] ),- (4.2) a
\nstatic_\xc2\xadcast
( [expr.static.cast] ),- (4.3) a
\nstatic_\xc2\xadcast
后接 aconst_\xc2\xadcast
,- (4.4) a
\nreinterpret_\xc2\xadcast
( [expr.reinterpret.cast] ),或- (4.5) a
\nreinterpret_\xc2\xadcast
后接 aconst_\xc2\xadcast
,可以使用显式类型转换的强制转换表示法来执行。适用相同的语义限制和行为,但在以下情况下执行 a 时,即使基类不可访问
\nstatic_\xc2\xadcast
,转换也是有效的:\n
\n- (4.6)指向派生类类型的对象的指针或派生类类型的左值或右值可以分别显式转换为指向明确基类类型的指针或引用;
\n- (4.7)指向派生类类型成员的指针可以显式转换为指向明确非虚拟基类类型成员的指针;
\n- (4.8)指向明确非虚拟基类类型的对象的指针、明确非虚拟基类类型的左值或指向明确非虚拟基类类型的成员的指针可以显式转换为分别是指针、引用或指向派生类类型成员的指针。
\n[...]
\n
因此,虽然任何 C++ 转换方法(如static_cast
/reinterpret_cast
等)不允许从Base::*
to进行转换Derived::*
,但允许使用 c 样式转换来执行该转换,即使基类不可访问也是如此。
例如:
\nint main() {\n Derived d;\n auto fn = &Derived::foo;\n\n // cast to base (only legal with c-style cast)\n // Base* b = static_cast<Base*>(&d); // not legal\n // Base* b = reinterpret_cast<Base*>(&d); // not legal\n Base* b = (Base*)&d; // legal\n (b->*fn)(12);\n\n // cast member function pointer to derived\n // (also only legal with c-style cast)\n using MemFn = int (Derived::*)(int) const;\n // auto fnD = static_cast<MemFn>(fn); // not legal\n // auto fnD = reinterpret_cast<MemFn>(fn); // not legal\n auto fnD = (MemFn)fn; // legal\n (d.*fnD)(12);\n\n // or as a one liner (provided by @KamilCuk in the comments):\n // slightly hard to read, but still legal c++:\n (d.*((int(decltype(d)::*)(int))&decltype(d)::foo))(12); // legal\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n是有效的 c++。
\n因此,只需将Derived
toBase
或成员函数指针转换为绑定到 的指针即可Derived
。
标准中甚至有一个示例正是这样做的:11.8.3 基类和基类成员的可访问性 (3)
\n&Derived::foo
返回Derived::*
memfn指针?因为标准是这么说的。我不知道他们为什么这样决定,但我可以推测潜在的原因可能是什么:
\n您可以检查哪个最派生类实现了给定的成员函数。&Derived::foo
如果返回一个指针,这就会中断Derived::*
。(例如,这可以与 CRTP 一起使用来检查给定Derived
类是否为 的给定成员提供了新定义Base
)\ne.g.:
class Base {\npublic:\n int foo(int x) const { return 2*x; }\n};\n\nclass Derived : private Base {\npublic:\n using Base::foo;\n};\n\ntemplate<class T>\nstruct implementing_class_helper;\n\ntemplate<class T, class R>\nstruct implementing_class_helper<R T::*> {\n typedef T type;\n};\n\ntemplate<class T>\nstruct implementing_class : implementing_class_helper<typename std::remove_cv<T>::type> {\n\n};\n\ntemplate<class T>\nusing implementing_class_t = implementing_class<T>;\n\nint main() {\n static_assert(std::is_same_v<\n typename implementing_class<decltype(&Derived::foo)>::type,\n Base\n >, "Shenanigans!");\n}\n
Run Code Online (Sandbox Code Playgroud)\n如果创建存根函数,例如:
\nclass Base {\npublic:\n int foo(int x) const { return 2*x; }\n};\n\nclass Derived : Base {\npublic:\n // pretending using Base::foo; would result in this:\n int foo(int x) { return Bar::foo(x); }\n};\n
Run Code Online (Sandbox Code Playgroud)\n编译器现在会遇到问题,因为Base::foo
和Derived::foo
是不同的函数,但仍然需要比较等于Base::foo
,因为那是实际的实现。
\n因此编译器需要知道所有using Base::foo;
编译单元中包含的所有类,并确保每当您将它们::foo
与Base::foo
结果进行比较时true
。这听起来像是针对奇怪的边缘情况进行了大量的实施工作。
归档时间: |
|
查看次数: |
609 次 |
最近记录: |