无法使用来自私有基的公共成员函数的指针

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

长话短说:

\n
    \n
  • 声明成员的类是将成员函数指针绑定到的类。
  • \n
  • ->*on aDerived不能与Base::成员函数指针一起使用,除非您可以访问private Basein Derived(例如,在 的成员函数中Derived或在声明为 的友元的函数中Derived)。
  • \n
  • c 风格的强制转换允许您转换Derived*Base*这些类型的成员函数指针,即使Base不可访问(这对于任何 c++ 风格的强制转换都是非法的),例如:\n
    Base* b = (Base*)&d;\n
    Run Code Online (Sandbox Code Playgroud)\n在你的例子中是合法的。
  • \n
\n
\n

1. 为什么会得到Base::成员函数指针

\n

9.9using声明(强调我的)

\n
\n

12 [注 5:为了在重载决策期间形成一组候选者,派生类中使用声明命名的函数将被视为派生类的直接成员。特别是,隐式对象参数被视为对派生类的引用,而不是对基类的引用 ( [over.match.funcs] )。这对函数的类型没有影响,并且在所有其他方面该函数仍然是基类的一部分。\xe2\x80\x94 尾注]

\n
\n

因此using- 声明不会创建foofor的版本Derived,但编译器需要假装它是一个Derived::成员,以便进行重载决策。

\n

因此,这种情况下的相关位是这对函数的类型没有影响Base::- 即,如果您获取 的地址,您仍然会获得函数指针foo

\n

注意:唯一的例外是构造函数

\n

2. 为什么不能->*Base::成员指针一起使用

\n

7.6.4 指向成员的指针运算符(重点是我的)

\n
\n

3二元运算符->*将其第二个操作数(类型为 \xe2\x80\x9c 指向T \xe2\x80\x9d 成员的指针)绑定到其第一个操作数(类型为 \xe2\x80\x9c 指向U \xe2 的指针) \x80\x9d其中UT或其中T明确可访问的基类的类。该表达式E1->*E2被转换为等价形式(*(E1)).*E2

\n
\n

这里的问题是,在您使用指针时foo不可Base访问因此调用不起作用。

\n
\n

3. 如何让它发挥作用

\n

实际上,只要满足一些条件,成员函数就需要可转换为任何派生类型:

\n

7.3.13 指针到成员的转换(重点是我的)

\n
\n

2 类型为 \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
\n

鉴于这Base既不像您的示例中那样含糊或虚拟,我们需要关注的唯一问题是可访问性部分。

\n

或者我们呢?

\n

该标准实际上有一个我们可以利用的小漏洞:

\n

7.6.3 显式类型转换(强制转换符号)(强调我的)

\n
\n

4执行的转换

\n\n

可以使用显式类型转换的强制转换表示法来执行。适用相同的语义限制和行为,但在以下情况下执行 a 时,即使基类不可访问static_\xc2\xadcast,转换也是有效的

\n
    \n
  • (4.6)指向派生类类型的对象的指针或派生类类型的左值或右值可以分别显式转换为指向明确基类类型的指针或引用;
  • \n
  • (4.7)指向派生类类型成员的指针可以显式转换为指向明确非虚拟基类类型成员的指针;
  • \n
  • (4.8)指向明确非虚拟基类类型的对象的指针、明确非虚拟基类类型的左值或指向明确非虚拟基类类型的成员的指针可以显式转换为分别是指针、引用或指向派生类类型成员的指针
  • \n
\n

[...]

\n
\n

因此,虽然任何 C++ 转换方法(如static_cast/reinterpret_cast等)不允许从Base::*to进行转换Derived::*,但允许使用 c 样式转换来执行该转换,即使基类不可访问也是如此

\n

例如:

\n
int 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

因此,只需将DerivedtoBase或成员函数指针转换为绑定到 的指针即可Derived

\n

标准中甚至有一个示例正是这样做的:11.8.3 基类和基类成员的可访问性 (3)

\n
\n

4. 为什么不&Derived::foo返回Derived::*memfn指针?

\n

因为标准是这么说的。我不知道他们为什么这样决定,但我可以推测潜在的原因可能是什么:

\n
    \n
  • 您可以检查哪个最派生类实现了给定的成员函数。&Derived::foo如果返回一个指针,这就会中断Derived::*。(例如,这可以与 CRTP 一起使用来检查给定Derived类是否为 的给定成员提供了新定义Base)\ne.g.:

    \n
    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
  • \n
  • 如果创建存根函数,例如:

    \n
    class 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::fooDerived::foo是不同的函数,但仍然需要比较等于Base::foo,因为那是实际的实现。
    \n因此编译器需要知道所有using Base::foo;编译单元中包含的所有类,并确保每当您将它们::fooBase::foo结果进行比较时true。这听起来像是针对奇怪的边缘情况进行了大量的实施工作。

    \n
  • \n
\n

  • `(d.*((int(decltype(d)::*)(int))&amp;decltype(d)::foo))(12);` 这很有趣 (2认同)
  • @KamilCuk 绝对应该因为默默无闻而获得奖项 :D 但它仍然是合法的 c++ :) (2认同)